Studying Dependency Injection
Despite the fact that the pattern has been around for more than a decade, there are many articles (and translations), nevertheless, there are more and more disputes, comments, questions, and various realizations.
There is enough information even on the hub, but the fact that it is discussed everywhere how to do it, but practically nowhere - WHY, inspired me to write the post . Is it possible to create a good architecture if you do not know what it is for and in what it should be good? Certain principles and clear trends can be taken into account - this will help minimize unforeseen problems, but understanding is even better.
Dependency injection is a design pattern in which fields or parameters for creating an object are configured externally.
Knowing that many will be limited to reading the first paragraphs, I changed the article.
Despite the fact that such a “definition” of DI is found in many sources - it is ambiguous, because it makes the user think that injection is something that replaces the creation / initialization of objects, or at least is very actively involved in this process. Of course, no one will prohibit doing such an implementation of DI. But DI can be a passive wrapper around the creation of an object that provides the provision of input parameters. In such an implementation, we get another level of abstraction and an excellent separation of duties: the object itself is responsible for its initialization, and the injection implements the storage of data and providing them with application modules.
Now about everything in order and in detail.
I'll start with a simple one, why there was a need for new patterns, and why did some old patterns become very limited in scope?
In my opinion, the bulk of the changes were introduced by the massive introduction of self-testing. And for those who are actively writing autotests, this article is obvious as a white day, you can not read further. Only you can not imagine how many people do not write them. I understand that small companies and start-ups do not have these resources, but, unfortunately, large companies often have more priority problems.
The reasoning here is very simple. Suppose you are testing a function with parameters a and b , and you expect to get the result x. At some point, your expectations do not come true, the function returns the result y , and after spending some time, you find a singleton inside the function, which in some states brings the result of the function to a different value. This singleton was called an implicit addiction , and in every possible way refused to use it in such situations. Unfortunately, you won’t throw words out of the song, otherwise it will be a completely different song. Therefore, we take out our singleton as an input variable in the function. Now we have 3 input variables a , b , s . Everything seems to be obvious: we change the parameters - we get an unambiguous result.
While I will not give examples. Moreover, we are not only talking about functions within a class, it is a schematic argument that can also be applied to creating a class, module, etc.
Supplement the above example. You have an object that contains 9 user settings (variables), for example rights to read / edit / sign / print / forward / delete / lock / execute / copy a document. Your function uses only three variables from these settings. What do you pass to the function: the whole object with 9 variables as one parameter, or only three necessary settings with three separate parameters? Very often we enlarge the transferred objects so as not to set many parameters, that is, we select the first option. This method will be considered the transfer of "unreasonably broad dependencies . " As you have already guessed, for the purposes of self-testing it is better to use the second option and pass only those parameters that are used.
We made 2 conclusions:
- the function should receive all the necessary parameters at the input
- the function should not receive unnecessary parameters at the input
We wanted the best - but received a function with 6 parameters. Suppose that everything is in order inside the function, but someone should take the responsibility of providing input parameters to the function. As I already wrote, my reasoning is sketchy. I mean not just an ordinary class function, but rather a module initialization / creation function (vip, viper, data object, etc.). In this context, we rephrase the question: who should provide the input parameters for creating the module?
One solution would be to shift this case to the calling module. But then it turns out that the calling module needs to pass the parameters of the child. This entails the following complications:
Firstly, a little earlier we decided to avoid "unreasonably broad dependencies." Secondly, you don’t have to work hard to understand that there will be a lot of parameters, and it will be very tedious to edit them each time you add child modules, it even hurts to think about deleting child modules. By the way, in some applications it’s impossible to build a hierarchy of modules at all: look at any social network: profile -> friends -> friend’s profile -> friend’s friends, etc. Thirdly, on this topic we can recall the principle of SOLI D : “Top-level modules do not depend on lower-level modules”
This gives rise to the idea of putting the module creation / initialization in a separate structure. Then it's time to write a few lines as an example:
In the example, there is a module of the list of accounts AccountList, which calls the module of detailed information on the account AccountDetail.
To initialize the AccountDetail module, 3 variables are needed. The variable Account AccountDetail receives from the parent module, the variables permission1, permission2 are injected. Due to injection, a module call with invoice details will look like:
instead
and the parent module of the list of accounts, AccountList, will be relieved of the obligation to pass parameters with permissions about which he knows nothing.
I rendered the injection (assembly) implementation into a static function in a class extension. But the implementation can be any at your discretion.
As we see:
The essence of dependency injection is the construction of such a process in which, when calling one module from another, an independent object / mechanism transfers (injects) data to the called module. In other words, the called module is configured externally.
There are several configuration methods:
Constructor Injection , Property injection , Interface Injection .
For Swift:
Initializer Injection , Property Injection , Method Injection .
The most common are constructor (initialization) injections and properties.
Important:in almost all sources, it is recommended that constructor injections be preferred. Compare Constructor / Initializer Injection and Property injection:
better than
It seems that the advantages of the first method are obvious, but for some reason some understand the injection as configuring an already created object and use the second method. I am for the first method:
However, until we got rid of some dependencies, we just shifted them from one shoulder to another. A logical question is where to get the data from in the assembly itself (make function in the example).
The use of singletones in the assembly mechanism no longer leads to the above problems with hidden dependency, because You can test the creation of modules with any data set.
But here we are faced with another minus of the singleton: poor handling (you can probably bring a lot of hateful arguments, but laziness). It is not good to scatter your many stored / singletones in assemblies, by analogy with anyone, as they were scattered in functional modules. But even such refactoring will already be the first step towards hygiene, because then you can restore order in assemblies almost without affecting the code and module tests.
If you want to streamline the architecture further, as well as test transitions and assembly work, you will have to work a little more.
The DI concept offers us to store all the necessary data in a container. It's comfortable. Firstly, saving (registering) and receiving (resolve) data goes through a single container object, respectively, so it’s easier to manage data and test it. Secondly, you can take into account the dependence of data on each other. In many languages, including swift, there are ready-made dependency management containers, usually dependencies form a tree. The remaining pros and cons I will not list, you can read about them on the links that I posted at the beginning of the post.
Here's what the assembly using the container might look like.
This is a possible implementation example. The example uses the Swinject framework , which was born not so long ago. Swinject allows you to create a container for automated dependency management, and also allows you to create containers for Storyboards. More information about Swinject can be found in the examples on raywenderlich . I really like this site, but this example is not the most successful, since it considers the use of the container only in autotests, while the container should be laid in the application architecture. You in your code, you can write a container yourself.
Thank you all for this. I hope you didn’t get bored reading this text.
Background
In 2004, Martin Fowler wrote the famous article “ Inversion of Control Containers and the Dependency Injection pattern ”, which described the above pattern and its implementation for Java. Since then, the pattern has become widely debated and implemented. In mobile development, especially on iOS, this came with a significant delay. On the Habré there are good translations of the article , good luck and bright karma to their author.
There is enough information even on the hub, but the fact that it is discussed everywhere how to do it, but practically nowhere - WHY, inspired me to write the post . Is it possible to create a good architecture if you do not know what it is for and in what it should be good? Certain principles and clear trends can be taken into account - this will help minimize unforeseen problems, but understanding is even better.
Dependency injection is a design pattern in which fields or parameters for creating an object are configured externally.
Knowing that many will be limited to reading the first paragraphs, I changed the article.
Despite the fact that such a “definition” of DI is found in many sources - it is ambiguous, because it makes the user think that injection is something that replaces the creation / initialization of objects, or at least is very actively involved in this process. Of course, no one will prohibit doing such an implementation of DI. But DI can be a passive wrapper around the creation of an object that provides the provision of input parameters. In such an implementation, we get another level of abstraction and an excellent separation of duties: the object itself is responsible for its initialization, and the injection implements the storage of data and providing them with application modules.
Now about everything in order and in detail.
I'll start with a simple one, why there was a need for new patterns, and why did some old patterns become very limited in scope?
In my opinion, the bulk of the changes were introduced by the massive introduction of self-testing. And for those who are actively writing autotests, this article is obvious as a white day, you can not read further. Only you can not imagine how many people do not write them. I understand that small companies and start-ups do not have these resources, but, unfortunately, large companies often have more priority problems.
The reasoning here is very simple. Suppose you are testing a function with parameters a and b , and you expect to get the result x. At some point, your expectations do not come true, the function returns the result y , and after spending some time, you find a singleton inside the function, which in some states brings the result of the function to a different value. This singleton was called an implicit addiction , and in every possible way refused to use it in such situations. Unfortunately, you won’t throw words out of the song, otherwise it will be a completely different song. Therefore, we take out our singleton as an input variable in the function. Now we have 3 input variables a , b , s . Everything seems to be obvious: we change the parameters - we get an unambiguous result.
While I will not give examples. Moreover, we are not only talking about functions within a class, it is a schematic argument that can also be applied to creating a class, module, etc.
Singleton Notes
Note 1. If, given the criticism of the singleton pattern, you decide to replace it, for example, with UserDefaults, then in relation to this situation, the same implicit dependency looms.
Note 2. It is not entirely correct to say that only because of autotesting it is not worth using singletones inside the function body. In general, from the point of view of programming, it is not entirely correct that with the same input, the function produces different results. It's just that in autotests this problem loomed more clearly.
Note 2. It is not entirely correct to say that only because of autotesting it is not worth using singletones inside the function body. In general, from the point of view of programming, it is not entirely correct that with the same input, the function produces different results. It's just that in autotests this problem loomed more clearly.
Supplement the above example. You have an object that contains 9 user settings (variables), for example rights to read / edit / sign / print / forward / delete / lock / execute / copy a document. Your function uses only three variables from these settings. What do you pass to the function: the whole object with 9 variables as one parameter, or only three necessary settings with three separate parameters? Very often we enlarge the transferred objects so as not to set many parameters, that is, we select the first option. This method will be considered the transfer of "unreasonably broad dependencies . " As you have already guessed, for the purposes of self-testing it is better to use the second option and pass only those parameters that are used.
We made 2 conclusions:
- the function should receive all the necessary parameters at the input
- the function should not receive unnecessary parameters at the input
We wanted the best - but received a function with 6 parameters. Suppose that everything is in order inside the function, but someone should take the responsibility of providing input parameters to the function. As I already wrote, my reasoning is sketchy. I mean not just an ordinary class function, but rather a module initialization / creation function (vip, viper, data object, etc.). In this context, we rephrase the question: who should provide the input parameters for creating the module?
One solution would be to shift this case to the calling module. But then it turns out that the calling module needs to pass the parameters of the child. This entails the following complications:
Firstly, a little earlier we decided to avoid "unreasonably broad dependencies." Secondly, you don’t have to work hard to understand that there will be a lot of parameters, and it will be very tedious to edit them each time you add child modules, it even hurts to think about deleting child modules. By the way, in some applications it’s impossible to build a hierarchy of modules at all: look at any social network: profile -> friends -> friend’s profile -> friend’s friends, etc. Thirdly, on this topic we can recall the principle of SOLI D : “Top-level modules do not depend on lower-level modules”
This gives rise to the idea of putting the module creation / initialization in a separate structure. Then it's time to write a few lines as an example:
class AccountList {
public func showAccountDetail(account: String) {
let accountDetail = AccountDetail.make(account: account)
// to do something with accountDetail
}
}
class AccountDetail {
private init(account: String, permission1: Bool, permission2: Bool) {
print("account = \(account), p1 = \(permission1), p2 = \(permission2)")
}
}
extension AccountDetail {
public static func make(account: String) -> AccountDetail? {
let p1 = ...
let p2 = ...
return AccountDetail(account: account, permission1: p1, permission2: p2)
}
}
In the example, there is a module of the list of accounts AccountList, which calls the module of detailed information on the account AccountDetail.
To initialize the AccountDetail module, 3 variables are needed. The variable Account AccountDetail receives from the parent module, the variables permission1, permission2 are injected. Due to injection, a module call with invoice details will look like:
let accountDetail = AccountDetail.make(account: account)
instead
let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2)
and the parent module of the list of accounts, AccountList, will be relieved of the obligation to pass parameters with permissions about which he knows nothing.
I rendered the injection (assembly) implementation into a static function in a class extension. But the implementation can be any at your discretion.
As we see:
- The module received the necessary parameters. Its creation and execution can be safely tested on all sets of values.
- The modules are independent, there is no need to transfer anything for children or just the necessary minimum.
- Modules do NOT do the job of providing data; they use ready-made data (p1, p2). Thus, if you want to change something in the storage or provision of data, then you do not have to make changes to the functional code of the modules (as well as their autotests), but you only need to change the assembly system itself, or to extensions with the assembly.
The essence of dependency injection is the construction of such a process in which, when calling one module from another, an independent object / mechanism transfers (injects) data to the called module. In other words, the called module is configured externally.
There are several configuration methods:
Constructor Injection , Property injection , Interface Injection .
For Swift:
Initializer Injection , Property Injection , Method Injection .
The most common are constructor (initialization) injections and properties.
Important:in almost all sources, it is recommended that constructor injections be preferred. Compare Constructor / Initializer Injection and Property injection:
let account = ..
let p1 = ...
let p2 = ...
let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2)
better than
let accountDetail = AccountDetail()
accountDetail.account = ..
accountDetail.permission1 = ...
accountDetail.permission2 = ...
It seems that the advantages of the first method are obvious, but for some reason some understand the injection as configuring an already created object and use the second method. I am for the first method:
- creation by the designer guarantees a valid object;
- with Property injection, it is not clear whether it is necessary to test a change in a property in places other than creation;
- in languages that use optionality, to implement Property injection, you need to make the fields optional, or come up with clever initialization methods (lazy ones will not always work). Excessive optionality adds unnecessary code and unnecessary test suites.
However, until we got rid of some dependencies, we just shifted them from one shoulder to another. A logical question is where to get the data from in the assembly itself (make function in the example).
The use of singletones in the assembly mechanism no longer leads to the above problems with hidden dependency, because You can test the creation of modules with any data set.
But here we are faced with another minus of the singleton: poor handling (you can probably bring a lot of hateful arguments, but laziness). It is not good to scatter your many stored / singletones in assemblies, by analogy with anyone, as they were scattered in functional modules. But even such refactoring will already be the first step towards hygiene, because then you can restore order in assemblies almost without affecting the code and module tests.
If you want to streamline the architecture further, as well as test transitions and assembly work, you will have to work a little more.
The DI concept offers us to store all the necessary data in a container. It's comfortable. Firstly, saving (registering) and receiving (resolve) data goes through a single container object, respectively, so it’s easier to manage data and test it. Secondly, you can take into account the dependence of data on each other. In many languages, including swift, there are ready-made dependency management containers, usually dependencies form a tree. The remaining pros and cons I will not list, you can read about them on the links that I posted at the beginning of the post.
Here's what the assembly using the container might look like.
import Foundation
import Swinject
public class Configurator {
private static let container = Container()
public static func register(name: String, value: T) {
container.register(type(of: value), name: name) { _ in value }
}
public static func resolve(service: T.Type, name: String) -> T? {
return container.resolve(service, name: name)
}
}
extension AccountDetail {
public static func make(account: String) -> AccountDetail? {
if let p1 = Configurator.resolve(service: Bool.self, name: "permission1"),
let p2 = Configurator.resolve(service: Bool.self, name: "permission2") {
return AccountDetail(account: account, permission1: p1, permission2: p2)
} else {
return nil
}
}
}
// где-то в других модулях, например при входе в приложение вы должны получить
// разрешения и зарегистрировать(сохранить) их
Configurator.register(name: "permission1", value: true)
Configurator.register(name: "permission2", value: false)
...
This is a possible implementation example. The example uses the Swinject framework , which was born not so long ago. Swinject allows you to create a container for automated dependency management, and also allows you to create containers for Storyboards. More information about Swinject can be found in the examples on raywenderlich . I really like this site, but this example is not the most successful, since it considers the use of the container only in autotests, while the container should be laid in the application architecture. You in your code, you can write a container yourself.
Thank you all for this. I hope you didn’t get bored reading this text.