📜 ⬆️ ⬇️

Containers of the introduction of dependencies and the benefits of their use

From translator


Hello! I continue the series of translations, in which we analyze by bone what Dependency Injection is.

The previous articles in the series dealt with “dependency injection” as an approach to designing applications and possible ways to implement such an approach. Were analyzed the types of dependencies, options for their implementation, were given tips to reduce the connectivity of the code components.

In today's translation, we’ll talk about what a DI container is, its functions, advantages of use and difference 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 the 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 a DI container does. The reason why this is called a “ container ” rather than a “factory” is that a container usually takes responsibility not only for creating instances and introducing dependencies.

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

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

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

Currently there are several DI containers available. For Java, there are Butterfly Container , Spring , Pico Container ( ed. In its development was played by Martin Fowler), Guice (approx. By Google) and others ( ed. For example, there is also Dagger , also developed by Google. Jakob Jenkov, author translated article, developed Butterfly Container. Its source code is available on github , and the documentation is contained in a separate series of posts ).

Benefits from using DI and DI containers

Key terms : dependency transfer, collaborators

There are several advantages from using DI containers in comparison with the fact that components have to independently resolve their dependencies ( note “to independently resolve dependencies” in this context means “to create objects necessary for the component to work inside the component”).

Some of these benefits are:

Less dependencies
Less “transfer” of dependencies
Code easier to reuse
Code easier to test
Code easier to read

These advantages are explained in more detail below.

Less dependencies


DI makes it possible to eliminate or at least reduce optional component dependencies. The component is vulnerable to changing its dependencies. If the dependency changes, the component may need 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, it is less susceptible to the need for change.

Code easier to reuse


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

Code easier to test


DI also enhances the ability to test components. When dependencies can be embedded in a component, it is also possible to implement the mocks of these objects. Mock objects are used for testing as a replacement for real implementation. The behavior of the mock object can be configured. Thus, all possible behavior of a component 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 usually record which of their methods were invoked and, thus, the test can verify that the component using the mock used them ( ed. Methods) as expected.

Code easier to read


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

Less “transfer” of dependencies


Another nice bonus from DI - eliminates the fact that I call the " dependency transfer ." The transfer of dependencies is manifested in the fact that an object receives a parameter in one of its methods, which is not needed by the object itself, but is needed by one of the objects that it calls for its work. This may sound a bit abstract, so let's give a simple example.

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

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 entire chain: from A to B, from B to C, and finally from C to D. However, neither C nor D is needed for the operation of the Config object. All they do is “transfer” Config to D, which depends on Config. From here and the name "transfer of dependences".

If you worked on a large system, you may have seen many cases of dependency transfers — parameters that are simply passed to a lower level.

Migrating dependencies creates a lot of “noise” in the code, making it harder to read and maintain. In addition, it makes testing components difficult. If calling the method of component A requires an OX object just because it is needed by its CY "collaborator" (the original editor uses the word collaborator. By definition , collaborators are classes that either depend on others or provide something to the other class. It turns out that the category “ collaborator ” combines the concepts of “dependent class” and “dependency” is their superset), you still need to provide an instance of OX 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 work around this by passing null instead of OX if there is no null check in the test method. Sometimes during the test it can be difficult to create an OX object. 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, moving dependencies is a real pain. Especially if you find that a component from the bottom of the stack needs another object that is available higher up the stack. Then you will 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 where it is needed.

The common solution for the problem of “transfer of dependencies” 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 ( note ed. Not to be confused with the Factory Method pattern ). Unfortunately, static singletons are pulling a whole bunch of other problems behind them, which I won't dive here. Static singletons are evil. Do not use them if you can avoid it.

When you use a DI container, you can reduce the “dependency transfer” and reduce the use of static singletons. The container knows about all the components in the application. Therefore, it can perfectly link components without having to pass dependencies to one component through another. An example with components 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, he does not need to pass the Config object to B. D already knows about the Config object.

However, there may still be situations in which the transfer of dependencies cannot be avoided. For example, if your application processes requests (this is a web application or a web service), and the components that process requests are singletones. Then the request object may need to be passed down the chain of calls each time it needs access to a component of a lower layer.

In the next article, When to use dependency injection, Jakob Jenkov gives practical examples of the use of DI. How to write code if you need: embed configuration information in one or more components, embed the same dependency in one or more components, implement different implementations of the same dependency, implement the same implementation in different configurations, get some data from the container. Also, the author tells about the cases in which you will not need DI. Stay tuned!

To top

Source: https://habr.com/ru/post/350708/


All Articles