Calm calm strife
Three years ago, I wrote an article about the DI library for the Swift language. Since then, the library has changed a lot and become the best of its kind worthy competitor to Swinject, surpassing it in many respects. The article is devoted to the capabilities of the library, but also has theoretical considerations. So, who are interested in the topics of DI, DIP, IoC, or who makes a choice between Swinject and Swinject, I ask for cat:
Theory is one of the most important components in programming. Yes, you can write code without education, but despite this, programmers constantly read articles, are interested in various practices, etc. That is, one way or another I get theoretical knowledge in order to put it into practice.
One of the topics that people like to ask for interviews is SOLID . No article is not about him at all, do not be alarmed. But we need one letter, as it is closely related to my library. This is the letter `D` - Dependency Inversion Principle.
The Dependency Inversion Principle states:
Many people mistakenly assume that if they use protocols / interfaces, then they automatically adhere to this principle, but this is not entirely true.
The first statement says something about dependencies between modules - modules must depend on abstractions. Wait, what is abstraction? - It’s better to ask yourself not what abstraction is, but what abstraction is? That is, you need to understand what the process is, and the result of this process will be an abstraction. Abstraction is a distraction in the process of cognition from non-essential parties, properties, relationships in order to highlight essential, regular signs.
The same object, depending on the goals, can have different abstractions. For example, the machine from the point of view of the owner has the following important properties: color, elegance, convenience. But from the point of view of the mechanic, everything is somewhat different: brand, model, modification, mileage, participation in an accident. Two different abstractions for one object have just been named - the machine.
Note that in Swift it is customary to use protocols for abstractions, but this is not a requirement. No one bothers to make a class, allocate a set of public methods from it, and leave implementation details private. In terms of abstraction, nothing is broken. We must remember the important thesis - “abstraction is not tied to the language” - this is a process that happens constantly in our head, and how this is transferred to the code is not so important. Here you can also mentionencapsulation , as an example of what is associated with the language. Each language has its own means in order to provide it. On Swift, these are classes, access fields, and protocols; on Obj-C interfaces, protocols and separation of h and m files.
the second statement is more interesting, since it is ignored or misunderstood. It talks about the interaction of abstractions with details, and what are details? There is a misconception that the details are classes that implement protocols - yes this is true, but not complete. You need to understand that the details are not tied to programming languages - the C language has neither protocols nor classes, but this principle also acts on it. It is difficult for me to theoretically explain what the catch is, so I will give two examples, and then try to prove why the second example is more correct.
Suppose there is a class car and a class engine. It so happened that we need to connect them - the machine contains an engine. We, as competent programmers, select the protocol engine, implement the protocol and pass the implementation of the protocol to the machine class. Everything seems to be good and right - now you can easily replace the engine implementation and not think that something will break. Next, an engine mechanic is added to the circuit. He is interested in the engine completely different characteristics than the car. We are expanding the protocol and now it contains a larger set of features than initially. The story is repeated for the owner of the car, for the factory producing engines, etc.
But where is the error in reasoning? The problem is that the described connection, despite the availability of protocols, is actually a “detail” - a “detail”. More precisely, in what name and where the protocol is located the engine.
Now consider thecorrect other option.
As before, there are two classes - engine and car. As before, they must be connected. But now we are announcing the protocol “Car Engine” or “Heart of a Car”. We place in it only those characteristics that the car needs from the engine. And we place the protocol not next to its “engine” implementation, but next to the machine. Further, if we need a mechanic, we will need to create another protocol and implement it in the engine. It seems that nothing has changed, but the approach is radically different - the question is not so much in the names, but in who the protocols belong to and what the protocol is - an “abstraction” or a “detail”.
Now let's draw an analogy with another case, as these arguments may not be obvious.
There is a backend and some functionality is needed from it. Backend gives us a large method that contains a bunch of data, and says - “you need these 3 fields out of 1000”
Sounds not very good, right? Usually the backend writes a method for specific tasks for the frontend, and frontend is the customer / user of these methods. Hmm ... But if you think about it, the backend is the engine, and the frontend is the car - the machine needs some engine characteristics, and not the engine needs to be given the characteristics for the car. So why, in spite of this, we continue to write the protocol Engine and place it closer to the implementation of the engine, and not the machine? It's all about the scale - in most iOS programs it is very rare to have to expand the functionality so much that such a solution becomes a problem.
There is a substitution of concepts - DI is not an abbreviation for DIP, but a completely different abbreviation, despite the fact that it intersects very closely with DIP. DI is a dependency injection or Dependency Injection, not Inversion. Inversion talks about how classes and protocols should interact with each other, and implementation tells you where to get them from. In general, you can implement it in various ways - starting where the dependencies come: constructor, property, method; ending with those who create them and how automated this process is. The approaches are different but, in my opinion, the most convenient are containers for dependency injection. In short, their whole meaning boils down to a simple rule: We tell the container where and how to implement it and after that everything is implemented independently.
In many languages, the following approach is used for this implementation: In individual classes / files, implementation rules are described using the language syntax, after which they are compiled and automatically implemented. There is no magic - nothing happens automatically, just the libraries are closely integrated with the basic means of the language, and overload the creation methods. So for Swift / Obj-C it is generally accepted that the starting point is the UIViewController, and libraries can easily integrate themselves into the created ViewController from the Storyboard. True, if you do not use the Storyboard, you will have to do part of the work with pens.
Oh yes, I almost forgot - the answer to the main question, “why do we need this?” Undoubtedly, you can take care of dependency injection yourself, prescribe everything with pens. But problems arise when the graphs become large - you have to mention a lot of connections between classes, the code starts to grow very much. Therefore, libraries that automatically implement recursively (and even cyclically) dependencies take this care upon themselves and, as a bonus, control their lifetime. That is, the library does nothing beyond the natural - it simply simplifies the life of the developer. True, do not think that you can write such a library in a day - it’s one thing to write with pen all the dependencies for a particular case, it’s another thing to teach a computer to implement universally and correctly.
The story would not be complete if I did not tell the story briefly. If you follow the library from the beta version, it will not be so interesting for you, but for those who see it for the first time, I think it’s worth understanding how it appeared and what goals the author followed (that is, I).
The library was my second project, which I decided, for the purposes of self-education, to write in Swift. Before that I managed to write a logger, but did not upload it to the public domain - it’s better and better.
But with DI, the story is more interesting. When I started doing it, I was able to find only one library on Swift - Swinject. At that time, she had 500 stars and bugs that the cycles are not normally processed. I looked at all this and ... My behavior is best described by my favorite phrase “And then Ostap suffered” - I went through 5-6 languages, looked at what is in these languages, read articles on this topic and realized that it can be done better. And now, after almost three years, I can say with confidence that the goal has been achieved, at the moment DITranquillity is the best in my worldview.
Let's understand what a good DI library is:
It is these principles that I try to adhere to throughout the development of the library.
First, a link to the repository: github.com/ivlevAstef/DITranquillity
The main competitive advantage, which is quite important for me, is that the library talks about startup errors. After starting the application and calling the desired function, all problems, both existing and potential, will be reported. This is precisely the meaning of the name of the library “calm” - in fact, after starting the program, the library guarantees that all the required dependencies will exist and there are no unsolvable cycles. In places where there is ambiguity, the library will warn that there may be potential problems.
It sounds just fine to me. There are no crashes during the execution of the program, if the programmer forgot something, then this will be immediately reported.
A log function is used to describe the problems, which I highly recommend using. Logging has 4 levels: error, warning, info, verbose. The first three are quite important. The latter is not so important - he writes everything that happens - which object was registered, which object began to be introduced, what object was created, etc.
But this is not all the library boasts:
And so let's get started. As last time the project will be considered: SampleHabr . I specifically did not begin to change the example - so you can compare how everything has changed. And the example displays many features of the library.
Just in case, so that there is no misunderstanding, since the project is on display, it uses many features. But no one bothers to use the library in a simplified way - downloaded, created a container, registered a couple of classes, use the container.
First we need to create a framework (optional):
And at the start of the program, create your own container, with the addition of this framework:
Next, you need to create a basic screen. Usually Storyboards are used for this, and in this example I will use it, but no one bothers to use UIViewControllers.
To begin with, we need to register a Storyboard. To do this, create a “part” (optional - you can write all the code in the framework) with the Storyboard registered in it:
And add a part to AppFramework:
As you can see, the library has a convenient syntax for registering Storyboard, and I highly recommend using it. In principle, you can write equivalent code without this method, but it will be larger and will not be able to support StoryboardReferences. That is, this Storyboard will not work from another.
Now the only thing left is to create a Storyboard and show the start screen. This is done in AppDelegate, after checking the container:
Creating a Storyboard using a library is not much more complicated than usual. In this example, the name could be missed, since we have only one Storyboard - the library would have guessed that you had it in mind. But in some projects there are a lot of Storyboards, so do not miss the name again.
Go to the screen itself. We will not load the project with complex architectures, but we will use the usual MVP. Moreover, I am so lazy that I will not create a protocol for a presenter. The protocol will be a little later for another class, here it is important to show how to register and link Presenter and ViewController.
To do this, add the following code to AppPart:
These three lines will allow us to register two classes, and establish a connection between them.
Curious people may wonder - why is the syntax that Swinject has in a separate library made the main one in the project? The answer lies in the goals - thanks to this syntax, the library stores all the links in advance, rather than calculating them at runtime. This syntax gives you access to many features that are not available to other libraries.
We start the application, and everything works, all classes are created.
Well, now we need to add a class and protocol to receive data from the server:
And for beauty, we will create a separate ServerPart DI class for the server, in which we register it. Let me remind you that this is not necessary and can be registered directly in the container, but we are not looking for easy ways :)
In this code, everything is not as transparent as in the previous ones, and requires clarification. Firstly, inside the function register, a class is created with a parameter passed.
Secondly, there is the `as` function - it says that the class will be accessible by another type - the protocol. The strange end of this operation in the form of `{$ 0}` is part of the name `check:`. That is, this code ensures that ServerImpl is a successor to Server. But there is another syntax: `as (Server.self)` which will do the same, but without checking. To see what the compiler will output in both cases, you can remove the protocol implementation.
There may be several `as` functions - this will mean that the type is available by any of these names. I draw your attention that this will be one registration, which means that if the class is a singleton, then the same instance will be available for any specified type.
In principle, if you want to protect yourself from the possibility of creating a class by type of implementation, or you have not yet got used to this syntax, then you can write:
Which will be some equivalent, but without the ability to specify several separate types.
Now you can implement the server in Presenter, for this we’ll fix Presenter so that it accepts Server:
We start the program, and it falls on the `validate` functions in AppDelegate, with the message that the type` Server` was not found, but it is required by `YourPresenter`. What's the matter? Please note that the error occurred at the beginning of the execution of the program, and not a post factum. And the reason is quite simple - they forgot to add `ServerPart` to the` AppFramework`:
We start - everything works.
Prior to this, there was an acquaintance with opportunities that are not very impressive and many have. Now there will be a demonstration that other libraries on Swift do not know how.
A separate Project was created under the logger .
First, let's understand what will be a logger. For educational purposes, we will not do a tricked out system, so the logger is a protocol with one method and several implementations:
Total, we have:
The project created `LoggerFramework` and` LoggerPart`. I will not write out their code, but I will write out only the internals of `LoggerPart`:
We have already seen the first 3 registrations, and the last raises questions.
A parameter is passed to the input. A similar one was already shown when the presenter was created, although there was an abbreviated record - the `init` method was just used, but no one bothers to write like this:
If there were several parameters, then one could use `$ 1`,` $ 2`, `$ 3`, etc. to 16.
But this parameter calls the `many` function. And here the fun begins. There are two modifiers `many` and` tag` in the library.
The tag, in turn, is a separate any type that must be specified both during use and during registration. That is, tags are additional criteria if there are not enough basic types.
You can read more about this: Modifiers
The presence of modifiers, especially `many`, makes the library better than others. For example, you can implement the Observer pattern at a completely different level. Due to these 4 letters, in the project it was possible to remove 30-50 lines of code from each Observer in the project and solve the problem with the question - where and when should objects be added to the Observable. Clear business is not the only application, but significant.
Well, we’ll finish the presentation of features by introducing a logger in YourPresenter:
Here, for example, it is written a little differently than before - this is done for an example of a different syntax.
Please note that the logger property is optional:
And this does not appear in the syntax of the library. Unlike the first version, now all operations for the usual type, optional and forced optional look the same. Moreover, the logic inside is different - if the type is optional, and it is not registered in the container, then the program will not crash, but will continue execution.
The results are similar to the last time, only the syntax has become shorter and more functional.
First of all, it is planned to switch to checking the graph at the compilation stage - that is, closer integration with the compiler. There is a preliminary implementation using SourceKitten, but such an implementation has serious difficulties with type inference, so it is planned to switch to ast-dump - in swift5 it became working on large projects. Here I want to say thanks to Nekitosss for the huge contribution in this direction.
Secondly, I would like to integrate with visualization services. This will be a slightly different project, but closely related to the library. What's the point? Now the library stores the entire graph of connections, that is, in theory everything that is registered in the library can be shown as a UML class / component diagram. And it would be nice to sometimes see this diagram.
This functionality is planned in two parts - the first part will allow you to add an API to get all the information, and the second is already integration with various services.
The simplest option is to display a graph of links in the form of text, but I have not seen readable options - if so, suggest options in the comments.
WatchOS - I myself do not write projects for them. For his life he wrote only once, and then small. But I would like to make tight integration, as with the Storyboard.
That's all thank you for your attention. I really hope for comments and answers to the survey.
What is DIP, IoC and what does it eat with?
Theory of DIP and IoC
Theory is one of the most important components in programming. Yes, you can write code without education, but despite this, programmers constantly read articles, are interested in various practices, etc. That is, one way or another I get theoretical knowledge in order to put it into practice.
One of the topics that people like to ask for interviews is SOLID . No article is not about him at all, do not be alarmed. But we need one letter, as it is closely related to my library. This is the letter `D` - Dependency Inversion Principle.
The Dependency Inversion Principle states:
- Upper level modules should not depend on lower level modules. Both types of modules should depend on abstractions.
- Abstractions should not depend on the details. Details should depend on abstractions.
Many people mistakenly assume that if they use protocols / interfaces, then they automatically adhere to this principle, but this is not entirely true.
The first statement says something about dependencies between modules - modules must depend on abstractions. Wait, what is abstraction? - It’s better to ask yourself not what abstraction is, but what abstraction is? That is, you need to understand what the process is, and the result of this process will be an abstraction. Abstraction is a distraction in the process of cognition from non-essential parties, properties, relationships in order to highlight essential, regular signs.
The same object, depending on the goals, can have different abstractions. For example, the machine from the point of view of the owner has the following important properties: color, elegance, convenience. But from the point of view of the mechanic, everything is somewhat different: brand, model, modification, mileage, participation in an accident. Two different abstractions for one object have just been named - the machine.
Note that in Swift it is customary to use protocols for abstractions, but this is not a requirement. No one bothers to make a class, allocate a set of public methods from it, and leave implementation details private. In terms of abstraction, nothing is broken. We must remember the important thesis - “abstraction is not tied to the language” - this is a process that happens constantly in our head, and how this is transferred to the code is not so important. Here you can also mentionencapsulation , as an example of what is associated with the language. Each language has its own means in order to provide it. On Swift, these are classes, access fields, and protocols; on Obj-C interfaces, protocols and separation of h and m files.
the second statement is more interesting, since it is ignored or misunderstood. It talks about the interaction of abstractions with details, and what are details? There is a misconception that the details are classes that implement protocols - yes this is true, but not complete. You need to understand that the details are not tied to programming languages - the C language has neither protocols nor classes, but this principle also acts on it. It is difficult for me to theoretically explain what the catch is, so I will give two examples, and then try to prove why the second example is more correct.
Suppose there is a class car and a class engine. It so happened that we need to connect them - the machine contains an engine. We, as competent programmers, select the protocol engine, implement the protocol and pass the implementation of the protocol to the machine class. Everything seems to be good and right - now you can easily replace the engine implementation and not think that something will break. Next, an engine mechanic is added to the circuit. He is interested in the engine completely different characteristics than the car. We are expanding the protocol and now it contains a larger set of features than initially. The story is repeated for the owner of the car, for the factory producing engines, etc.
But where is the error in reasoning? The problem is that the described connection, despite the availability of protocols, is actually a “detail” - a “detail”. More precisely, in what name and where the protocol is located the engine.
Now consider the
As before, there are two classes - engine and car. As before, they must be connected. But now we are announcing the protocol “Car Engine” or “Heart of a Car”. We place in it only those characteristics that the car needs from the engine. And we place the protocol not next to its “engine” implementation, but next to the machine. Further, if we need a mechanic, we will need to create another protocol and implement it in the engine. It seems that nothing has changed, but the approach is radically different - the question is not so much in the names, but in who the protocols belong to and what the protocol is - an “abstraction” or a “detail”.
Now let's draw an analogy with another case, as these arguments may not be obvious.
There is a backend and some functionality is needed from it. Backend gives us a large method that contains a bunch of data, and says - “you need these 3 fields out of 1000”
Little story
Many can say that this does not happen. And they will be relatively right - it happens that the backend is written separately for the mobile application. It so happened that I worked for a company where backend is a service with a 10 year history that, among other things, is tied to the state API. For many reasons, it was not customary for the company to write a separate method for the mobile, and I had to use what was. And there was one wonderful method with about a hundred parameters in the root, and some of them were nested dictionaries. Now imagine 100 parameters, 20% of which have nested parameters, and within each nested one there are another 20-30 parameters that have all the same nesting. I don’t remember exactly, but the number of parameters exceeded 800 for simple objects, and for complex ones it could be higher than 1000.
Sounds not very good, right? Usually the backend writes a method for specific tasks for the frontend, and frontend is the customer / user of these methods. Hmm ... But if you think about it, the backend is the engine, and the frontend is the car - the machine needs some engine characteristics, and not the engine needs to be given the characteristics for the car. So why, in spite of this, we continue to write the protocol Engine and place it closer to the implementation of the engine, and not the machine? It's all about the scale - in most iOS programs it is very rare to have to expand the functionality so much that such a solution becomes a problem.
And then what is DI
There is a substitution of concepts - DI is not an abbreviation for DIP, but a completely different abbreviation, despite the fact that it intersects very closely with DIP. DI is a dependency injection or Dependency Injection, not Inversion. Inversion talks about how classes and protocols should interact with each other, and implementation tells you where to get them from. In general, you can implement it in various ways - starting where the dependencies come: constructor, property, method; ending with those who create them and how automated this process is. The approaches are different but, in my opinion, the most convenient are containers for dependency injection. In short, their whole meaning boils down to a simple rule: We tell the container where and how to implement it and after that everything is implemented independently.
In many languages, the following approach is used for this implementation: In individual classes / files, implementation rules are described using the language syntax, after which they are compiled and automatically implemented. There is no magic - nothing happens automatically, just the libraries are closely integrated with the basic means of the language, and overload the creation methods. So for Swift / Obj-C it is generally accepted that the starting point is the UIViewController, and libraries can easily integrate themselves into the created ViewController from the Storyboard. True, if you do not use the Storyboard, you will have to do part of the work with pens.
Oh yes, I almost forgot - the answer to the main question, “why do we need this?” Undoubtedly, you can take care of dependency injection yourself, prescribe everything with pens. But problems arise when the graphs become large - you have to mention a lot of connections between classes, the code starts to grow very much. Therefore, libraries that automatically implement recursively (and even cyclically) dependencies take this care upon themselves and, as a bonus, control their lifetime. That is, the library does nothing beyond the natural - it simply simplifies the life of the developer. True, do not think that you can write such a library in a day - it’s one thing to write with pen all the dependencies for a particular case, it’s another thing to teach a computer to implement universally and correctly.
Library history
The story would not be complete if I did not tell the story briefly. If you follow the library from the beta version, it will not be so interesting for you, but for those who see it for the first time, I think it’s worth understanding how it appeared and what goals the author followed (that is, I).
The library was my second project, which I decided, for the purposes of self-education, to write in Swift. Before that I managed to write a logger, but did not upload it to the public domain - it’s better and better.
But with DI, the story is more interesting. When I started doing it, I was able to find only one library on Swift - Swinject. At that time, she had 500 stars and bugs that the cycles are not normally processed. I looked at all this and ... My behavior is best described by my favorite phrase “And then Ostap suffered” - I went through 5-6 languages, looked at what is in these languages, read articles on this topic and realized that it can be done better. And now, after almost three years, I can say with confidence that the goal has been achieved, at the moment DITranquillity is the best in my worldview.
Let's understand what a good DI library is:
- It should provide all basic implementations: constructor, properties, methods
- It should not affect business code.
- She should clearly describe what went wrong.
- She must understand in advance where there are errors, not at runtime.
- It must be integrated with basic tools (Storyboard)
- It should have a concise, concise syntax.
- She must do everything quickly and efficiently.
- (Optional) It should be hierarchical
It is these principles that I try to adhere to throughout the development of the library.
Features and Benefits of the Library
First, a link to the repository: github.com/ivlevAstef/DITranquillity
The main competitive advantage, which is quite important for me, is that the library talks about startup errors. After starting the application and calling the desired function, all problems, both existing and potential, will be reported. This is precisely the meaning of the name of the library “calm” - in fact, after starting the program, the library guarantees that all the required dependencies will exist and there are no unsolvable cycles. In places where there is ambiguity, the library will warn that there may be potential problems.
It sounds just fine to me. There are no crashes during the execution of the program, if the programmer forgot something, then this will be immediately reported.
A log function is used to describe the problems, which I highly recommend using. Logging has 4 levels: error, warning, info, verbose. The first three are quite important. The latter is not so important - he writes everything that happens - which object was registered, which object began to be introduced, what object was created, etc.
But this is not all the library boasts:
- Full thread-safety - any operation can be done from any thread and everything will work. Most people do not need this, so in terms of thread-safety, work was done to optimize the speed of execution. But the competitor library, despite the promises, falls if you start to register and receive an object at the same time
- Fast execution speed. On a real device, DITranquillity is twice as fast as its competitor. True on the simulator, the execution speed is almost equivalent. Test Link
- Small size - the library weighs less than Swinject + SwinjectStoryboad + SwinjectAutoregistration, but surpasses this bundle in capabilities
- A concise, concise note, although addictive
- Hierarchy. For large projects, which consist of many modules, this is a very big plus, since the library is able to find the necessary classes by distance from the current module. That is, if you have your own implementation of one protocol in each module, then in each module you will get the desired implementation without making any effort
Demonstration
And so let's get started. As last time the project will be considered: SampleHabr . I specifically did not begin to change the example - so you can compare how everything has changed. And the example displays many features of the library.
Just in case, so that there is no misunderstanding, since the project is on display, it uses many features. But no one bothers to use the library in a simplified way - downloaded, created a container, registered a couple of classes, use the container.
First we need to create a framework (optional):
public class AppFramework: DIFramework { // центральный фреймворк
public static func load(container: DIContainer) {
//позже сюда будем добавлять код
}
}
And at the start of the program, create your own container, with the addition of this framework:
let container = DIContainer() // создаем контейнер
container.append(framework: AppFramework.self)
// функция проверки валидности графа связей.
// на самом деле эту функцию я рекомендую включать в ifdef DEBUG так как она требует времени исполнения, а граф зависимостей от запуска к запуску не изменяется, при условии не изменения кода.
if !container.validate() {
fatalError()
}
Storyboard
Next, you need to create a basic screen. Usually Storyboards are used for this, and in this example I will use it, but no one bothers to use UIViewControllers.
To begin with, we need to register a Storyboard. To do this, create a “part” (optional - you can write all the code in the framework) with the Storyboard registered in it:
import DITranquillity
class AppPart: DIPart {
static func load(container: DIContainer) {
container.registerStoryboard(name: "Main", bundle: nil)
.lifetime(.single) // время жизни - один на всю программу.
}
}
And add a part to AppFramework:
container.append(part: AppPart.self)
As you can see, the library has a convenient syntax for registering Storyboard, and I highly recommend using it. In principle, you can write equivalent code without this method, but it will be larger and will not be able to support StoryboardReferences. That is, this Storyboard will not work from another.
Now the only thing left is to create a Storyboard and show the start screen. This is done in AppDelegate, after checking the container:
window = UIWindow(frame: UIScreen.main.bounds)
/// создаем Storyboard
let storyboard: UIStoryboard = container.resolve(name: "Main")
window!.rootViewController = storyboard.instantiateInitialViewController()
window!.makeKeyAndVisible()
Creating a Storyboard using a library is not much more complicated than usual. In this example, the name could be missed, since we have only one Storyboard - the library would have guessed that you had it in mind. But in some projects there are a lot of Storyboards, so do not miss the name again.
Presenter and ViewController
Go to the screen itself. We will not load the project with complex architectures, but we will use the usual MVP. Moreover, I am so lazy that I will not create a protocol for a presenter. The protocol will be a little later for another class, here it is important to show how to register and link Presenter and ViewController.
To do this, add the following code to AppPart:
container.register(YourPresenter.init)
container.register(YourViewController.self)
.injection(\.presenter) // устанавливаем связь
These three lines will allow us to register two classes, and establish a connection between them.
Curious people may wonder - why is the syntax that Swinject has in a separate library made the main one in the project? The answer lies in the goals - thanks to this syntax, the library stores all the links in advance, rather than calculating them at runtime. This syntax gives you access to many features that are not available to other libraries.
We start the application, and everything works, all classes are created.
Data
Well, now we need to add a class and protocol to receive data from the server:
public protocol Server {
func get(method: String) -> Data?
}
class ServerImpl: Server {
init(domain: String) {
...
}
func get(method: String) -> Data? {
...
}
}
And for beauty, we will create a separate ServerPart DI class for the server, in which we register it. Let me remind you that this is not necessary and can be registered directly in the container, but we are not looking for easy ways :)
import DITranquillity
class ServerPart: DIPart {
static func load(container: DIContainer) {
container.register{ ServerImpl(domain: "https://github.com/") }
.as(check: Server.self){$0}
.lifetime(.single)
}
}
In this code, everything is not as transparent as in the previous ones, and requires clarification. Firstly, inside the function register, a class is created with a parameter passed.
Secondly, there is the `as` function - it says that the class will be accessible by another type - the protocol. The strange end of this operation in the form of `{$ 0}` is part of the name `check:`. That is, this code ensures that ServerImpl is a successor to Server. But there is another syntax: `as (Server.self)` which will do the same, but without checking. To see what the compiler will output in both cases, you can remove the protocol implementation.
There may be several `as` functions - this will mean that the type is available by any of these names. I draw your attention that this will be one registration, which means that if the class is a singleton, then the same instance will be available for any specified type.
In principle, if you want to protect yourself from the possibility of creating a class by type of implementation, or you have not yet got used to this syntax, then you can write:
container.register{ ServerImpl(domain: "https://github.com/") as Server }
Which will be some equivalent, but without the ability to specify several separate types.
Now you can implement the server in Presenter, for this we’ll fix Presenter so that it accepts Server:
class YourPresenter {
init(server: Server) {
...
}
}
We start the program, and it falls on the `validate` functions in AppDelegate, with the message that the type` Server` was not found, but it is required by `YourPresenter`. What's the matter? Please note that the error occurred at the beginning of the execution of the program, and not a post factum. And the reason is quite simple - they forgot to add `ServerPart` to the` AppFramework`:
container.append(part: ServerPart.self)
We start - everything works.
Logger
Prior to this, there was an acquaintance with opportunities that are not very impressive and many have. Now there will be a demonstration that other libraries on Swift do not know how.
A separate Project was created under the logger .
First, let's understand what will be a logger. For educational purposes, we will not do a tricked out system, so the logger is a protocol with one method and several implementations:
public protocol Logger {
func log(_ msg: String)
}
class ConsoleLogger: Logger {
func log(_ msg: String) { ... }
}
class FileLogger: Logger {
init(file: String) { ... }
func log(_ msg: String) { ... }
}
class ServerLogger: Logger {
init(server: String) { ... }
func log(_ msg: String) { ... }
}
class MainLogger: Logger {
init(loggers: [Logger]) { ... }
func log(_ msg: String) { ... }
}
Total, we have:
- Public protocol
- 3 different logger implementations, each of which writes to a different place
- One central logger that calls the logging function for everyone else
The project created `LoggerFramework` and` LoggerPart`. I will not write out their code, but I will write out only the internals of `LoggerPart`:
container.register{ ConsoleLogger() }
.as(Logger.self)
.lifetime(.single)
container.register{ FileLogger(file: "file.log") }
.as(Logger.self)
.lifetime(.single)
container.register{ ServerLogger(server: "http://server.com/") }
.as(Logger.self)
.lifetime(.single)
container.register{ MainLogger(loggers: many($0)) }
.as(Logger.self)
.default()
.lifetime(.single)
We have already seen the first 3 registrations, and the last raises questions.
A parameter is passed to the input. A similar one was already shown when the presenter was created, although there was an abbreviated record - the `init` method was just used, but no one bothers to write like this:
container.register { YourPresenter(server: $0) }
If there were several parameters, then one could use `$ 1`,` $ 2`, `$ 3`, etc. to 16.
But this parameter calls the `many` function. And here the fun begins. There are two modifiers `many` and` tag` in the library.
Hidden text
The `many` modifier says that you need to get all the objects corresponding to the desired type. In this case, the Logger protocol is expected, so all classes that inherit from this protocol will be found and created, with one exception - itself, that is, recursively. It will not create itself during initialization, although it can safely do this when implemented through a property. There is a third `arg` modifier, but it is not safe
The tag, in turn, is a separate any type that must be specified both during use and during registration. That is, tags are additional criteria if there are not enough basic types.
You can read more about this: Modifiers
The presence of modifiers, especially `many`, makes the library better than others. For example, you can implement the Observer pattern at a completely different level. Due to these 4 letters, in the project it was possible to remove 30-50 lines of code from each Observer in the project and solve the problem with the question - where and when should objects be added to the Observable. Clear business is not the only application, but significant.
Well, we’ll finish the presentation of features by introducing a logger in YourPresenter:
container.register(YourPresenter.init)
.injection { $0.logger = $1 }
Here, for example, it is written a little differently than before - this is done for an example of a different syntax.
Please note that the logger property is optional:
internal var logger: Logger?
And this does not appear in the syntax of the library. Unlike the first version, now all operations for the usual type, optional and forced optional look the same. Moreover, the logic inside is different - if the type is optional, and it is not registered in the container, then the program will not crash, but will continue execution.
Summary
The results are similar to the last time, only the syntax has become shorter and more functional.
What was reviewed:
- Briefly register
- Work with Storyboard, but the library can work with StoryboardReferences
- Registration through an initialization method or property
- Multiple implementation
- Work with frameworks and parts
- A bit of validation
What else can the library do:
- 5 lifetimes: single, perRun (.weak / .strong), perContainer (.weak / .strong), objectGraph, prototype
- Tags name
- Logging
- Work with cyclic dependencies
- Take root in the views
- Отложенное внедрение
- Внедрение аргументов, но функционал не документирован в силу его не безопастности
Планы
First of all, it is planned to switch to checking the graph at the compilation stage - that is, closer integration with the compiler. There is a preliminary implementation using SourceKitten, but such an implementation has serious difficulties with type inference, so it is planned to switch to ast-dump - in swift5 it became working on large projects. Here I want to say thanks to Nekitosss for the huge contribution in this direction.
Secondly, I would like to integrate with visualization services. This will be a slightly different project, but closely related to the library. What's the point? Now the library stores the entire graph of connections, that is, in theory everything that is registered in the library can be shown as a UML class / component diagram. And it would be nice to sometimes see this diagram.
This functionality is planned in two parts - the first part will allow you to add an API to get all the information, and the second is already integration with various services.
The simplest option is to display a graph of links in the form of text, but I have not seen readable options - if so, suggest options in the comments.
WatchOS - I myself do not write projects for them. For his life he wrote only once, and then small. But I would like to make tight integration, as with the Storyboard.
That's all thank you for your attention. I really hope for comments and answers to the survey.
About myself
Ивлев Александр Евгеньевич — senior/team lead в iOS команде. Работаю в коммерции 7 лет, под iOS 4.5 года – до этого был С++ разработчиком. Но общий стаж программирования более 15 лет – еще в школе познакомился с этим удивительным миром и так им увлекся, что был период, когда променял игры, еду, туалет, сон на написания кода. По одной из моих статей можно догадаться, что я бывший олимпиадник – соответственно написать грамотную работу с графами мне не составляло сложности. Специальность – Информационно-измерительные системы, и в свое время я был помешан на многопоточности и параллелизме – да я пишу код, в котором делаю допущения и баги на подобные темы, но я осознаю проблемные места и прекрасно понимаю, где можно пренебречь мьютексом, а где не стоит.
Only registered users can participate in the survey. Please come in.
Will you use the library
- 10.5% Definitely Yes - Super 2 Library
- 26.3% Most likely yes, but still not sure 5
- 36.8% Need to think 7
- 15.7% No 3
- 5.2% Throw this bad job 1
- 5.2% I will write in the comments 1