image


You must admit that it is pleasant after a tiring day to sharply close the workspace in Xcode with a fine hand movement, so that with a sigh of relief, you can open another workspace with your home project.


And if today is also Friday, then you can allow yourself a little mischief, violating a couple of principles and good practices. After all, the only developer who will then have to look in the eye is you: a wonderful, understanding interlocutor who is ready to compromise.


I advise you to stock up on doshirak and energy. Here is a series of articles about how I did not deny myself anything, implementing MVVM in one of my home projects. Today's pilot release is about dependency management.


Introduction


First of all, I want to make a virtual cuming out and admit that I'm a big fan MVVM pattern . For me, it is not limited to a solid arrow pointing from View to ViewModel and dotted in the opposite direction. Correctly prepared MVVM, as it seems to me, is an infrastructure, if you want, a framework that includes, in addition to implementing the pattern itself, a dependency management solution, implementing routing and a number of auxiliary components to simplify life, health and longevity.


This is exactly what did the first MVVM frameworks with which I worked in time immemorial, when mobile platforms there were more than two. That is what I plan to do in the next three articles. And we will start with dependency management, because this is the foundation on which the whole fascinating world of your iOS application rests.


Once I read somewhere that in any quality article there should be good content. So in this article let there be at least something of quality.


Good content



Principles


In order to give my code more integrity, and to my own significance, I decided that all the code in this and subsequent articles should, if possible, obey several general principles. Here they are:


  1. Don't show off. Dumb and understandable code in most cases is better than smart and incomprehensible.
  2. Be short. The code should be so small that it was not a pity to throw it out at any time and write again in one day.
  3. Convenience above rules. If you can make your life easier by sacrificing SOLID principles, sacrifice SOLID principles.
  4. Have fun. If there are different solutions to the problem, choose a more fun one.

Having such a list of principles is very convenient: it justifies all the strange decisions that I intend to make over the course of three articles.


Dependency Management Problem


The problem of dependency management is pretty typical in programming. Few entities in code can boast of independence like your ex. Usually, everything depends on someone. In MVVM, for example, a view controller depends on a view model that prepares data for it. The view model depends on the service that goes to the network for this data. A service depends on another service — a low-level network implementation, and so on. All these entities, which can be a great many, need to be created somewhere and somehow delivered to consumers. For any typical problem, as a rule, there is a typical solution - a pattern.In the case of a dependency management problem, this is the Dependency Injection (DI) container.


I have no intention of explaining in detail what a DI container is. This is famously described in two articles from the Ninject repository: times , two (remove the children from the screen, there is a code in C #). There is also a small explanation in the repository of the most popular DI container for iOS - Swinject (noticed that Swinject is Ninject on Swift?). I can offer hardcore workers Fowler's article from 2004.


Nevertheless, I cannot deny myself the pleasure of cleverly thinking a bit and I will say that the DI container is such a hat from which you can get almost any essence of your program like a rabbit by the ears. If this entity depends on other entities, and those, in turn, depend on some other entities, the DI container knows what to do with all this dependency graph. If you have a DI container in your project, then the eternal question “how do I get dependency A to entity B” will always have the same answer: “entity B should be taken out of the container, which itself will recursively resolve all its dependencies.”


Solution


There are several fairly popular iOS DI container implementations ( Swinject , Cleanse , Dip , DITranquility , EasyDI ), but it’s boring to use someone else’s implementation. It’s much more fun to use mine.


Ready to have some fun and write a DI container from scratch? A similar implementation was once shown to me by one of the coolest iOS developers, a simple Siberian guy teanet , for which many thanks to him. I rethought it a bit and am ready to share it with you. Let's start with the CDMY0CDMY protocol:


protocol IContainer: AnyObject { func resolve<T: IResolvable>(args: T.Arguments) -> T } 

A habit from a past life - I always write I in front of the protocols. The letter I stands for interface. Our interface protocol has only one CDMY1CDMY method, which takes some CDMY2CDMY arguments from us, and returns an instance of type CDMY3CDMY in return. As you can see, not every entity can be CDMY4CDMY. To become a full CDMY5CDMY, you need to implement CDMY6CDMY. CDMY7CDMY is another protocol that the letter I at the beginning of the name helpfully tells us about. It looks like this:


protocol IResolvable: AnyObject { associatedtype Arguments static var instanceScope: InstanceScope { get } init(container: IContainer, args: Arguments) } 

All rabbits that want to be accessible from the hat are required to implement CDMY8CDMY.


The most important thing here is the initializer, which accepts the container itself and the arguments. It is assumed that each entity, implementing this initializer, will itself get the necessary dependencies directly from the container. And if for this full-fledged existence this rabbit needs some more arguments, then please - they are also in the initializer.


The CDMY9CDMY property is responsible for the scope in which the object instance will exist:


enum InstanceScope { case perRequest case singleton } 

This is a pretty standard thing for DI containers. A value of CDMY10CDMY means that for each call to CDMY11CDMY a new instance of CDMY12CDMY will be created. A value of CDMY13CDMY means that an instance of CDMY14CDMY will be created once - the first time CDMY15CDMY is called. Subsequent calls to CDMY16CDMY in the case of CDMY17CDMY will give a cached copy.


We’ve figured out the protocols, proceed to implementation:


class Container { private var singletons: [ObjectIdentifier: AnyObject]=[:] func makeInstance<T: IResolvable>(args: T.Arguments) -> T { return T(container: self, args: args) } } 

Nothing special here: we will store the singleton cache as a CDMY18CDMY dictionary. CDMY19CDMY will serve as the dictionary key - this is a standard type that supports CDMY20CDMY and is a unique identifier for an object of a reference type (through it, by the way, implemented operator CDMY21CDMY in Swift). The CDMY22CDMY method can create any instances of CDMY23CDMY on the fly due to the fact that we obliged all CDMY24CDMY to implement the same initializer.


In Swift, it is customary to move protocol implementations into a separate extension. We will not stand out and do what is customary - we will show old Lattner that we speak his native language without an accent. We just need to implement one method:


extension Container: IContainer { func resolve<T: IResolvable>(args: T.Arguments) -> T { switch T.instanceScope { case.perRequest: return makeInstance(args: args) case.singleton: let key=ObjectIdentifier(T.self) if let cached=singletons[key], let instance=cached as? T { return instance } else { let instance: T=makeInstance(args: args) singletons[key]=instance return instance } } } } 

Here everything is quite prosaic: if CDMY25CDMY wants to be CDMY26CDMY, we immediately return a new instance. Otherwise, you need to get into the cache. What we find in the cache — we get it, give it away, which we cannot find in the cache — create it, put it in the cache, give it back.


That's actually all. We just wrote our DI container in 50 lines of code. But how to use this thing at all? Yes, it’s very simple.


Use case


For example, consider a textbook story with customers and their orders. Suppose we want to display a list of orders for a specific customer for a specific date. Let's get two entities for our purposes: CDMY27CDMY and CDMY28CDMY - these guys must be accessible from the container, which means they will have to implement CDMY29CDMY.


I am lazy every time when implementing CDMY30CDMY to implement CDMY31CDMY, therefore, before plunging into the fascinating world of application programming, I suggest that you cover yourself with a couple of useful extensions.


Useful extension number of times:


protocol ISingleton: IResolvable where Arguments == Void { } extension ISingleton { static var instanceScope: InstanceScope { return.singleton } } 

And the second one is the same, but different:


protocol IPerRequest: IResolvable { } extension IPerRequest { static var instanceScope: InstanceScope { return.perRequest } } 

Instead of CDMY32CDMY, you can now conform to the more concise CDMY33CDMY and thereby save a few seconds of life by spending them on self-development. And here the CDMY34CDMY implementation has arrived:


class OrdersProvider: ISingleton { required init(container: IContainer, args: Void) { } func loadOrders(for customerId: Int, date: Date) { print("Loading orders for customer '\(customerId)', date '\(date)'") } } 

We provided CDMY35CDMY as required by the protocol, but since CDMY36CDMY does not depend on anything, this initializer is empty. Each time we get CDMY37CDMY from the container, we will get the same instance, because this is the default implementation of CDMY38CDMY for CDMY39CDMY.


And here’s the self-presentation model:


final class OrdersVM: IPerRequest { struct Args { let customerId: Int let date: Date } private let ordersProvider: OrdersProvider private let args: Args required init(container: IContainer, args: Args) { self.ordersProvider=container.resolve() self.args=args } func loadOrders() { ordersProvider.loadOrders(for: args.customerId, date: args.date) } } 

This view model cannot exist without the CDMY40CDMY arguments that we get through CDMY41CDMY. The container itself also gets into this initializer, from which we extract the CDMY42CDMY instance by calling CDMY43CDMY without fuss.


The call to the CDMY44CDMY method uses CDMY45CDMY to load orders, providing it with the necessary arguments. Each time we get CDMY46CDMY from the container, we will get a new instance, because this is the default implementation of CDMY47CDMY for CDMY48CDMY.


The final touch. To get a finished instance of the view model, just take it out of the container, like this:


let container=Container() let viewModel: OrdersVM=container.resolve(args:.init(customerId: 42, date: Date())) viewModel.loadOrders() 

For reference, at the time of writing these lines, the code above produces the following console output:


Loading orders for customer '42', date '2020-04-22 17:41:49 +0000' 

Criticism


To use or not to use the DI container from this article in your project is not for me to decide. As a responsible author, I can only suggest options and talk about the pros and cons equally objectively.


The inquisitive mind of an assiduous reader will notice that the implementation presented above, to be honest, is not very similar to a DI container. This implementation is more like a Service Locator, which, frankly, in a decent society is considered to be no more than antipattern.


I don’t want to delve into the subtleties of the difference between the DI container and the service locator within this article, you can read about it from the same Fowler. But if it’s rude, then when using a DI container, your entity most likely accepts in the constructor initializer a certain set of dependencies that are closed by interfaces protocols. Something like this:


final class OrdersVM { private let ordersProvider: IOrdersProvider init(ordersProvider: IOrdersProvider) { self.ordersProvider=ordersProvider } } 

If you dare to use the Service Locator, then your entity probably gets the dependencies from some dubious place like a static factory. For example, like this:


final class OrdersVM { private let ordersProvider: IOrdersProvider init() { self.ordersProvider=ServiceLocator.shared.resolve() } } 

Programmers do not like the Service Locator primarily because it hides the true dependencies of entities when they are created.


From a practical point of view, this means that there is no way to understand that CDMY49CDMY depends on CDMY50CDMY - for this you need to read the initializer code.In addition, CDMY51CDMY directly depends on CDMY52CDMY, which makes it difficult to reuse this class in systems where a different solution can be chosen for DI.


The second, more important, in my opinion, drawback of the current implementation is that we most monstrously ignore the letter D in the sacred abbreviation SOLID for many programmers. Let me remind you that D in SOLID is the so-called dependency inversion principle , which states that all entities, roughly speaking, should depend from abstractions.


All of our entities, if you look closely, generally not a bit dependent on abstractions. On the contrary, they themselves decide which specific implementation of their dependencies should be used. For example, CDMY53CDMY retrieves a completely specific CDMY54CDMY from the container, and not some CDMY55CDMY protocol.


From a practical point of view, this makes it difficult to replace one implementation of CDMY56CDMY with another implementation of this protocol. Meanwhile, such a substitution can be useful not only in development, but also in the framework of refactoring, as well as when writing unit tests.


Full-fledged DI containers, of course, are devoid of all these disadvantages. Moreover, they offer us a lot of additional features. Deprived of all this, what do you get in return? In return, you get a simple, easy, reliable and predictable implementation as a presidential election that either works correctly or does not compile.


For example, when resolving from a container, you always get a ready-made entity, and not some optional type. When resolving from a container, it is not necessary to handle exceptions, because exceptions are excluded, they never occur.


Adding new dependencies to an existing class looks as concise as possible and comes down to extracting them from the container. If you try to extract something that cannot be extracted from the container, your code will not compile. When resolving with arguments, it is impossible to pass arguments of the wrong type or to forget to pass arguments: the incorrect code will not compile.


And, finally, it’s very difficult to forget to register some entity, because you do not need to register anything in the container. Many of the above for most “industrial” DI containers, unfortunately, are not available.


Here is the meaning of this verbose section in the form of two lists.


In short, the cons


  • We get the dependencies in the constructor directly from the container (Service Locator).
  • It will not be possible to close the dependency with the protocol (principle with the letter D).

In short, the pros


  • Simple and concise implementation (50 lines of code).
  • No need to register dependencies (no need at all).
  • Removing from a container will never break (never at all).
  • Cannot pass invalid arguments (cannot compile).

If the mentioned disadvantages for your project are not critical, and the pluses cause an uncontrolled release of dopamine - welcome aboard, we are most likely on the way.


One More Thing: automatic dependency injection through property wrappers


In 2019, Apple invented to encapsulate a repeating the logic of getters and seters into reusable attributes and called it property wrappers. With the help of such wrappers, your properties can magically get a new behavior: writing a value to CDMY57CDMY or CDMY58CDMY, thread safety, validation, logging - and much more.


You can find a lot of articles on how to make dependency injection using property wrappers, and that’s pretty fun, so we’ll do this trick right now. Watch your hands.


In order to write your property wrapper in the minimum configuration, you need to create a class or structure, provide the CDMY59CDMY property and mark the whole thing with the CDMY60CDMY attribute:


@propertyWrapper struct Resolvable<T: IResolvable> where T.Arguments == Void { private var cache: T? var wrappedValue: T { mutating get { if let cache=cache { return cache } let resolved: T=ContainerHolder.container.resolve() cache=resolved return resolved } } } 

From this straightforward code, we see that our property wrapper is called CDMY61CDMY. It works with all types of CDMY62CDMY, which implement the protocol of the same name and do not require arguments during initialization.


Magic occurs when accessing the CDMY63CDMY property: we return the cached value, if any. If not, we get this value from the container and save it in the cache. For our wrapper to gain access to the container, we had to do a dirty trick - put the container in a static property of the CDMY64CDMY class:


final class ContainerHolder { static var container: IContainer! } 

Having the wrapper CDMY65CDMY in our arsenal, we can apply it to some kind of dependency, for example to CDMY66CDMY:


@Resolvable private var ordersProvider: OrdersProvider 

This will cause the compiler to generate something like this for us:


private var _ordersProvider=Resolvable<OrdersProvider>() var ordersProvider: OrdersProvider { get { return _ordersProvider.wrappedValue } } 

It can be seen that the compiler generated the CDMY67CDMY property, when accessed it actually uses a wrapper that can get the dependency from the container.


Now, the familiar view model can afford not to retrieve CDMY68CDMY from the container in the initializer, but simply mark the corresponding property with the attribute CDMY69CDMY. Like this:


final class OrdersVM: IPerRequest { struct Args { let customerId: Int let date: Date } @Resolvable private var ordersProvider: OrdersProvider private let args: Args required init(container: IContainer, args: Args) { self.args=args } func loadOrders() { ordersProvider.loadOrders(for: args.customerId, date: args.date) } } 

It's time to put everything together and be glad that everything works as before:


ContainerHolder.container=Container() let viewModel: OrdersVM=ContainerHolder.container.resolve( args:.init(customerId: 42, date: Date())) viewModel.loadOrders() 

For reference. This code produces the following console output:


Loading orders for customer '42', date '2020-04-23 18:47:36 +0000' 

ITKarma picture

Unit tests, section under the asterisk


I think everyone will agree that it is difficult to overestimate the importance of automatic tests in modern development. I sincerely hope that you constantly use at least unit tests in your daily work tasks and in home projects. Personally, I do not. Maybe for this reason, the DI container from this article is not well suited for integration with unit tests. However, if you, being in your right mind and solid memory, decided to go the thorny path of automation, I have a couple of options for you.

To warm up a bit, start with the first option, which is simpler. Suppose, for testing needs, we want to lock CDMY70CDMY so that it does not go into the network, but gives test data. First, close it with the protocol:


protocol IOrdersProvider { func loadOrders(for customerId: Int, date: Date) } extension OrdersProvider: IOrdersProvider {} 

Now in the view model we can make a second initializer that will accept this protocol:


final class OrdersVM: IPerRequest { struct Args { let customerId: Int let date: Date } private let ordersProvider: IOrdersProvider private let args: Args required convenience init(container: IContainer, args: Args) { self.init( ordersProvider: container.resolve() as OrdersProvider, args: args) } init(ordersProvider: IOrdersProvider, args: Args) { self.args=args self.ordersProvider=ordersProvider } func loadOrders() { ordersProvider.loadOrders(for: args.customerId, date: args.date) } } 

This approach allows you to create entities in a real application through a container using required init, and in tests use the second initializer and create entities with locked dependencies.


However, the dependency graph can be quite confusing, and most often it’s convenient to continue using the container even in tests, but at the same time learn how to replace some dependencies with mocha ones. This smoothly brings us to the second, more interesting option.


Looking ahead, I’ll say that further from us it will be required to store CDMY71CDMY objects in some collection. However, if we try to do this, we will encounter a harsh reality in the form of an error that is painfully familiar to every iOS developer: protocol 'IResolvable' can only be used as a generic constraint because it has Self or associated type requirements . A typical way to somehow deal with this situation is to pour yourself something stronger and apply a mechanism with the frightening name “type erasure”.


Erasing types assumes that instead of the defective CDMY72CDMY protocol, we will use the most common CDMY73CDMY structure, not burdened with the associated type. Such a structure can be safely stored in an array, but it, of course, lacks the useful functionality of CDMY74CDMY. We compensate for the latter with a closure. Attention to the code:


struct AnyResolvable { private let factory: (IContainer, Any) -> Any? init<T: IResolvable>(resolvable: T.Type) { self.factory={ container, args in guard let args=args as? T.Arguments else { return nil } return T(container: container, args: args) } } func resolve(container: IContainer, args: Any) -> Any? { return factory(container, args) } } 

There’s not much code here, but it’s tricky. In the initializer, we accept a real living type T, which we cannot save anywhere. Instead, we retain a closure trained to create instances of this type. The closure is subsequently used for its intended purpose in the CDMY75CDMY method, which we will need later.


Armed with CDMY76CDMY, we can write a container for unit tests in 20 lines, which will allow us to selectively wet some of the dependencies. Here it is:


final class ContainerMock: Container { private var substitutions: [ObjectIdentifier: AnyResolvable]=[:] public func replace<Type: IResolvable, SubstitutionType: IResolvable>( _ type: Type.Type, with substitution: SubstitutionType.Type) { let key=ObjectIdentifier(type) substitutions[key]=AnyResolvable(resolvable: substitution) } override func makeInstance<T: IResolvable>(args: T.Arguments) -> T { return makeSubstitution(args: args) ?? super.makeInstance(args: args) } private func makeSubstitution<T: IResolvable>(args: T.Arguments) -> T? { let key=ObjectIdentifier(T.self) let substitution=substitutions[key] let instance=substitution?.resolve(container: self, args: args) return instance as? T } } 

Let's figure it out.


The CDMY77CDMY class inherits from the regular CDMY78CDMY, overriding the CDMY79CDMY method used by the container to create entities. The new implementation tries to create a fake dependency instead of the real one. If she does not succeed, she sadly throws up her hands and falls in love with the implementation of the base class.


The CDMY80CDMY method allows you to configure a mocha container by specifying the type of dependency and its corresponding type of moka. This information is stored in the CDMY81CDMY dictionary, which uses the familiar CDMY82CDMY for the key and CDMY83CDMY for storing the type of mok.


The CDMY84CDMY method is used to create mocks, which by key tries to get the necessary CDMY85CDMY from the substitutions dictionary and create the corresponding instance using the CDMY86CDMY method.


We will use this whole thing as follows. Create the CDMY87CDMY mocha by overriding the CDMY88CDMY method:


final class OrdersProviderMock: OrdersProvider { override func loadOrders(for customerId: Int, date: Date) { print("Loading mock orders for customer '\(customerId)', date '\(date)'") } } 

Create a mocha container and configure it. We take out the view model from the container in the usual way. The container creates an instance of the view model, resolving all its dependencies taking into account substitutions:


let container=ContainerMock() container.replace(OrdersProvider.self, with: OrdersProviderMock.self) let viewModel: OrdersVM=container.resolve(args:.init(customerId: 42, date: Date())) viewModel.loadOrders() 

For reference, this code produces the following console output:


Loading mock orders for customer '42', date '2020-04-24 17:47:40 +0000' 

Conclusion


Today, we treacherously abandoned the principle of dependency inversion and once again invented a bicycle by implementing a budget DI using the anti-pattern Service Locator. Along the way, we met a couple of useful iOS development techniques, such as type erasure and property wrappers, and did not forget about unit tests.


The author does not recommend using the code from this article in an application for controlling a nuclear reactor, but if you have a small project and you are not afraid to experiment, swipe to the right, it’s a match & lt; 3




All of the code in this article can ​​download as a Swift Playground.

.

Source