Basics of dependency injection

Original author: Alireza Ahmadi
  • Transfer

Basics of dependency injection


In this article I will talk about the basics of dependency injection (Eng. Dependency Injection, DI ) in simple language, and also talk about the reasons for using this approach. This article is intended for those who do not know what is dependency injection, or doubts the need to use this technique. So, let's begin.


What is addiction?


Let's first look at an example. We have ClassA, ClassBand ClassCas shown below:


classClassA{
  var classB: ClassB
}
classClassB{
  var classC: ClassC
}
classClassC{
}

You can see that the class ClassAcontains an instance of the class ClassB, so we can say that the class ClassAdepends on the class ClassB. Why? Because the class ClassAneeds a class ClassBto work correctly. We can also say that a class ClassBis a class dependency ClassA.


Before continuing, I want to clarify that such a relationship is good, because we do not need one class to do all the work in the application. We need to divide the logic into different classes, each of which will be responsible for a particular function. And in this case, the classes will be able to effectively interact.


How to work with addictions?


Let's look at three ways that are used to perform dependency injection tasks:


First way: create dependencies in a dependent class


Simply put, we can create objects whenever we need them. Look at the following example:


classClassA{
  var classB: ClassB
  funsomeMethodOrConstructor() {
    classB = ClassB()
    classB.doSomething()
  }
}

It is very easy! We create a class when we need it.


Benefits


  • It is easy and simple.
  • The dependent class ( ClassAin our case) completely controls how and when to create dependencies.

disadvantages


  • ClassAand ClassBclosely related to each other. Therefore, whenever we need to use ClassA, we will be forced to use and ClassB, and it will be impossible to replace ClassBwith something else .
  • With any change in the class initialization, ClassBit will be necessary to adjust the code inside the class ClassA(and all other ClassBclass- dependent ). This complicates the process of changing dependencies.
  • ClassAimpossible to test. If you need to test a class, and this is one of the most important aspects of software development, then you will have to carry out unit testing of each class separately. This means that if you want to check the correctness of a class exclusively ClassAand create several unit tests to test it, then, as was shown in the example, you will create an instance of the class in any case ClassB, even when it does not interest you. If an error occurs during testing, you will not be able to understand where it is located - in ClassAor ClassB. After all, there is a possibility that part of the code ClassBled to an error, whileClassAworks correctly. In other words, unit testing is impossible, because modules (classes) cannot be separated from each other.
  • ClassAmust be configured so that it can inject dependencies. In our example, he should know how to create ClassCand use it to create ClassB. I wish he knew nothing about it. Why? Because of the principle of uniform responsibility .

Each class must do only their work.

Therefore, we do not want classes to be responsible for anything other than their own tasks. The introduction of dependencies in this case is an additional task that we set for them.


The second way: implement dependencies through a custom class.


So, realizing that dependency injection inside the dependent class is not the best idea, let's explore the alternative method. Here, the dependent class defines all the dependencies it needs inside the constructor and allows the user class to provide them. Is this a way to solve our problem? We learn a little later.


Look at the sample code below:


classClassA{
    var classB: ClassB
    constructor(classB: ClassB){
        this.classB = classB
    }
}
classClassB{
    var classC: ClassC
    constructor(classC: ClassC){
        this.classC = classC
    }
}
classClassC{
    constructor(){
    }
}
classUserClass(){
    fundoSomething(){
        val classC = ClassC();
        val classB = ClassB(classC);
        val classA = ClassA(classB);
        classA.someMethod();
    }
}
view rawDI Example In Medium - 

Now it ClassAgets all the dependencies inside the constructor and can simply call the methods of the class ClassBwithout initializing anything.


Benefits


  • ClassAand ClassBnow loosely coupled, and we can replace ClassBwithout breaking the code inside ClassA. For example, instead of passing, ClassBwe will be able to transfer AssumeClassB, which is a subclass ClassB, and our program will work properly.
  • ClassAnow you can test. When writing a unit test, we can create our own version ClassB(test object) and transfer it to ClassA. If an error occurs during the test, then now we know for sure that this is definitely an error in ClassA.
  • ClassB relieved of dependency and can concentrate on performing his tasks.

disadvantages


  • This method resembles a chain mechanism, and at some point the chain should break. In other words, a class user ClassAshould know everything about initialization ClassB, which in turn requires knowledge of initialization ClassC, etc. So, you see that any change in the constructor of any of these classes can lead to a change in the calling class, not to mention that it ClassAcan have more than one user, so the logic of creating objects will be repeated.
  • Despite the fact that our dependencies are clear and easy to understand, user code is nontrivial and difficult to manage. Therefore, everything is not so simple. In addition, the code violates the principle of common responsibility, since it is responsible not only for its work, but also for introducing dependencies into dependent classes.

The second method obviously works better than the first, but it still has its drawbacks. Is it possible to find a more suitable solution? Before we look at the third method, let's first talk about the very concept of dependency injection.


What is dependency injection?


Dependency injection is a way to handle dependencies outside a dependent class when the dependent class does not need to do anything.

Based on this definition, our first solution does not explicitly use the idea of ​​dependency injection, and the second way is that the dependent class does nothing to provide dependencies. But we still think the second solution is bad. WHY?!


Since the definition of dependency injection says nothing about where the work with dependencies should occur (except outside the dependent class), the developer must choose a suitable place for dependency injection. As you can see from the second example, the custom class is not the right place.


How to do better? Let's look at the third way to handle dependencies.


The third way: let someone else handle dependencies instead of us.


According to the first approach, the dependent classes are responsible for obtaining their own dependencies, and in the second approach, we moved the processing of dependencies from the dependent class to a custom class. Let's imagine that there is someone else who could handle the dependencies, as a result of which neither the dependent nor the user classes would do this work. This method allows you to work with dependencies in the application directly.


“Net” implementation of dependency injection (in my personal opinion)

Responsibility for handling dependencies rests with the third party, so no part of the application will interact with them.

Dependency injection is not a technology, framework, library, or something similar. This is just an idea. The idea to work with dependencies outside the dependent class (preferably in a specially selected part). You can apply this idea without using any libraries or frameworks. However, we usually refer to frameworks for dependency injection, because it simplifies the work and avoids writing template code.


Any dependency injection framework has two inherent characteristics. Other additional functions may be available to you, but these two functions will always be present:


First, these frameworks offer a way to define the fields (objects) to be implemented. Some frameworks do this by annotating a field or constructor using annotation @Inject, but there are other methods. For example, Koin uses Kotlin's built-in language features to define deployment. Under Injectimplies that the dependency must be processed DI-framework. The code will look something like this:


classClassA{
  var classB: ClassB
  @Injectconstructor(classB: ClassB){
    this.classB = classB
  }
}
classClassB{
  var classC: ClassC
  @Injectconstructor(classC: ClassC){
    this.classC = classC
  }
}
classClassC{
  @Injectconstructor(){
  }
}

Secondly, frameworks allow you to determine how to provide each dependency, and this happens in a separate file (s). Approximately it looks like this (keep in mind that this is only an example, and it may differ from framework to framework):


classOurThirdPartyGuy{
  funprovideClassC(){
    return ClassC() //just creating an instance of the object and return it.
  }
  funprovideClassB(classC: ClassC){
    return ClassB(classC)
  }
  funprovideClassA(classB: ClassB){
    return ClassA(classB)
  }
}

So, as you can see, each function is responsible for handling one dependency. Therefore, if we need to use somewhere in the application ClassA, the following will happen: our DI framework creates one instance of the class ClassC, calling it provideClassC, passing it to provideClassBand receiving an instance ClassBthat is passed to provideClassA, and as a result is created ClassA. It is almost magic. Now let's explore the advantages and advantages of the third method.


Benefits


  • Everything is as simple as possible. Both the dependent class and the dependency class are clear and simple.
  • Classes are loosely coupled and easily replaceable by other classes. Suppose we want to replace ClassCwith AssumeClassC, which is a subclass ClassC. To do this, you only need to change the provider code as follows, and wherever it is used ClassC, the new version will now be automatically used:

funprovideClassC(){
  return AssumeClassC()
}

Please note that no code inside the application is changed, only the provider method. It seems that nothing can be even simpler and more flexible.


  • Incredible testability. You can easily replace dependencies with test versions during testing. In fact, dependency injection is your main assistant when it comes to testing.
  • Improved code structure, because The application has a separate place for handling dependencies. As a result, the remaining parts of the application may focus solely on performing their functions and not overlapping with dependencies.

disadvantages


  • DI frameworks have a certain threshold of entry, so the project team must spend time and study it before it can be used effectively.

Conclusion


  • Dependency handling without DI is possible, but this can lead to application crashes.
  • DI is simply an effective idea, according to which it is possible to handle dependencies outside the dependent class.
  • The most effective use of DI is in certain parts of the application. Many frameworks contribute to this.
  • Frameworks and libraries are not needed for DI, but they can help a lot.

In this article I tried to explain the basics of working with the concept of dependency injection, and also listed the reasons for using this idea. There are many more resources that you can explore to learn more about using DI in your own applications. For example, a separate section is devoted to this topic in the advanced part of our Android profession course .


Also popular now: