How will dependency injection help me?
Dependency injection is one form of the broader technique of inversion
of control. It is used to increase modularity of the program and make it
extensible.
Contents
Installing
go get -u github.com/defval/inject/v2
This library follows SemVer strictly.
Tutorial
Let's learn to use Inject by example. We will code a simple application
that processes HTTP requests.
The full tutorial code is available here
Providing
To start, we will need to create two fundamental types: http.Server
and http.ServeMux
. Let's create a simple constructors that initialize
it:
func NewServer(mux *http.ServeMux) *http.Server {
return &http.Server{
Handler: mux,
}
}
func NewServeMux() *http.ServeMux {
return &http.ServeMux{}
}
Supported constructor signature:
func([dep1, dep2, depN]) (result, [cleanup, error])
Now let's teach a container to build these types.
container := inject.New(
inject.Provide(NewServer),
inject.Provide(NewServeMux)
)
The function inject.New()
parse our constructors, compile dependency
graph and return *inject.Container
type for interaction. Container
panics if it could not compile.
I think that panic at the initialization of the application and not in
runtime is usual.
We can extract the built server from the container. For this, define the
variable of extracted type and pass variable pointer to Extract
function.
If extracted type not found or the process of building instance cause
error, Extract
return error.
If no error occurred, we can use the variable as if we had built it
yourself.
var server *http.Server
err := container.Extract(&server)
if err != nil {
}
server.ListenAndServe()
Note that by default, the container creates instances as a singleton.
But you can change this behaviour. See Prototypes.
Invocation
As an alternative to extraction we can use Invoke()
function. It
resolves function dependencies and call the function. Invoke function
may return optional error.
func StartServer(server *http.Server) error {
return server.ListenAndServe()
}
container.Invoke(StartServer)
Lazy-loading
Result dependencies will be lazy-loaded. If no one requires a type from
the container it will not be constructed.
Interfaces
Inject make possible to provide implementation as an interface.
func NewServer(handler http.Handler) *http.Server {
return &http.Server{
Handler: handler,
}
}
For a container to know that as an implementation of http.Handler
is
necessary to use, we use the option inject.As()
. The arguments of this
option must be a pointer(s) to an interface like new(Endpoint)
.
This syntax may seem strange, but I have not found a better way to
specify the interface.
Updated container initialization code:
container := inject.New(
inject.Provide(NewServer),
inject.Provide(NewServeMux, inject.As(new(http.Handler)))
)
Now container uses provide *http.ServeMux
as http.Handler
in server
constructor. Using interfaces contributes to writing more testable code.
Groups
Container automatically groups all implementations of interface to
[]<interface>
group. For example, provide with
inject.As(new(http.Handler)
automatically creates a group
[]http.Handler
.
Let's add some http controllers using this feature. Controllers have
typical behavior. It is registering routes. At first, will create an
interface for it.
type Controller interface {
RegisterRoutes(mux *http.ServeMux)
}
Now we will write controllers and implement Controller
interface.
OrderController
type OrderController struct {}
func NewOrderController() *OrderController {
return &OrderController{}
}
func (a *OrderController) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/orders", a.RetrieveOrders)
}
func (a *OrderController) RetrieveOrders(writer http.ResponseWriter, request *http.Request) {
}
UserController
type UserController struct {}
func NewUserController() *UserController {
return &UserController{}
}
func (e *UserController) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/users", e.RetrieveUsers)
}
func (e *UserController) RetrieveUsers(writer http.ResponseWriter, request *http.Request) {
}
Just like in the example with interfaces, we will use inject.As()
provide option.
container := inject.New(
inject.Provide(NewServer),
inject.Provide(NewServeMux),
inject.Provide(NewOrderController, inject.As(new(Controller))),
inject.Provide(NewUserController, inject.As(new(Controller))),
)
Now, we can use []Controller
group in our mux. See updated code:
func NewServeMux(controllers []Controller) *http.ServeMux {
mux := &http.ServeMux{}
for _, controller := range controllers {
controller.RegisterRoutes(mux)
}
return mux
}
Advanced features
Named definitions
In some cases you have more than one instance of one type. For example
two instances of database: master - for writing, slave - for reading.
First way is a wrapping types:
type MasterDatabase struct {
*Database
}
type SlaveDatabase struct {
*Database
}
Second way is a using named definitions with inject.WithName()
provide
option:
inject.Provide(NewMasterDatabase, inject.WithName("master"))
inject.Provide(NewSlaveDatabase, inject.WithName("slave"))
If you need to extract it from container use inject.Name()
extract
option.
var db *Database
container.Extract(&db, inject.Name("master"))
If you need to provide named definition in other constructor use
di.Parameter
with embedding.
type ServiceParameters struct {
di.Parameter
MasterDatabase *Database `di:"master"`
SlaveDatabase *Database `di:"slave"`
}
func NewService(parameters ServiceParameters) *Service {
return &Service{
MasterDatabase: parameters.MasterDatabase,
SlaveDatabase: parameters.SlaveDatabase,
}
}
Optional parameters
Also di.Parameter
provide ability to skip dependency if it not exists
in container.
type ServiceParameter struct {
di.Parameter
Logger *Logger `di:"optional"`
}
Constructors that declare dependencies as optional must handle the
case of those dependencies being absent.
You can use naming and optional together.
type ServiceParameter struct {
di.Parameter
StdOutLogger *Logger `di:"stdout"`
FileLogger *Logger `di:"file,optional"`
}
Parameter Bag
If you need to specify some parameters on definition level you can use
inject.ParameterBag
provide option. This is a map[string]interface{}
that transforms to di.ParameterBag
type.
inject.Provide(NewServer, inject.ParameterBag{
"addr": ":8080",
})
func NewServer(pb di.ParameterBag) *http.Server {
return &http.Server{
Addr: pb.RequireString("addr"),
}
}
Prototypes
If you want to create a new instance on each extraction use
inject.Prototype()
provide option.
inject.Provide(NewRequestContext, inject.Prototype())
todo: real use case
Cleanup
If a provider creates a value that needs to be cleaned up, then it can
return a closure to clean up the resource.
func NewFile(log Logger, path Path) (*os.File, func(), error) {
f, err := os.Open(string(path))
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := f.Close(); err != nil {
log.Log(err)
}
}
return f, cleanup, nil
}
After container.Cleanup()
call, it iterate over instances and call
cleanup function if it exists.
container := inject.New(
inject.Provide(NewFile),
)
container.Cleanup()
Cleanup now work incorrectly with prototype providers.
Visualization
Dependency graph may be presented via
(Graphviz). For it, load string
representation:
var graph *di.Graph
if err = container.Extract(&graph); err != nil {
}
dotGraph := graph.String()
And paste it to graphviz online tool:
Contributing
I will be glad if you contribute to this library. I don't know much
English, so contributing to the documentation is very meaningful to me.