Alice: an additive dependency injection container for Golang

magic003
5 min readApr 22, 2017

--

Dependency injection and the use of DI containers is a common practice in software development, no matter which programming language you use. After writing code in Golang for a while, I started searching for an open source DI container. I explored a few which meet the basic requirements. However, none of them does it in a way that I prefer. So I ended up writing my own DI container Alice. In this post, I will talk about why I wrote Alice, and how it works.

What is a preferable way?

I believe a good framework should help developers follow the design principles, and prevent them from making careless mistakes. My preferred DI container should have following characteristics.

No code pollution

Proper dependency injection doesn’t rely on a DI container. You can always build the dependency graph manually. The challenging part is to ensure components are created in the right order and it doesn’t break whenever adding or removing a component. A DI container is there to assist in building the dependency graph. It should not require any change to existing components, either to start using a container or to remove a container.

On the other hand, components should have no knowledge of a container. It is only the main function or bootstrapper that uses the container to build the dependency graph and starts the application.

Ideally, a container should be additive to components, without polluting code.

No partial object

It’s a widely suggested to use immutable objects, because they are easy to test and are always thread-safe. In order to achieve immutability, an object should only be returned after all states are initialized. So a DI container should not encourage to create partially initialized objects.

Easy to navigate the dependency graph

The dependency graph could get really complicated for a huge application. A DI container should allow to put the graph definition in a central place, and make it easy to figure out how exactly an object is created and how the dependency is wired.

Why a new DI container?

If you search “golang injection” in Google, you will probably get facebookgo/inject on the first page. It requires minimal effort to define the dependency graph and builds it up automatically. It is probably the DI container I would choose, without considering the characteristics I mentioned previously. However, it breaks all three.

In order to inject a dependency into a component, it requires to define the dependency as a public field, and add a tag to it. It pollutes the component and makes it aware of the usage of a container. It encourages to create a partial object and pass it to inject.Populate() function, which fills in the dependency object later. The dependency graph is defined using tags in each component, this distributed manner makes it difficult to understand how dependency is wired.

I also used another container codegansta/inject in my work. It has a simple implementation and is very flexible. It is basically a map which maintains the mapping between name and object, and between type and object. The Injector.Apply() function accepts a partial object and wires dependencies for it. Again, it requires the dependency fields to be public and tagged. It does have a Injector.Invoke() function which accepts a factory function and wires dependencies to its arguments. Technically, code pollution and partial object can be avoid using Injector.Invoke() only. However, the main limitation of this container is it just wires up the dependencies, but doesn’t actually build the dependency graph. It is still the developer’s responsibility to ensure objects are initiated in the right order.

For the above reasons, I decided to write my own DI container Alice. I am not claiming Alice is better than them, but it satifies my personal flavor. It is very close to the characteristics mentioned previously, even though it is not perfect.

How it works?

The idea behide Alice is not original. It is inspired by Spring JavaConfig. It usually takes 3 steps to use Alice.

Define modules

The dependency graph is defined using modules. An application could have multiple modules, which might be grouped by layers or components. Modules are usually placed in a separate package.

A module defines how to create objects, as well as declares dependencies on objects defined in other module. Take a common microservice as an example. The service client objects are defined in ClientModule, the database accessing objects are defined in RepositoryModule, and there is a BusinessModule which defines business objects. It dependes on objects defined in ClientModule and RepositoryModule, and inject them when creating business objects.

A typical module looks like this:

type ExampleModule struct {
alice.BaseModule
Foo Foo `alice:""`
Bar Bar `alice:"Bar"`
Baz Baz
}

func (m *ExampleModule) InstanceX() X {
return X{m.Foo}
}

func (m *ExampleModule) InstanceY() Y {
return Y{m.Baz}
}

A module must have an embedded alice.BaseModule struct. It allows 3 types of fields:

  • Field tagged by alice:"". It will be wired to object with the same or assignable type defined in other modules.
  • Field tagged by alice:"Bar". It will be wired to object with the specified named Bar defined in other modules.
  • Field without alice tag. It will not be wired to any object defined in other modules. It is expected to be provided when creating the module. It is not managed by the container and could not be retrieved.

It’s possible that no field is defined in a module.

Any public method of the module defines an object to be created and maintained by the container. It is required to use a pointer receiver. The method name is used as the object name. The return type is used as the object type. Inside the method, it creates object either using struct literal or using factory method. The module fields can be injected to the object.

The module is additive to application components, and doesn’t pollute the code at all. By organizing modules in a separate package, it makes it easy to navigate the dependency graph.

Create container

Main function or bootstrapper is the only component knows the container. It initializes modules and creates the container. The container sorts out the dependencies between modules and ensures objects are created in the right order.

m1 := &ExampleModule1{}
m2 := &ExampleModule2{...}
container := alice.CreateContainer(m1, m2)

You may notice here we create partial module objects, whose fields are filled in when creating the container. Since the modules are specific to Alice and won’t be reused by other library, this tradeoff is acceptable.

Retrieve instances

There are 2 ways to retrieve objects: by name and by type.

instanceX := container.InstanceByName("InstanceX")instanceY := container.Instance(reflect.TypeOf((Y)(nil)))

Limitations

Like other DI containers, Alice has its limitations.

  • Alice uses method name as the object name. It introduces name conflicts if two modules define methods using the same name, and this could not be captured by compiler.
  • The name based wiring is not friendly to refactor. If a module declares dependency using name, once the method name of the dependency is changed, the name should be changed manually. Again, this could not be captured by compiler.
  • Objects in Alice are singleton, and there is not way to specify creating a new object for each retrieval. A workaround is to create a factory object and call it to create new objects.

--

--