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.
Let's first look at an example. We have ClassA
, ClassB
and ClassC
, as shown below:
class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { }
You can see that the class ClassA
contains an instance of the class ClassB
, so we can say that the class ClassA
depends on the class ClassB
. Why? Because the ClassA
class needs the ClassB
class to work correctly. We can also say that the ClassB
class is a dependency of the ClassA
class.
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 interact effectively.
Let's look at three ways that are used to perform dependency injection tasks:
Simply put, we can create objects whenever we need them. Look at the following example:
class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } }
It is very easy! We create a class when we need it.
Benefits
ClassA
in our case) completely controls how and when to create dependencies.disadvantages
ClassA
and ClassB
closely related to each other. Therefore, whenever we need to use ClassA
, we will have to use ClassB
, and it will be impossible to replace ClassB
with something else .ClassB
class, it is necessary to correct the code inside the ClassA
class (and all other ClassB
dependent classes). This complicates the process of changing dependencies.ClassA
cannot be tested. 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 how ClassA
works exclusively and create several unit tests to test it, then, as was shown in the example, you will create an instance of the ClassB
class, 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 ClassA
or ClassB
. After all, there is a possibility that part of the code in ClassB
led to an error, while ClassA
works correctly. In other words, unit testing is impossible, because modules (classes) cannot be separated from each other.ClassA
must be configured so that it can inject dependencies. In our example, he should know how to create ClassC
and 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 but their own tasks. The introduction of dependencies is an additional task that we set for them.
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:
class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium -
Now ClassA
gets all the dependencies inside the constructor and can simply call the methods of the ClassB
class without initializing anything.
Benefits
ClassA
and ClassB
now loosely coupled, and we can replace ClassB
without breaking the code inside ClassA
. For example, instead of passing ClassB
we will be able to pass AssumeClassB
, which is a subclass of ClassB
, and our program will work AssumeClassB
.ClassA
can now be tested. When writing a unit test, we can create our own version of ClassB
(a test object) and pass 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
free from dependency and can focus on performing its tasks.disadvantages
ClassA
must know everything about initializing ClassB
, which in turn requires knowledge and about initializing 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 ClassA
can have more than one user, so the logic of creating objects will be repeated.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 considering the third method, let's first talk about the very concept of dependency injection.
Dependency injection is a way to handle dependencies outside the 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.
According to the first approach, the dependent classes are responsible for getting their own dependencies, and in the second approach, we moved the dependency processing 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 of ​​working with dependencies outside the dependent class (preferably in a dedicated 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 might 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 the @Inject
annotation, but there are other methods. For example, Koin uses Kotlin's built-in language features to define embedding. By Inject
meant that the dependency must be handled by the DI framework. The code will look something like this:
class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } }
Secondly, the 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):
class OurThirdPartyGuy { fun provideClassC(){ return ClassC() //just creating an instance of the object and return it. } fun provideClassB(classC: ClassC){ return ClassB(classC) } fun provideClassA(classB: ClassB){ return ClassA(classB) } }
So, as you can see, each function is responsible for handling one dependency. Therefore, if we need to use ClassA
somewhere in the application, the following will happen: our DI framework creates one instance of ClassC
, invoking provideClassC
, passing it to provideClassB
and receiving an instance of ClassB
, which is passed to provideClassA
, and as a result ClassA
is created. It is almost magic. Now let's explore the advantages and advantages of the third method.
Benefits
ClassC
with AssumeClassC
, which is a subclass of ClassC
. To do this, you only need to change the provider code as follows, and wherever ClassC
is used, the new version will now be automatically used: fun provideClassC(){ return AssumeClassC() }
Please note that no code inside the application changes, only the provider method. It seems that nothing can be even simpler and more flexible.
disadvantages
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 .
Source: https://habr.com/ru/post/434380/
All Articles