Hi,
I would consider Functional Options Pattern. On the positive, it allows condensing multiple constructors into a single one, with possibility of extending it later, without breaking client code, and provides clients with sensible defaults (globalLogger in your example). Downside is that the code looks a bit difficult, until you get the hang of it.
First you need to add two options structs for each of your structs, which contain all defaultable options
type FooOptions struct {
logger Logger
}
...
type BarOptions struct {
logger Logger
Foo *Foo
}
Then, keep only NewFoo and NewBar constructors as follows
// Params before options - 'a' in this case,
//those without which service cannot work,
// and you, as implementor, cannot know defaults for
// (for example, an API Key)
// options - for params that can be defaulted (like Logger)
func NewFoo(a int, options ...func(*FooOptions)) *Foo {
// define defaults
ops := &FooOptions{}
ops.logger = globalLogger
// overwrite configuration with provided options
for _, option := range options {
option(ops)
}
// configure Foo based on final set of options
return &Foo{a: a, logger: ops.logger}
}
...
func NewBar(a int, b bool, options ...func(*BarOptions)) *Bar {
// define defaults
ops := &BarOptions{}
ops.logger = globalLogger
ops.Foo = NewFoo(a) // Foo With Default Logger
// overwrite configuration with provided options
for _, option := range options {
option(ops)
}
// configure Foo based on final set of options
return &Bar{b: b, logger: ops.logger, foo: ops.Foo}
}
Now, you can define options funcs as follows, for convenience
func FooWithLogger(logger Logger) func(*FooOptions) {
return func(ops *FooOptions) {
ops.logger = logger
}
}
...
func BarWithLogger(logger Logger) func(*BarOptions) {
return func(ops *BarOptions) {
ops.logger = logger
}
}
func BarWithFoo(foo *Foo) func(*BarOptions) {
return func(ops *BarOptions) {
ops.Foo = foo
}
}
And now your client can use it like shown below
func main() {
// now you can either have bar with default logger, and default Foo
_ = NewBar(2, true)
// or provide everything
_ = NewBar(2, false, BarWithLogger(Logger{}), BarWithFoo(NewFoo(3, FooWithLogger(Logger{}))))
}
Hope this helps