Why do I need a Dependency Injection container?

The principle of Inversion of Control is clear and logical to me, but I can't understand why DI is needed. Example:

public class Samurai 
{
    public IWeapon Weapon { get; private set; }
    public Samurai(IWeapon weapon) 
    {
        this.Weapon = weapon;
    }
}

And here, somewhere in one place of the program, we introduce a dependency between the interface and a specific implementation. I.e., if necessary, it is enough only to replace another implementation here, for example, for testing or just like that. Here Ninject is used, but it doesn't matter which framework it is.

this.Bind<IWeapon>().To<Sword>();

Okay, that's fine, BUT why do we need a DI framework if we can do essentially the same thing:

var samurai = new Samurai(new Sword());

That is, in the constructor, just as in the example with the DI framework, we can replace the implementation by changing just one word in one place. So what is the point of DI container then? Sorry if the question is stupid, I'm a beginner.

Author: andreycha, 2015-10-27

4 answers

Inversion Of Control is a principle used to reduce code connectivity.

Dependency Injection is one of the ways to implement this principle in which dependencies are passed through the constructor (usually) or through setting properties (less often).

Ninject, Unity, etc. this is Dependency Injection Container - a tool for easier application of Dependency Injection. As a rule, it allows more than it is convenient to set the composition of objects, their lifetime, as well as interception.

For the application of Dependency Injection, the use of any frameworks (containers) is not necessary. They only allow you to do it more conveniently.


To make it more clear, I will try (quite briefly) to describe the advantages of a DI container.

In the above example, everything is quite simple. There are only a few classes that can be easily compiled manually together. In this case, the meaning of the DI-container is really absent and everything can be done by hand quite simply.

But when the project gets bigger, there are more objects and their nesting in each other becomes deeper - it becomes quite difficult to manage all this manually. Because it will require either writing a large amount of code or a native, built-in framework.

The container, in turn, allows you to:

  1. Ask questions different configuration and it is convenient to manage it. Use various conventions so that the container itself "picks up" the types it needs found under certain conditions in the project. If the configuration is read from an external file (for example, xml), the behavior of the program can be changed without recompiling. etc. etc.

  2. Since in complex cases, it may be that objects that are passed as dependencies may also require some dependencies for themselves that may also to require for yourself what dependencies, etc. to implement all this manually will be time-consuming. The container will easily do it all on its own.

  3. Allow you to set the lifetime of objects. Consider a simple example:

    public class A
    {
        public A(IB b, IC c) {}
    }
    
    public interface IB {}
    public class B 
    {
        public B(IC c) {}
    }
    
    public interface IC {}
    public class C {}
    

    In this case, when creating an instance of type A, we need an instance of type IB and IC, IB in turn also requires an instance of type IC. We may need to either create a new instance of type IC in each case, or use it in both in both cases, it is the same. The container makes it easy to manipulate this. In addition to these two cases, there are also others.

  4. Allows you to use interception. This is the ability to use the container to embed some useful code (such as logging, checking rights, etc.) before\after calls to the methods of the dependency interface. It is better to read about this separately, with examples, for a specific container. Ninject has a special app for this extension.

 25
Author: ApInvent, 2015-10-28 06:51:57

This is what dependency injection in the real world might look like:

enter a description of the image here

The socket is your samurai, the plug is your weapon. There is an interface between the socket and the plug, which is determined by the size of the plug and the mains voltage. There can be a thousand sockets and a thousand plugs that implement this interface and they will be compatible with each other in any combination.

When you implement a dependency through a constructor, you create something similar to a fork with an outlet.

Further. The work on your program can be (very conditionally) divided into two parts:

  1. Defining interfaces between components and implementing these components (you think through the interfaces between plugs and sockets and create objects that implement these interfaces)
  2. Assembling components into a running program (you sit down and connect everything together in a certain order)

If you do this:

var samurai = new Samurai(new Sword());

Then the build will be smeared all over your code, you will have sockets that will connect components to themselves, this is equivalent to this:

When you use a dependency injection container, you build the components outside of the components themselves in a single location called the build root. This is where you write this:

this.Bind<IWeapon>().To<Sword>();

This code simply specifies which object to use when required. interface IWeapon. In a real application, the configuration will be more complicated. It will contain instructions indicating that some classes should be singletons, instructions describing the use of decorators in your project.

In general, DI is one of those concepts that only seem very simple and that is very easy to use incorrectly. More precisely, it is really quite simple, but there are subtle nuances that solve everything and a lack of understanding of which can devalue your experience its application, turning it into a Cargo cult.

Therefore, I highly recommend reading as much as possible on this topic, one answer to so is clearly not enough here.

 22
Author: Andrew Prigorshnev, 2018-10-09 09:18:04

OK, that's fine, BUT why do we need DI and additional frameworks if we can do essentially the same thing:

var samurai = new Samurai(new Sword());

This is Dependency Injection. Even without using any frameworks, you still used DI in this example. You gave the Samurai class its dependency by injecting this dependency into the constructor.

DI is always recommended, regardless of whether you use ready-made libraries or not. The use consists of the problem is that classes that have dependencies (Samurai) will not know where and how to get these dependencies. They will simply say declaratively: I cannot be used without this dependency (IWeapon).

Using DI, even without containers, significantly reduces code connectivity. Your classes lose knowledge about the infrastructure of your application and become more independent, subject to the principles of SOLID, testable and supported.

Counter-example

Now imagine that you are not using DI. Instead, some kind of antipattern Service Locator is used (I called it ConfigurationService):

public class Samurai 
{
    public IWeapon Weapon { get; private set; }
    public Samurai() 
    {
        this.Weapon = ConfigurationService.Get<IWeapon>();
    }
}

Now, if you decide to use an IoC container in the future, or to feed the dependency to the class not through ConfigurationService, but in some other way, then you will have to change the class Samurai. What if you have dozens, hundreds of classes in your app, who use this Service Locator? We'll have to change them all.

When using the Dependency Injection pattern (as in your original example), when adding an IoC container or changing the way dependencies are created/received, the classes that use dependencies themselves will not have to be changed. This is a huge benefit of DI. Classes that use this pattern do not know and do not want to know about the application infrastructure. Due to this, they are more versatile.

 9
Author: Дмитрий Шевченко, 2016-04-19 13:01:22

DI thing is convenient because you can describe the factory-builders that create the necessary objects in one place, and then use the annotation in the constructor or in the class fields to link them. Thus, if your object accepts 10 interfaces in the constructor, then you will not write a long sheet and pull dependencies, but simply ask the DI framework to create the object you need (if it cannot resolve the dependencies, it will swear), which will significantly increase readability the code.
However, DI has a downside because of which I personally do not use this approach - all these non-explicit initializations make it very difficult to analyze the code (looking at it, you can not say for sure which implementation was passed), especially when the DI approach is applied not to the constructor, but to the fields - here comes hell.

 1
Author: Александр Мартынцев, 2017-07-02 06:52:51