Dependency Injection In Swift
I will talk about the way I use DI in my daily work. Before I continue: I should emphasize if I screw something up it’s my fault, not my team.
Summary
1. Why dependency injection
1.1. Reduce software complexity2. Dependency injection
2.1. Type of injection
2.2. The way i use Dependency Injection
2.3. Reduce compilation time3. TL;DR
Why Dependency injection?
One of the most important techniques for reducing software complexity is to design the systems so that developers only need to face a small fraction of the overall complexity, aka, Modular design.
In modular design, a software system is decomposed into a collection of modules that are relatively independent. From the system’s vision, these modules work together by calling each other’s functions or methods. That will be the dependencies between the modules. You will get yourself killed if you don’t do well with the dependencies. For example, The arguments for a method create a dependency between the method and any code that invokes the method. If the arguments change, all invocations of the method must be modified to conform to the new signature. The interaction between modules is called Coupling.
In software engineering, coupling is the degree of interdependence between software modules; a measure of how closely connected two routines or modules are;[1] the strength of the relationships between modules.[2]
Coupling is usually contrasted with cohesion. Low coupling often correlates with high cohesion, and vice versa. Low coupling is often a sign of a well-structured computer system and a good design, and when combined with high cohesion, supports the general goals of high readability and maintainability. --- From wiki
Great modular design which means different components of modules can be replaced with alternative implementation without affecting other components. This required developers to use proper abstractions(An abstraction is a simplified view of an entity, which omits unimportant detail and make it easier for us to think about and manipulate complex things) to make modules become loose coupling. The main goal of Dependency Injection is loose coupling.
Conclusion:
Dependency Injection makes modules become loose coupling which contributes to reducing the overall software complexity.
Dependency injection
“Dependency injection is really just passing in an instance variable.” - James Shore
Type Of Injection
- Constructor Injection
- Property Injection
- Method Injection
- Ambient context
Constructor Injection
Pros:
- Make dependencies explicit
- Information hiding
Cons:
- Boilerplate code in the object initializer
- Maybe too much passthrough parameters
Providing default value in the class method is always a handy feature in swift, but be careful with the case UserSession is not the instance you want. Especially, UserSession has some preconditions.
Property Injection
Pros:
- Clean initializer interface, Separate initialization from injection
- “Easy on boarding”
Cons:
- Information leakage, exposing internal property, incomplete initialization
- Cluttering code with unwrapping optionals
- Can not define property as immutable
- Hard to define default value
Method Injection
With method injection dependency is passed as a parameter to ViewController’s login method.
Pros:
- Free to compose injected method
- No need to keep reference to injected method
Cons:
- Information leakage, exposing internal logic to the class that use it
Ambient context
An ambient context is implemented using a static method or static property. The common usage on iOS will be the Singleton which makes your API look very simple and “No dependencies”. As for me, I only use singleton on logger that doesn’t affect the execution of the code, and doe’s have any global state.
Pros:
- Keep your API clean and simple
Cons:
- Global state
- Hiding dependencies
- Make your unit test harder
The way i use Dependency Injection
I use constructor injection as my preferred way to do Dependency Injection. The diagram below is the main concept I use in my own app. If you have any suggestions, please let me know.
AppContext holds the dependencies when an application running, in case of massive AppContext, you can separate the AppContext into sub-context and composed them into AppContext.
Coordinator handles ViewController’s navigation, logic flow(A/B test), and even user data modification. It contains all the dependencies that using within the ViewControllers and passing them to object initializer.
ViewControllers will be placed at the same level. This point is simple but very important until you find you have lots of passthrough parameters.
In the example, AppContext depends on API implemented by SQLite and Alamofire that my code tightly coupled with specific implementation. In object-oriented design, this kind of problem can be solved with Dependency inversion principle. Following the DIP instructions to have loosely coupled code.
High-level code should not depend on low-level code, they both should depend on abstractions and abstractions should not depend on details.
Now, take the AppContext as high-level code, SQLite as low-level code. They both depend on the same abstraction, Store. (Protocol is the way I use to model abstraction, but it doesn’t mean Protocol is an abstraction, good abstraction is critical. About abstraction, maybe next article.) DIP enhances replaceability, you can replace Store implementation with Leveldb without modified the high-level code. DIP also contributes to compilation time. Let’s see how this happened.
You can download the Demo to see what’s happened. Press CMB + B and then CMB + 9(Report Navigator), make sure to choose the Recent Tab. The compilation time is 5.9 seconds after every Clean Build(refers to cleaning the entire cache)
Build again, the compilation time is only: 0.2 seconds because using build description from memory.
The UML Class diagram below is the architecture of the demo.
Take the Feature1 for example: Add new struct on Feature1.swift, press CMD + B, CMD + 9
Feature2 and App were recompiled(Library needs to be recompiled to conform the ABI changes) and the build time increased up to 1.1 seconds. While this not much of a problem for the demo app, big ones take massive performance hits with this approach, easily reaching build times of over twenty minutes. The worst case is that modify the UserSession causes the entire app recompiled.
Reduce Compilation Time
To achieve the best possible build times, we should make the dependency graph as horizontal as possible. Because of that, I add three components: FeatureService, User, and DIContainer.
FeatureService contains the services provided by Feature1, Feature2, Feature3.
User contains the services provided by UserSession or anything related to the user.
DIContainer registers each service with the corresponding implementation. Feature1 for Feature1Service, Feature3 for Feature3Service. After registration, with the container, you can get the service everywhere as you wish.
Take Feature2 for example. The dependencies of Feature2 is Feature2. Dependencies that contains Feature1Service and Feature3Service implementation.
To get an instance of Feature2, just call the Feature2 init method and passing the dependencies created by AnyInitializer with the Feature2 dependencies description and the container.
Modify Feature1 again(e.g. comment out the PrivateModel), CMD+B, CMD+9, we can see only the App and Feature1 get recompiled, no Feature2. I assume that your app’s build performance will get massive performance with this approach.
Thx for reading, I’ve simplified the example for the sake of a conceptual demo in order to simply understand things, and get some insight on how to use technologies like DIContainer, Type Erasure in Swift, etc. The Demo code is here. Any suggestions will be helpful.
TL; DR
Ref:
Reduce build time using interface target
https://stackoverflow.com/questions/2171177/what-is-an-application-binary-interface-abi