Despite the fact that the pattern has been around for more than a decade and there are quite a few articles (and translations), however, disputes, comments, questions and various implementations are becoming more and more.
There is enough information even on Habré, but I was prompted to write the post by the fact that everywhere it is discussed
HOW to do it, but almost nowhere -
why . Is it possible to create a good architecture if you do not know what it is for and what exactly it should be good for? You can take into account certain principles and clear trends - this will help minimize unforeseen problems, but to understand is even better.
Dependency injection is a design pattern in which fields or parameters for creating an object are configured externally. In simple words, an application creates an object or group of objects responsible for storing data and providing them with application modules. As you will see later, this pattern is simple and straightforward, although in some implementations it acquires a beard so much that it seriously interferes with understanding.
I will begin with a simple one, why did the need for new patterns arise, and why did some of the old patterns become very limited in their scope?
')
In my opinion, the main part of the changes was introduced by the mass introduction of auto-testing. And for those who actively write autotests, this article is obvious as daylight, you can not read further. Only you can not imagine how many people do not write them. I understand that small companies and startups do not have this resources, but, unfortunately, large companies often have more immediate 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 of y, and after spending some time, you discover a singleton inside the function, which in some states results in the result of the function being executed to a different value. This singleton was called an
implicit dependency , and in every possible way we did not want to use it in such situations. Unfortunately, you can’t throw out the words from the song, otherwise you’ll have a completely different song. Therefore, we will render our singleton as an input variable in a function. Now we have 3 incoming variables a, b, s. It seems everything is obvious: changing the parameters - we get an unambiguous result.
While I will not give examples. Moreover, it is not only about functions within a class, it is a schematic argument that can be applied also to the creation of a class, module, and so on.
Remark 1. If, taking into account the criticism of the pattern of singleton, you decided to replace it, well, for example, with UserDefaults, then with respect to this situation, the same implicit dependence emerges.
Remark 2. It is not entirely correct to say that only because of autotesting you should not use singletones inside the function body. In general, from the point of view of programming is not entirely correct, that with the same incoming - the function produces different results. Just on auto tests this problem appeared more clearly.
Let's complete the above example. You have an object that contains 9 user settings (variables), for example, rights to read / edit / sign / print / forward / delete / block / 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 transmitted objects in order not to set many parameters, that is, we choose the first option. Such a method would be considered a transfer of
“unreasonably wide dependencies” . As you have already guessed, for the purposes of auto-testing it is better to use the second option and transfer 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 excessive input parameters
We wanted the best - and got a function with 6 parameters. Suppose that everything is in order inside the function, but someone has to take the job of providing the incoming parameters of the function. As I already wrote, my reasoning is sketchy. I mean not just the usual class function, but rather the initialization function / module creation (vip, viper, data object, etc.). In this context, let us rephrase the question: who should provide the incoming parameters to create a 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 causes the following complications:
First, a little earlier, we decided to avoid “unnecessarily broad dependencies”. Secondly, there is no need to strain hard to understand that there will be a lot of parameters, and each time it will be very tedious to edit them when adding child modules, it’s painful to even think about deleting child modules. By the way, in some applications it is generally impossible to build a hierarchy of modules: look at any social network: profile -> friends -> friend's profile -> friends of a friend, etc. Thirdly, on this topic, we can recall the principle of SOLI
D : “Top-level modules do not depend on lower-level modules”
From here the thought is born to carry out creation / initialization of the module in a separate construction. 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)
In the example, there is the AccountList list of accounts module, which calls the module for detailed information on the AccountDetail account.
To initialize the AccountDetail module, you need 3 variables. AccountDetail receives account variable from the parent module, permission1, permission2 variables are injected by injection. Due to the injection, calling the module with the details of the account 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 AccountList list of accounts will be released from the obligation to transfer parameters with permisense, about which he knows nothing.
I rendered the implementation of the injection (assembly) into a static function in the extension class. 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, you do not need to pass on anything for children or just the necessary minimum.
- The modules do NOT do the work of providing data, they use the already prepared data (p1, p2). Thus, if you want to change something in the storage or provision of data, then you will not have to make changes to the functional code of the modules (as well as to their autotests), but will only need to change the build system itself, or the extensions with the assembly.
The essence of dependency injection is the construction of such a process where, when calling one module from another, an independent object / mechanism transmits (injects) data into the called module. In other words, the called module is configured externally.
There are several ways to configure:
Constructor Injection ,
Property injection ,
Interface Injection .
For Swift:
Initializer Injection ,
Property Injection ,
Method Injection .
The most common are injections of the constructor (initialization) and properties.
Important: Practically in all sources it is recommended to give preference to the designer’s injection. 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 object that has already been created and use the second method. I am for the first method:
- creation by the designer guarantees a valid object;
- at Property injection, it is not clear whether it is necessary to test the change of the property, in other places than the creation;
- in languages ​​that use optional properties, to implement Property injection, you need to make fields optional, or invent clever initialization methods (it’s not always possible to be lazy). Excessiveness adds unnecessary code and unnecessary test suites.
However, until we got rid of any dependencies, we just shifted them from one shoulder to the other. The logical question is where to get the data in the assembly itself (the make function in the example).
The use of singletons in the assembly mechanism no longer leads to the problems described above with hidden dependency, since You can test the creation of modules with any data set.
But here we are confronted with another drawback of singletons: poor controllability (you can probably cite many more hateful arguments, but laziness). There is nothing good about scattering your numerous storage / singletons in assemblies, by analogy with whom, as they were scattered in functional modules. But even such refactoring will be the first step towards hygiene, because then you can clean up the order in assemblies almost without affecting the code and tests of the modules.
If you want to streamline the architecture further, as well as test the transitions and assembly work, you will have to do a little more work.
The concept of DI offers us to store all the necessary data in a container. It's comfortable. First of all, saving (registering) and receiving (resolve) data goes through one container object, respectively, so it is easier to manage and test data. Secondly, you can consider the dependence of data from each other. In many languages, including swift, there are ready-made dependency management containers, usually dependencies form a tree. The rest of the pros and cons, I will not list, you can read about them on the links that I posted at the beginning of the post.
That's about how the assembly might look like using a container.
import Foundation import Swinject public class Configurator { private static let container = Container() public static func register<T>(name: String, value: T) { container.register(type(of: value), name: name) { _ in value } } public static func resolve<T>(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 } } }
This is a possible implementation example. The example uses the
Swinject framework, which was born not too long ago. Swinject allows you to create a container for automated dependency management, and also allows you to create containers for Storyboards. More details about Swinject can be viewed in the examples on
raywenderlich . I really like this site, but this example is not the most successful, because it considers the use of the container only in auto-tests, while the container should be incorporated in the application architecture. You can write your own container in your code.
Thanks to all of this. I hope you are not much bored reading this text.
For more information I recommend reading the
translation from
Kiselioff .