Dependency Injection Driven By Constructor Functions
katana approaches DI in a fairly simple manner. For each type that needs to be available for injection -- a.k.a injectable
-- a constructor function needs to be registered with an instance of kanata.Injector
.
func NewUserService(depA *DependencyA, depB *DependencyB) *UserService {
return &UserService{depA, depB}
}
Once a provider is registered the corresponding injectable can be resolved and injected as dependency into other injectable providers, or even into arbitrary functions. Lets see how that translates into code:
// Get an instance of katana's injector
injector := katana.New()
// Register the following instances as injectables
depA, depB := &DependencyA{}, &DependencyB{}
// Register a constructor function to provide instances of *UserService
injector.Provide(depA, depB).ProvideNew(&UserService{}, NewUserService)
// Grab a new instance of *UserService with all its dependencies injected
var service *UserService
injector.Resolve(&service)
Katana will detect and panic upon any eventual cyclic dependency
when resolving an injectable, providing the cyclic dependency graph so you can easily troubleshoot.
Lets say you have the following types each with their own dependencies:
type Config struct {
DatastoreURL string
CacheTTL int
Debug bool
}
type Cache struct {
TTL int
}
type Datastore struct {
Cache *Cache
URL string
}
type AccountService struct {
Datastore *Datastore
}
A constructor function for each type of injectable is created and registered with a new instance of katana.Injector
// Grabs a new instance of katana.Injector
injector := katana.New()
// Registers the given instance of Config to be provided as a singleton injectable
injector.Provide(Config{
DatastoreURL: "https://myawesomestartup.com/db",
CacheTTL: 20000,
})
// Registers a constructor function that always provides a new instance of *Cache
injector.ProvideNew(&Cache{}, func(config Config) *Cache {
return &Cache{config.CacheTTL}
})
// Registers a constructor function that always provides a new instance of *Datastore
// resolving its dependencies -- Config and *Cache -- as part of the process
injector.ProvideNew(&Datastore{}, func(config Config, cache *Cache) *Datastore {
return &Datastore{cache, config.DatastoreURL}
})
// Registers a constructor function that lazily provides the same instance of *AccountService
// resolving its dependencies -- *Datastore -- as part of the process.
injector.ProvideSingleton(&AccountService{}, func(db *Datastore) *AccountService {
return &AccountService{db}
})
Finally you can get instances of the provided injectables
with all their dependencies -- if any -- resolved:
var service1, service2 *AccountService
var db1, db2 *Datastore
var cache1, cache2 *Cache
var config Config
// Katana allows you to resolve multiple instances on a single "shot"
//
// Note that:
// 1. service1 == service2: *AccountService provider is a singleton
// 2. db1 != db2: *Datastore injectable is not singleton
// 3. cache1 != cache2: *Cache is not a singleton
// 4. config will point to the Config instance defined in the previous code block, since it was provided using Injector#Provide method.
injector.Resolve(&service1, &service2, &db1, &db2, &cache1, &cache2, &config)
In Go there is no way to pass in types as function arguments and types are derived through reflection from actual instances.
In addition to that an interface cannot be instantiated either, which makes things a little trick when writing generic code like a DI container.
Katana solution for injecting into interface references might seem a bit strange at first, but you'll get used :)
Lets say we want to provide a particular implementation of http.ResponseWriter
to be injected as dependency. With katana
you would do the following:
injector.ProvideAs((*http.ResponseWriter)(nil), writer)
(*http.ResponseWriter)(nil)
is how we tell katana to treat writer
as a http.ResponseWriter
rather than its actual underlying implementation *http.response
.
With that whenever a dependency to http.ResponseWriter
is detected, it will be resolved as that particular writer
instance.
In order to use katana
in a multi-thread
environment you should use a copy of the injector per thread.
Copies of katana.Injector
can be created using Injector.Clone()
. This copy will have all the registered providers of the original injector and every new provider registered in the new copy will not be available to other copies of katana.Injector
.
Note Singleton providers will still yield the same instances across different threads.
Assuming we have the injector instance from the example above ^
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
var service *AccountService
injector.Clone().
ProvideAs((*http.ResponseWriter)(nil), w).
Provide(r).Resolve(&service)
})
log.Fatal(http.ListenAndServe(":8080", nil))
Katana also allows you to inject arguments into functions (that is how it resolves the arguments of a injectable provider):
fetchAllAccounts := injector.Inject(func(srv *AccountService, conf Config) ([]*Account, error) {
if conf.Debug {
return mocks.Accounts(), nil
}
return srv.Accounts()
})
Injector#Inject
returns a closure holding all the resolved function arguments and when called returns a katana.Output
with the function returning values.
if result := fetchAllAccounts(); !result.Empty() {
accounts, err := result[0], result[1]
}
Please feel free to submit issues, fork the repository and send pull requests!
When submitting an issue, please include a test function that reproduces the issue, that will help a lot to reduce back and forth :~