Back to Home

Dependency injection containers and benefits of using them

dependecy injection · inversion of control · container

Dependency injection containers and benefits of using them

Original author: Jakob Jenkov
  • Transfer

From translator


Hello! I continue a series of translations in which we look at the bones of what Dependency Injection is.

In previous articles in the series, we talked about “dependency injection” as an approach to application design and possible ways to implement this approach. The types of dependencies, options for their implementation were analyzed, tips were given to reduce the coherence of code components.

In today's translation, we will talk about what a DI container is, its functions, advantages of use and differences from factories.


The series includes the following articles



  1. Dependency injection
  2. Dependency Injection Containers
  3. Dependency Injection Benefits
  4. When to use Dependency Injection
  5. Is Dependency Injection Replacing the Factory Patterns?

Dependency Injection Containers


Key terms : container, component lifecycle management

If all components in your system have their own dependencies, then somewhere in the system some class or factory should know what to implement in all these components. This is what the DI container does. The reason this is called a “ container ” rather than a “factory” is because the container usually takes responsibility not only for instantiating and implementing dependencies.

When you configure a DI container, you determine which components it should be able to create instances of and which dependencies to implement in each component. You can also usually set up an instance mode for each component. For example, should a new instance be created each time? Or should the same component instance be reused ( singleton ) wherever it is embedded?

If some components are configured as singleton, then some containers have the ability to call singleton methods when the container is turned off. Thus, a singleton can free up any resources that it uses, such as a database connection or network connection. This is commonly referred to as “ asset lifecycle management.”". This means that the container is able to control the component at various stages of the component's life cycle. For example, create, configure, and delete.

Lifecycle management is one of the responsibilities that DI containers take in addition to instantiating and implementing them. The fact that a container sometimes maintains a reference to components after creating an instance is the reason why it is called a “container" rather than a factory. DI containers typically store references to objects whose life cycle they have to manage or which will be reused for future implementations, such as a singleton or adaptation. When a container is configured to create new instances of components with each call, the container usually "forgets" about the created objects. Otherwise, the garbage collector will have a hot time when the time comes to collect all these objects.

Currently, several DI containers are available. For java existButterfly Container , Spring , Pico Container ( Ed. Martin Fowler participated in its development), Guice (Ed. Development by Google) and others ( Ed. For example, there is also Dagger , also developed by Google. Jakob Jenkov, author translated article, developedButterfly Container. Its source code is available on github , and the documentation is contained in a separate series of posts ).

Benefits of using DI and DI containers

Key terms : dependency transfer, collaborators

There are several advantages of using DI containers compared to the fact that components have to resolve their dependencies on their own ( note ed. “Independently resolve dependencies” in this context means “create objects necessary for the component to work, inside the component itself ”).

Some of these advantages:

Less dependencies
Less “ dependency ” transferring
Code easier to reuse
Code easier to test
Code easier to read

These advantages are explained in more detail below.

Fewer dependencies


DI makes it possible to eliminate or at least reduce optional component dependencies. A component is vulnerable to changing its dependencies. If the dependency changes, the component may have to adapt to these changes. For example, if the signature of the dependency method changes, the component will have to change the call to this method. When component dependencies are minimized, they are less prone to changes.

Code is easier to reuse


Reducing the number of component dependencies usually makes it easier to reuse it in a different context. The fact that dependencies can be implemented and therefore configured externally increases the possibility of reusing this component. If in another context a different implementation of an interface is required, or a different configuration of the same implementation, the component can be configured to work with this implementation. There is no need to change the code.

The code is more convenient to test


DI also enhances component testing capabilities. When dependencies can be injected into a component, it is also possible to inject mocks of these objects. Mock objects are used for testing as a substitute for real implementation. The behavior of the mock object can be configured. Thus, all possible component behavior when using a mock-object can be tested for correctness. For example, handling a situation where mock returns a valid object, when it returns null, and when an exception is thrown. In addition, mock objects typically record which methods were called, and thus the test can verify that the component using mock used them ( approx. Ed methods) as expected.

The code is more convenient to read.


DI transfers dependencies to the component interface. This makes it clear what dependencies the component has, making the code more readable. You do not have to look at all the code in order to see what dependencies you will need to provide for this component. They are all visible in the interface.

Less dependency migration


Another nice bonus from DI is that it eliminates what I call “ dependency transfer ”. Dependency transfer is manifested in the fact that the object receives a parameter in one of its methods, which the object itself does not need, but is needed by one of the objects that it calls for its work. This may sound a little abstract, so let's give a simple example.

Component A downloads the application and creates a configuration object, Config, which is needed by some of the application objects, but not by all components in the system. Then A calls B, B calls C, C calls D. Neither B nor C needs an object of type Config, but D needs. Here is a chain of calls.

  A создает Config
  A --> B --> C --> D --> Config

Arrows symbolize method calls. If A creates B, and B creates C, and C creates D, and D needs Config, then the Config object must be passed through the whole chain: from A to B, from B to C, and finally from C to D. However, neither C nor D needs a Config object to do the job. All they do is “transfer” Config to D, which depends on Config. Hence the name “dependency transfer”.

If you were working on a large system, you may have seen many cases of transferring dependencies - parameters that are simply passed to a lower level.

Dependency transport creates a lot of “noise” in the code, making it harder to read and maintain. In addition, this makes component testing difficult. If the method call of component A requires some OX object just because its “collaborator” CY (approx. ed. the original uses the word collaborator. By definition , collaborators are classes that either depend on others or provide something to another class. It turns out that the category of “ collaborator”Combines the concepts of“ dependent class ”and“ dependency ”, is a superset of them), you still need to provide an OX instance when testing the method of object A, even if it does not use it. Even if you use the mock implementation of the CY collaborator, which may not use the OX object. You can get around this by passing null instead of OX if there is no null check in the test method. Sometimes it can be difficult to create an OX object during a test. If the OX constructor depends on many other objects or values, your test will also have to pass meaningful objects / values ​​for these parameters. And if OX depends on OY, which depends on OZ, it becomes real madness.

When call stacks are deep, dependency transfer is a real pain. Especially if you find that a component from the bottom of the stack needs another object that is available upstream. Then you have to add this object as a parameter to all method calls down the stack, starting from where the required object is available and ending with where it is needed.

The general solution to the “dependency transfer” problem is to make the necessary objects static singleton. Thus, any component of the system will be able to access the singleton through its static factory method . - Ed. Not to be confused with the Factory Method pattern.) Unfortunately, static singletones carry a whole bunch of other problems that I won’t dive into here. Static singleton is evil. Do not use them if you manage to avoid this.

When you use a DI container, you can reduce “dependency transfer” and reduce the use of static singletones. The container knows about all the components in the application. Therefore, it can ideally bind components without having to pass dependencies to one component through another. An example with components when using a container will look like this:

Контейнер создает Config
Контейнер создает D и внедряет Config
Контейнер создает C и внедряет D
Контейнер создает B и внедряет C
Контейнер создает A и внедряет B
A --> B --> C --> D --> Config

When A calls B, it does not need to pass the Config object to B. D already knows about the Config object.

However, there may still be situations where dependency transfer cannot be avoided. For example, if your application processes requests (is it a web application or a web service), and the components that process requests are singleton. Then the request object may need to be passed down the call chain every time a component of a lower level layer needs access to it.

In the next article, “When to use dependency injection,” Jakob Jenkov gives practical examples of using DI. How to write code if you need to: embed configuration information in one or more components, embed the same dependency in one or more components, embed different implementations of the same dependency, embed the same implementation in different configurations, get any data from the container. The author also talks about when you do not need DI. Stay tuned!

To the beginning

Read Next