Akisa (Container)
Container is an implementation of an IoC (inversion of control) pattern which adheres to the dependency inversion principle of the SOLID design principles.
An IoC container facilitates dependency injection (DI) which allows us to remove dependencies from our code. It is a pattern that allows developers hot swap dependencies without breaking our application.
— Built by Samuel Oloruntoba of Rafdel as part of the Akisa project.
We recommend locking to SemVer using Go's package manager dep
dep ensure -add go.rafdel.co/akisa/container
First, create a new instance of the container. You can head over to GoDoc for more info.
package main
import "go.rafdel.co/akisa/container"
func main() {
c := container.New()
// ...
}
After creating a container instance, we can bind dependencies to the container.
c.Bind("stuff", "nonsense")
The Bind()
call above binds a dependency "nonsense" to our container using the abstract (or identifier) "stuff". So, when we try to get a dependency from the container using the abstract "stuff", it'll return "nonsense".
To get the dependency from the container, we do:
dependency, err := c.Make("stuff")
We can also use another helper function Get()
:
dependency := c.Get("stuff")
Note: Get()
will panic if the dependency isn't found
One of the most important bindings we can do is binding an interface to a concrete. If you use an interface as an abstract, the concrete must implement the interface.
c.Bind(new(Formatter), HTMLFormatter{})
Doing this tells the container that whenever you need a new instance of the Formatter
interface, the HTMLFormatter
struct should be returned as the concrete.
structs
Just as we can use an interface as the abstract, we can also use a struct. Note that if you use a struct as an abstract, the concrete must be nil
otherwise, the container will panic
.
c.Bind(Formatter{}, nil)
The abstract and concrete can be of any type — but, avoid using maps, slices and arrays as abstracts as they can be quite problematic to resolve from the container.
Binding functions to the container takes a different route.
c.Bind("stuff", func() string {
return "nonsense"
})
When we try to get the binding above, the container will automatically call the function and return the return value from the function call.
c.Get("stuff") // nonsense
passing arguments
c.Bind("sum", func(a, b int) int {
return a + b
})
If we try to get c.Get("sum")
above, the invocation will fail as the function binding requires parameters to be passed to it. We can pass parameters to function bindings using:
c.Make("sum", 10, 20)
automatic injection
Taking advantage of the dependency injection of our application, we can also resolve dependencies from the container.
For example, we can do:
c.Bind(new(Formatter), HTMLFormatter{})
c.Bind("formatter", func(f Formatter) {
// ...
})
When we try to c.Get("formatter")
, the container will read the function and inject the dependencies from the container into the function. It'll panic if it cannot find the dependency.
If a bound function returns multiple values, an array of those values is returned. We can also resolve the "formatter" dependency above using Make()
. If we don't pass parameters to Make()
, it'll try to auto-resolve binding dependencies from the container.
Currently, when you get a binding from the container — a new instance is returned. So if we pass a function as the concrete, a new instance of that function is returned.
If we have:
c.Bind("random", func() int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(1000000)
})
Whenever you call Get("random")
, a random value will be returned. For times where we want the value returned to be the same throughout the application cycle, you'd use Singleton()
or BindShared()
instead of Bind()
c.Singleton("not.so.random", func() int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(1000000)
})
When you Get("not.so.random")
from the container, the same value will be returned every time because singletons are shared instances.
With Invoke()
, we pass in a function (or struct method) and any interface we pass to function as arguments will automatically get resolved.
value := c.Invoke(func(f Formatter) string {
return "one step at a time"
})
This is useful for times when we just want to resolve a couple of dependencies but do not want to bind the result.
We can give dependencies different names using the Alias()
method. This can serve as a means to shorten the name of the dependency.
c.Bind(new(Formatter), HTMLFormatter{})
c.Alias(new(Formatter), "formatter")
Aliasing doesn't affect existing bindings, it will only create a pointer to the underlying binding. After creating an alias, we can resolve it like we normally would:
c.Get("formatter")
Attempting to alias a non-existent abstract will cause a panic.
To check if a binding is present in a container, you can use the Has()
method.
if c.Has(new(Formatter)) {
fmt.Printf("All is right with the world")
}
It also works for aliases
When we resolve a dependency like a struct or map or something similar from the container, Go doesn't know which type to use for the dependency since the Make()
or Get()
function returns an interface{}
type.
If we do this:
c.Bind("stuff", map[string]string{"hello": "world"})
we can't do this:
c.Get("stuff")["hello"]
This invocation will fail as Go doesn't see the type of the binding. Instead, we could do this:
stuff := c.Get("stuff").(map[string]string)
stuff["hello"]
By telling Go to use that the map[string]string
type, Go can check to see if the interface below can be converted to map[string]string
and if it can, you regain previous functionality.
This issue doesn't apply to scalar types like strings, ints, floats, booleans or what have you. The same isn't true of structs, maps, arrays etc.
For questions and support feel free to send me a message on Twitter or create a new issue. If you discovered a bug or would like to make a feature request, you can create a new issue explaining the bug/feature.
- Uber's dig is a good DI container
This project uses the MIT license.