📜 ⬆️ ⬇️

Efficient DI library on Swift in 200 lines of code

The EasyDi library contains a dependency container for Swift. The syntax of this library has been specifically designed for quick mastering and efficient use. It fits in 200 lines, while it can do everything that is necessary for an adult Di library:

- Creating objects and introducing dependencies into existing ones
- Division into containers - Assemblies
- Types of dependency resolution: object graph, singleton, prototype
- Resolution of cyclic dependencies
- Substitution of objects and special tests of dependencies for tests

EasyDi has no register / resolve. Instead, dependencies are described like this:
')
var apiClient: IAPIClient { return define(init: APIClient()) { $0.baseURl = self.baseURL } } 

→ Cocoapods / EasyDi
→ Github / EasyDi

Under the cut there is a very brief description of “Why DI and what it is”, as well as examples of using the library:


Why DI and what is it? (Very briefly)


Inverting dependencies in a project is very important if it contains more than 5 screens and will be supported for more than a year.

Here are three basic scenarios where DI makes life better:


The essence of DI can be described as one sentence:

Dependencies for objects must be closed by protocols and transferred to the object from the outside.

Those. instead:

 class OrderViewController { func didClickShopButton(_ sender: UIButton?) { APIClient.sharedInstance.purchase(...) } } 

Worth using:

 protocol IPurchaseService { func perform(...) } class OrderViewController { var purchaseService: IPurchaseService? func didClickShopButton(_ sender: UIButton?) { self.purchaseService?.perform(...) } } 

You can learn more about dependency inversion and the SOLID concept.
here (objc.io # 15 DI) and here (wikipedia. SOLID) .

How to work with EasyDi (simple example)


A simple example of using the library: remove work from the network into the services from ViewController and place their creation and dependencies in a separate container. This is an easy and effective way to start dividing an application into layers. In the example, consider the service and controller from the example above.

Sample service code and controller
 protocol IPurchaseService { func perform(with objectId: String, then completion: (success: Bool)->Void) } class PurchaseService: IPurchaseService { var baseURL: URL? var apiPath = "/purchase/" var apiClient: IAPIClient? func perform(with objectId: String, then completion: (_ success: Bool) -> Void) { guard let apiClient = self.apiClient, let url = self.baseURL else { fatalError("Trying to do something with uninitialized purchase service") } let purchaseURL = baseURL.appendingPathComponent(self.apiPath).appendingPathComponent(objectId) let urlRequest = URLRequest(url: purchaseURL) self.apiClient.post(urlRequest) { (_, error) in let success: Bool = (error == nil) completion( success ) } } } 

Controller:

 class OrderViewController { var purchaseService: IPurchaseService? var purchaseId: String? func didClickShopButton(_ sender: UIButton?) { guard let purchaseService = self.purchaseService, let purchaseId = self.purchaseId else { fatalError("Trying to do something with uninitialized order view controller") } self.purchaseService.perform(with: self.purchaseId) { (success) in self.presenter(showOrderResult: success) } } } 


Service dependencies:

 class ServiceAssembly: Assembly { var purchaseService: IPurchaseService { return define(init: PurchaseService()) { $0.baseURL = self.apiV1BaseURL $0.apiClient = self.apiClient } } var apiClient: IAPIClient { return define(init: APIClient()) } var apiV1BaseURL: URL { return define(init: URL("http://someapi.com/")!) } } 

And this is how we implement the service in the controller:

 var orderViewAssembly: Assembly { var serviceAssembly: ServiceAssembly = self.context.assembly() func inject(into controller: OrderViewController, purchaseId: String) { define(init: controller) { $0.purchaseService = self.serviceAssembly.purchaseService $0.purchaseId = purchaseId } } } 

Now you can change the class of service without getting into the ViewController.

Types of dependency resolution (Example of average complexity)


ObjectGraph


By default, all dependencies are resolved through the object graph. If the object is already on the stack of the current object graph, then it is used again. This allows you to embed the same object in several, as well as allow circular dependencies. For example, let's take objects A, B and C with links A-> B-> C. (Let's not pay attention to the RetainCycle, it is needed for completeness of the example).

 class A { var b: B? } class B { var c: C? } class C { var a: A? } 

Here is the Assembly and here is the dependency graph for two requests A.

 class ABCAssembly: Assembly { var a:A { return define(init: A()) { $0.b = self.B() } } var b:B { return define(init: B()) { $0.c = self.C() } } var c:C { return define(init: C()) { $0.a = self.A() } } } var a1 = ABCAssembly.instance().a var a2 = ABCAssembly.instance().a 


It turned out two independent graphs.

Singleton


But it happens that you need to create one object, which will then be used everywhere, for example, an analytics system or a repository. You should not use classic Singleton with SharedInstance, since it will be impossible to replace it. For these purposes, EasyDi has scope: singleton. This object is created once, dependencies are injected into it once, and more than EasyDi does not change it, only returns. For example, make B a singleton.

 class ABCAssembly: Assembly { var a:A { return define(init: A()) { $0.b = self.B() } } var b:B { return define(scope: .lazySingleton, init: B()) { $0.c = self.C() } } var c:C { return define(init: C()) { $0.a = self.A() } } } var a1 = ABCAssembly.instance().a var a2 = ABCAssembly.instance().a 



This time we got one graph of objects, since B has become common.

Prototype


And sometimes you need to get a new object with each call. Using the example of ABC objects for an A-prototype, it would look like this:

 class ABCAssembly: Assembly { var a:A { return define(scope: .prototype, init: A()) { $0.b = self.B() } } var b:B { return define(init: B()) { $0.c = self.C() } } var c:C { return define(init: C()) { $0.a = self.A() } } } var a1 = ABCAssembly.instance().a var a2 = ABCAssembly.instance().a 



It turns out that two object graphs give 4 copies of object A

It is important to understand that this is the entry point to the graph and other dependencies do not need to be made prototypes. If you combine the prototypes into a loop, the stack will overflow and the application will crash.

Patches and contexts for tests (Complicated example)


When testing it is important to maintain the independence of tests. In EasyDi, this is provided by the Assemblies contexts. For example, integration tests where singletons are used. They are used like this:

 let context: DIContext = DIContext() let assemblyInstance2 = TestAssembly.instance(from: context) 

It is important to ensure that the contexts of shared Assemblies match.

 class FeedViewAssembly: Assembly { lazy var serviceAssembly:ServiceAssembly = self.context.assembly() } 

Another important part of testing is moki and stubs, that is, objects with known behavior. With known input data, the tested object gives a known result. If it does not, then the test fails. You can learn more about testing here (objc.io # 15 is all) . And this is how you can replace the object:

 protocol ITheObject { var intParameter: Int { get } } class MyAssembly: Assembly { var theObject: ITheObject { return define(init: TheObject()) { $0.intParameter = 10 } } } let myAssembly = MyAssembly.instance() myAssembly.addSubstitution(for: "theObject") { () -> ITheObject in let result = FakeTheObject() result.intParameter = 30 return result } 

Now theObject property will return a new object of a different type with a different intParameter.

about A / B tests

about A / B tests


The same mechanism can be used for a / b testing in the application. For example, like this:

 let FeatureAssembly: Assembly { var feature: IFeature { return define(init: Feature) { ... } } } let FeatureABTestAssembly: Assembly { lazy var featureAssembly: FeatureAssembly = self.context.assembly() var feature: IFeature { return define(init: FeatureV2) { ... } } func activate(firstTest: Bool) { if (firstTest) { self.featureAssembly.addSubstitution(for: "feature") { return self.feature } } else { self.featureAssembly.removeSubstitution(for: "feature") } } } 

Here a separate container is created for the test, which creates the second version of the object and allows you to enable / disable the substitution of this object.

VIPER dependency injection

VIPER dependency injection



It so happens that it is necessary to inject dependencies into an existing object, and someone also depends on it. The most obvious example is VIPER, when you need to add a Presenter to the ViewController, and he must get a link to the ViewController.

To do this, EasyDi has 'keys' and placeholders with which you can return the same object from different methods. It looks like this:

 lass ModuleAssembly: Assembly { func inject(into view: ModuleViewController) { return define(key: "view", init: view) { $0.presenter = self.presenter } } var view: IModuleViewController { return definePlaceholder() } var presenter: IModulePresenter { return define(init: ModulePresenter()) { $0.view = self.view $0.interactor = self.interactor } } var interaction: IModuleInteractor { return define(init: ModuleInteractor()) { $0.presenter = self.presenter ... } } } 

Here, injecting dependencies into the ViewController uses the inject method, which is keyed with the viewController property. Now this property returns the object passed to the inject method. And only when resolving dependencies of the object graph, which begins with the inject method.

Instead of conclusion


I didn’t have a goal to pack everything in 200 lines, it just happened. Typhoon had the most impact on this library, I really wanted to have something similar, but Swift was even simpler.

The longest was formed syntax, such as to write a minimum of code and with a minimum of space for the flight of thought. This is especially important when working in a team.

The library is packed into 1 file to make it easier to add to transitional projects where use_frameworks is not used yet, but Swift is already there.

Links to the library:


Current version is '1.1.1'
pod 'EasyDi', '~>1.1'

It should work equally well on Swift 3/4 , in iOS 8+ .
On iOS 7 - I do not know, I can not check.

And the depot app is the XKCD comic reader.

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


All Articles