Skip to content

Latest commit

 

History

History
175 lines (125 loc) · 6.01 KB

Readme.md

File metadata and controls

175 lines (125 loc) · 6.01 KB

Katana

Build Status

Dependency Injection Driven By Constructor Functions

Brief Overview

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.

Example

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)

Injecting Interfaces

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.

Thread-Safety

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.

Example: HTTP Server

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))

Injecting Function Arguments

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]
}

Contributing

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 :~