📜 ⬆️ ⬇️

Basics of dependency injection

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 , 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.


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:


 class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } } 

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


Benefits



disadvantages



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.


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:


 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



disadvantages



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.


What is 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.


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


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



 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



Conclusion



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