Central Factory with lazy closures for expensive dependencies
byob-factory-di.1
factory-di
Problem: package-level globals (config, DB handle, HTTP client) make
commands hard to test, hide the dependency surface, and force lifecycle
decisions at init time. At the other extreme, eagerly constructing every
dependency in main() means mytool --version and mytool --help pay
the cost of opening a database they never touch.
Idea: define a Factory struct that holds every cross-cutting
dependency. Cheap dependencies (IOStreams, prompter) are eager fields.
Expensive dependencies (config, store, HTTP client) are
func() (T, error) closures — lazily invoked only by commands that
actually need them. The factory is constructed once in main() and
threaded into every command constructor; no command ever touches a
global.
This gives you three wins in one shape:
- Testability — swap factory fields for fakes in tests, no globals to reassign.
- Explicit dependency surface —
grep NewCmdXxxshows every command's signature and what it touches viaf. - Cold-start latency —
mytool --helpnever opens the store because nothing callsf.Store()on the help path.
Tradeoffs: callers must remember f.Store() (invoke) rather than
f.Store (field access). One line of boilerplate per command constructor.
Worth it after the second command.
When not to use: a single-command tool with one dependency. At that
scale the factory is ceremony; a plain struct literal in main() is
fine.
Design
type Factory struct {
IOStreams *iostreams.IOStreams // eager, cheap
Prompter prompt.Prompter // eager, cheap, interface
Config func() (*config.Config, error) // lazy
Store func() (store.Store, error) // lazy
HTTPClient func() (*http.Client, error) // lazy
}
func New() *Factory {
ios := iostreams.System()
return &Factory{
IOStreams: ios,
Prompter: prompt.NewLive(ios),
Config: lazyConfig(),
Store: lazyStore(),
HTTPClient: lazyHTTPClient(),
}
}
// Every command takes *Factory:
func NewCmdList(f *Factory, runF func(*Options) error) *cobra.Command {
opts := &Options{IO: f.IOStreams, Store: f.Store}
// ...
}
// Usage inside a command:
cfg, err := f.Config()
if err != nil { return err }
s, err := f.Store()
See also: sync-oncevalue memory for the idiomatic way to implement
each lazy closure.