Reputation: 803
When implementing functional options for different types to enable easier configuration, I want to share some common options between these types.
Considering the following simple example (where name
represents a common complex shared type and id
and location
represent differences in the types):
type A struct{
Name string
ID int
}
type OptionFuncA func(*A)
type B struct{
Name string
Location string
}
type OptionFuncB func(*B)
func NewA(options ...OptionFuncA) A {
obj := A{}
for _, option := range options {
option(&obj)
}
return obj
}
func NewB(options ...OptionFuncB) B {
obj := B{}
for _, option := range options {
option(&obj)
}
return obj
}
In this example I want to add functional options to set the name
of types A
and B
. I can do it like that:
type OptionA struct{}
type OptionB struct{}
func (opt OptionA) WithName(name string) OptionFuncA {
return func(a *A) {
a.Name = name
}
}
func (opt OptionB) WithName(name string) OptionFuncB {
return func(b *B) {
b.Name = name
}
}
func (opt OptionA) WithID(id int) OptionFuncA {
return func(a *A) {
a.ID = id
}
}
func (opt OptionB) WithLocation(location string) OptionFuncB {
return func(b *B) {
b.Location = location
}
}
This allows me to use the code like this:
optA := OptionA{}
a := NewA(optA.WithName("A"), optA.WithID(1))
optB := OptionB{}
b := NewB(optB.WithName("B"), optB.WithLocation("location"))
I want to have type checking on the different options for type A
and B
, while avoiding duplication of code which is equal for different types. Since functional options require to return the fitting function type OptionFuncA
and OptionFuncB
for type A
and B
respectively, I am wondering whether a solution exists.
Alternatively, I want to define a common interface, which different OptionX
types have to implement:
// Assumption: Option is the common interface
// with options for multiple types.
var _ Option = OptionA{}
var _ Option = OptionB{}
While the actual code for setting the name
in this example obviously can be shared, I cannot find a solution to define a common interface for the functional options of both types. The different required return value for the functional options does not allow it from my pov.
Is it somehow possible to enforce that OptionA
and OptionB
have to implement some shared options, without loosing type safety for all other available options of a specific type?
Upvotes: 3
Views: 776
Reputation: 5897
(Disclosure: I am the owner of the repo)
This might not solve your current problem, but it could be helpful for future solutions. https://github.com/kazhuravlev/options-gen lets you define a structure, run go generate ./..., and automatically generate With** functions for each structure field.
For example
package example
import "net/http"
//go:generate options-gen -from-struct=Options
type Options struct {
httpClient *http.Client `validate:"required"`
token string
// Address that will be used for each request to the remote server.
addr string
}
After go generate ./...
you will get a several functions. One for each structure field.
package example
func WithHttpClient(opt *http.Client) OptOptionsSetter {
return func(o *Options) { o.httpClient = opt }
}
func WithToken(opt string) OptOptionsSetter {
return func(o *Options) { o.token = opt }
}
// Address that will be used for each request to the remote server.
func WithAddr(opt string) OptOptionsSetter {
return func(o *Options) { o.addr = opt }
}
Upvotes: -1
Reputation: 4072
If that fits your design, you can create semantic building blocks with embeddable structs. Then unified option could be implemented as a collection of options for those embeddings.
Such approach leverages composition and improves code consistency by establishing a single owner of domain data (e.g. WithName
is a single owner of name data as opposed to A and B had their own Name which were not syntactically related).
https://play.golang.org/p/DZMbqXVVurs
package main
import "fmt"
type WithName struct {
FirstName string
LastName string
}
type WithAge struct{ Age int }
type WithID struct{ ID int }
type WithLocation struct{ Location string }
type Option struct {
WithNameFunc func(v *WithName)
WithAgeFunc func(v *WithAge)
WithIDFunc func(v *WithID)
WithLocationFunc func(v *WithLocation)
}
func Name(first, last string) Option {
return Option{
WithNameFunc: func(v *WithName) {
v.FirstName = first
v.LastName = last
},
}
}
func ID(id int) Option {
return Option{
WithIDFunc: func(v *WithID) {
v.ID = id
},
}
}
type A struct {
WithName
WithID
}
type B struct {
WithName
WithAge
WithLocation
}
func NewA(options ...Option) A {
obj := A{}
for _, option := range options {
if option.WithNameFunc != nil {
option.WithNameFunc(&obj.WithName)
}
if option.WithIDFunc != nil {
option.WithIDFunc(&obj.WithID)
}
}
return obj
}
func NewB(options ...Option) B {
obj := B{}
for _, option := range options {
if option.WithNameFunc != nil {
option.WithNameFunc(&obj.WithName)
}
if option.WithAgeFunc != nil {
option.WithAgeFunc(&obj.WithAge)
}
if option.WithLocationFunc != nil {
option.WithLocationFunc(&obj.WithLocation)
}
}
return obj
}
func main() {
a := NewA(Name("John", "Doe"), ID(123))
fmt.Println(a.FirstName, a.LastName, a.ID) // John Doe 123
}
Upvotes: 2
Reputation: 711
If the "Name" of A and B has the same meaning, it could be combined like this in my opinion.
type X struct {
Name string
ID int
Location string
}
func NewA(options ...OptionFuncX) A {
obj := X{}
for _, option := range options {
option(&obj)
}
return A{
Name: obj.Name,
ID: obj.ID,
}
}
So WithName
could be single. But I guess this is not what you are looking for.
Upvotes: 2