⬆️ ⬇️

Inversion of control in iOS

image



Evgeny Yolchev rsi , KODE iOS timlide



Recently, I increasingly hear about DI. They are interested in my students at Geek University, it is mentioned in chat rooms. Although the pattern is far from young, many do not quite understand it.

Often, DI means a framework, for example, typhoon or swinject. The article details the principles of implementing DI, as well as the principle of IoC. If interested, please under the cat.



DI (dependency injection, Eng. Dependency injection) is the process of providing external dependency to a software component. It is a specific “IoC” form when it is applied to dependency management. In full accordance with the principle of sole responsibility, the object gives care to build the dependencies required by it to an external, specially designed for this general mechanism.


IoC (Inversion of Control, English Inversion of Control) is an important principle of object-oriented programming used to reduce hooking (connectedness) in computer programs.

Despite the fact that the article is about DI, we will begin our journey not with it, but with IoC, for the reason that DI is only one of the IoC types and the picture needs to be seen in its entirety.



IoC



For a start, let's look at what management is. Take the simplest example - the console “Hello world”:



let firstWord = «hello» let secondWord = "world!" let phrase = firstWord + " " + secondWord print(phrase) 


In this example, our commands manage data that is represented by string literals and variables. At this level of abstraction, there is no longer any control, but we can add it using the ternary operator:



 let number = arc4random_uniform(1) let firstWord = number == 0 ? "hello" : "bye" let secondWord = "world!" let phrase = firstWord + " " + secondWord print(phrase) 


Our code has become ambiguous, and now, depending on a random number, the string in the console will change. In other words, the data controls our program. This is the most trivial and simple example of inversion control.



In a typical iOS application, control is everywhere. System, user, server control application. The application controls the server, the user and the system. Our code contains a huge number of objects that also control each other. For example, an object of class AuthViewController can control an object of class AuthService .



Such management of objects in turn is based on several aspects. First, the AuthViewController calls AuthService methods, and second, it creates it. All this leads to high connectivity of objects, using the AuthViewController becomes impossible without an AuthService . This is called a dependency, AuthViewController is completely dependent on AuthService .



There is an opinion that there is nothing terrible in such dependencies. As a rule, our controllers are not re-used and go hand in hand with their services all the time the application is supported. But those who have been supporting long-lived applications know that this is not the case. Requirements are constantly changing, we find bugs, change the flow, make a redesign. If at the same time your application is more complicated than several controllers with a pair of buttons and services that are just wrappers for URLSession, then it sinks in dependencies. The dependencies between the classes form a web, sometimes cyclic dependencies can be detected. You cannot make changes to your classes, because it is not clear how and where they are used, it is easier for you to create a new method than to change the old one. Replacing the class does turn into pain. Challenge his designer scattered on various methods that you also have to change. In the end, you stop understanding what is happening, the code turns into plain text and, armed with a search, you begin to replace words or sentences in this text, checking only compiler errors.



In order to prevent such an outcome of events, many principles and techniques have been invented. For example, one of the principles of the SOLID DIP principle describes how to reduce connectivity when calling methods and this is IoC.



DIP (dependency inversion principle, Eng. Dependency inversion principle) is one of five of the SOLID principles.



Formulation:



The modules of the upper levels should not depend on the modules of the lower levels. Both types of modules must depend on abstractions.

')

Abstractions should not depend on the details. Details must depend on abstractions.

But still, when someone says “ IoC ”, he means the inversion of control when creating dependencies. Further I will use it only in this value. By the way, DIP is almost impossible to implement without IoC , but not vice versa. Using IoC does not guarantee compliance with DIP. Another important nuance. DIP and DI are different principles.



Towards IoC



Actually, IoC is a very simple concept, and you don’t need to read a lot of literature, go to Tibet for a few years to understand Zen and start using it.



As an example, I will consider the class of “knight” ( Knight ) and its “armor” ( Armor ), all classes are shown below.

image

Now look at the implementation of the class Armor



 class Armor { private var boots: Boots? private var pants: Pants? private var belt: Belt? private var chest: hest? private var bracers: Bracers? private var gloves: Gloves? private var helmet: Helmet? func configure() { self.boots = Boots() self.pants = Pants() self.belt = Belt() self.chest = hest() self.bracers = Bracers() self.gloves = Gloves() self.helmet = Helmet() } } 


and Knight



 class Knight { private var armor: Armor? func prepareForBattle() { self.armor = Armor() self.armor.configure() } } 


At first glance, all is well. If we need a knight, we'll just create it.



 let knight = Knight() 


But not everything is so simple. Unfortunately, surrogate examples cannot convey all the pain that this approach carries.



Our classes are soldered together. In Armor make method, 7 classes are created. This makes the classes ossified. With this approach, we cannot simply determine where and how a class is created. If you need to inherit from armor and create, for example, front armor, replacing the helmet, we will have to redefine the whole method.



The only plus in this approach is the speed of writing code, because when creating classes you don’t have to think about the future.



Here is a small example of how it might look in life:



 class FightViewController: BaseViewController { var titleLabel: UIView! var knightList: UIView! override func viewDidLoad() { super.viewDidLoad() self.title = "" //       ,       //   let backgroundView = UIView() //    self.view.addSubview(backgroundView) //    backgroundView.backgroundColor = UIColor.red //   backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true backgroundView.topAnchor.constraint(equalTo: topAnchor).isActive = true backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true let title = Views.BigHeader.View() self.titleLabel = title title.labelView.text = "labelView" self.view.addSubview(title) title.translatesAutoresizingMaskIntoConstraints = false title.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true title.topAnchor.constraint(equalTo: topAnchor).isActive = true title.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true title.heightAnchor.constraint(equalToConstant: 56).isActive = true let knightList = Views.DataView.View() self.knightList = knightList knightList.titleView.text = "knightList" knightList.dataView.text = "" self.view.addSubview(knightList) knightList.translatesAutoresizingMaskIntoConstraints = false knightList.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true knightList.topAnchor.constraint(equalTo: title.topAnchor).isActive = true knightList.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true knightList.heightAnchor.constraint(equalToConstant: 45).isActive = true } } 


This code is easy to find in someone else's project. It perfectly illustrates that creating dependency classes in arbitrary places is not a good idea. In addition, unlike armor, the elements here are not only created, but customized and even positioned. The code has turned to mush.



How can this be improved? Use the “factory method” pattern. It will not solve all problems, but it will make the class more flexible.



The factory method (English Factory Method, also known as the Virtual Designer (English Virtual Constructor)) is a generic design pattern that provides an interface for subclasses to create instances of a certain class.



 class Armor { private var boots: Boots? private var pants: Pants? func configure() { self.boots = makeBoots() self.pants = makePants() } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


Already better, the creation of dependencies rendered in separate methods. They are easy to find, just change without risk of damaging the logic of the class. With inheritance, we can override them and thus override dependencies.



But still, our class should not be aware of the details of creating its dependencies, it should just use them. How to deal with it? It is necessary to move the generating logic from the class to a higher level.



Generating logic is code that creates instances of a class or structure. In other words - the code that generates objects.



 class Armor { private var boots: Boots? private var pants: Pants? func configure(boots: Boots?, pants: Pants?) { self.boots = boots self.pants = pants } } 


Now our class Armor has no idea how its dependencies are created, they are simply passed as arguments. This gives maximum flexibility. We can even replace classes with protocols and completely ignore implementation details.



But our Knight class is not doing so well.



 class Knight { private var armor: Armor? func preapreForBattle() { self.armor = Armor() let boots = makeBoots() let pants = makePants() self.armor?.make(boots: boots, pants: pants) } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


He creates all parts of his armor. You could say our knight is his own blacksmith.

This is wrong, the knights should not forge armor for themselves, not their level of task, but how then to be? You can again take out the generating logic to a higher level, but then the class at the top of the graph will be a huge dump for creating dependencies.



We will come to the aid of another generating pattern - "factory".



Factory (eng. Factory) - an object that creates other objects.

We will build a smithy in which parts of armor will be made and assembled into a single set.



 class Forge { func makeArmor() -> Armor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


The classes Armor and Knight will get rid of the generating logic and will look concisely.



 class Armor { var boots: Boots? var pants: Pants? } class Knight { var armor: Armor? } 


Now the question arises before us: how, where and when to pick up dependencies from the “factory” and transfer them to our classes. So, we finally came to the concepts of DI and SL.



Service Locator (SL)



Let's start with this pattern. First, it is easier. Secondly, many people think that this is DI, although it is not.



SL (service locator) is a design pattern used in software development for encapsulating processes associated with obtaining a service with a strong level of abstraction. This template uses a central registry, known as a “service locator,” which, upon request, returns information (usually objects) necessary to perform a specific task.

What is its essence? In order to get dependencies, a “factory” is transferred from the constructor to the class, from which he chooses what to get.



In this case, our classes will look like this:



 class Forge { func makeArmor() -> Armor { let armor = Armor(forge: self) return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


 class Knight { private let forge: Forge private var armor: Armor? init(forge: Forge) { self.forge = forge configure() } private func configure() { armor = forge.makeArmor() } } 


 class Armor { private let forge: Forge private var boots: Boots? private var pants: Pants? init(forge: Forge) { self.forge = forge configure() } private func configure() { boots = forge.makeBoots() pants = forge.makePants() } } 


 let forge = Forge() let knight = Knight(forge: forge) 


Personally, this approach causes a double feeling to me. On the one hand, the generating logic is in the “factory”, on the other hand, the process of obtaining dependencies is somewhat confused. But the main drawback is that, looking at a class, it is impossible to unambiguously determine its dependencies. He can get anything from the “factory”, a typical development mistake is to create one such “factory” for the entire application. At the same time, the “factory” turns into a huge junk dump and creates the temptation to get inside the classes what they do not really need. At classes contact, restrictions vanishes.



You can imagine that our knight was presented with a treasure chest from which he can get the armor he needs, but no one will prevent him from gaining unnecessary decorations.

It is for this reason that this pattern crossed the line of good and evil and turned into an anti-pattern. If you have a choice between DI and SL, always choose DI.



DI



The second way to deliver dependencies to classes is DI. This is currently the most common pattern. It is so popular that in the world of backend all normal frameworks support it out of the box. Unfortunately, we were not so lucky.



The essence of this pattern is that dependencies are introduced into the class from the outside, while the dependency graph is built inside the DI container, which is a “factory” or a set of “factories”.



Our classes look like this:



 class Armor { var boots: Boots? var pants: Pants? } class Knight { var armor: Armor? } class Forge { func makeArmor() -> Armor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


 class Garrison { lazy var forge: Forge = { return Forge() }() func makeKnight() -> Knight { let knight = Knight() knight.armor = forge.makeArmor() return knight } } 


 let garrison = Garrison() let knight = garrison.makeKnight() 


In this case, the classes look clean, they completely lack generating logic. Two "factories" took full responsibility for the assembly: Garrison and Forge . If desired, the number of these "factories" can be increased to prevent the growth of classes. A good practice is to create a “factory”, responsible for creating any kindred objects. For example, this “factory” can create services, controllers for a particular user story.



At the same time, our knight finally finished doing things that are not appropriate to his status, the squire is in charge of his ammunition, and the knight can concentrate on fights and princesses.

This could be finished, but it is worth talking about some aspects of DI and currently available frameworks.



Types DI



Initializer Injection - dependency injection through a constructor. This approach is used when a class cannot exist without its dependencies, but even if this is not the case, it can be used to more clearly define a class contract. If all the dependencies are declared as arguments to the constructor, it is easy to define them. But do not get carried away, if the class has dozens of dependencies, it is better not to pass them in the constructor (or even better to figure out why your class has so many dependencies).



 class Armor { let boots: Boots let pants: Pants init(boots: Boots, pants: Pants) { self.boots = boots self.pants = pants } } class Forge { func makeArmor() -> Armor { let boots = makeBoots() let pants = makePants() let armor = Armor(boots: boots, pants: pants) return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


Property Injection - the introduction of dependencies through the properties. This method is used when a class has optional dependencies, without which it can do without, or when dependencies can change not only at the object initialization stage.



 class Armor { var boots: Boots? var pants: Pants? } class Forge { func makeArmor() -> Armor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


Method Injection - dependency injection through a method. This method is very similar to Property Injection, but it can be used to introduce a time dependency only at the time of performing an action or more closely associate dependency injection with the class logic.



 class Knight { private var armor: Armor? func winTournament(armor: Armor) { self.armor = armor defeatEnemy() seducePrincess() self.armor = nil } func defeatEnemy() {} func seducePrincess() {} } class Garrison { lazy var forge: Forge = { return Forge() }() func makeKnight() -> Knight { let knight = Knight() return knight } } let garrison = Garrison() let knight = garrison.makeKnight() let armor = garrison.forge.makeArmor() knight.winTournament(armor: armor) 


According to my observations, the most common types are Initializer Injection and Property Injection, Method Injection is less commonly used. And although I have described typical cases of choosing one type or another, we must remember that Swift is a very flexible language, providing more options for choosing a type. For example, even with optional dependencies, you can implement a constructor with optional arguments and nil by default. In this case, you can use Initializer Injection instead of Property Injection. In any case, this is a compromise that can improve or degrade your code, and the choice is yours.



Dip



Simple use of IoC, as in the examples above, brings good dividends by itself, but you can go further and enforce the DIP principle from SOLID. To do this, we will close the dependencies with the protocols, and only the “factories” will know exactly what implementation lies behind this protocol.



 class Knight { var armor: AbstractArmor? } class Forge { func makeArmor() -> AbstractArmor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


In this case, we can easily substitute the implementation of armor with an alternative one.



SOLID is beyond the scope of this article, however, if you do not know what it is, it is better to familiarize yourself with this set of principles. You can start with a good introductory article , continue reading the relevant chapters in this book .



Scopes



By itself, managing the scope of objects is not part of the IoC concept; it’s more likely to be the details of its implementation, but nevertheless it is a very powerful mechanism that makes it possible to drop singletons and solve other problems with common dependencies. The scope determines how long the dependency created inside the “factory” will live, whether it will be created each time anew or saved after the first creation and simply passed by reference.



Since scopes are not described in patterns, each implements and names them as it sees fit. We consider the two most commonly used types.



The standard scope is the behavior that we implemented in all the examples above. “Factory” creates an object, gives it away and forgets about its existence. When you call the factory method again, a new object will be created.



The scope of the container is similar to the behavior of the singleton. When you first call the factory method, a new object is created, then the “factory” saves a reference to it and returns the result of the factory method, with all the other method calls, the new object is not created, but the reference to the first object is returned.



 class Forge { private var armor: AbstractArmor? func makeArmor() -> AbstractArmor { //        if let armor = self.armor { return armor } let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() self.armor = armor return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } } 


As you can see, in the example above, armor is created only once, in all other cases, the previously created instance is returned. Similar to singleton, we will always work with the same class instance, without global scope.



Advantages and disadvantages



Like any other principles in programming IoC is not a silver bullet, it has its advantages:





And cons:





Although my opinion is that the main and only drawback is over-engineering as a result of an unbridled desire to strictly follow the principle of DIP. , , , .



, , . , ? , ? ? , ?



Summarizing



, IoC , , . iOS-, , android- DI, dagger, . , , spring . php-, , , Laravel DI . iOS, , , , . Objective-C , swift.



Fortunately, you do not need to use the famous framework. One of the goals of this article was to show that IoC is not a framework, and the fact that if there is no typhoon in the project, this does not mean that there is no DI. For the implementation of IoC in the project it does not matter whether you choose DI or SL, a rather ordinary “factory” that you can write yourself. Such a “factory” is the simplest DI container.

Source: https://habr.com/ru/post/352088/



All Articles