Pass common object to nested structs

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

3 Likes