Dependency injection is an important design pattern that is widely used.

This article will study this pattern around a few questions:

  • What is dependency?
  • What is the dependency inversion principle?
  • What is dependency injection?
  • When is dependency injection used?
  • What are the common ways of dependency injection?
  • The role of dependency injection

What is dependency?

Dependence on others or things without being independent or self-sufficient is called dependence

Dependency is a common relationship in the program. For example, the CarEngine class engine is used in the Vehicle class. The common approach is to explicitly create an instance of the CarEngine class in the Vehicle class and assign the value to engine. Such as the following code:

// Race engine
class RaceCarEngine {
    func move(a) {
        print("The CarEngine start")}}/ / car
class Vehicle {
    var engine: RaceCarEngine

    init(a) {
        engine = RaceCarEngine()}func forward(a) {
        engine.move()
    }
}

let car = Vehicle()
car.forward()
Copy the code

We use CarEngine as a Vehicle attribute, and when car calls forward, we call Engine’s Move method.

Existing problems:

  1. engineIt should not be a concrete class, which we would have to change if we wanted to switch to another engineVehicle.engineReplacing other classes does not comply with the dependency inversion principle — rely on abstractions, not concrete implementations.
  2. classVehicleTake on extra responsibilities, take chargeengineObject creation, there must be coupling.
  3. Scalability, let’s say we want to changeengineFor the rocket engine, then we have to modifyVehicleThis class clearly does not comply with the open close principle.
  4. Unit testing is not convenient. If you want to test differentenginerightVehicleThe impact is difficult becauseengineInitialization is written dead inVehicleConstructor of the

What is dependency inversion (DIP)?

Dependence Inversion Principle, abbreviated as DIP, Dependence Inversion Principle.

High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should Not depend upon details. Details should depend upon Abstractions.

A high-level module should not depend on a low-level module; both should depend on its abstraction; Abstractions should not depend on details, details should depend on abstractions.

Therefore, Vehicle cannot rely on RaceCarEngine directly. We need to define a rule for the engine, abstracting it into a protocol:

protocol Propulsion {
    func move(a)
}

class RaceCarEngine: Propulsion {
    func move(a) {
        print("The CarEngine start")}}/ / car
class Vehicle {
    var engine: Propulsion

    init(a) {
        engine = RaceCarEngine()}func forward(a) {
        engine.move()
    }
}
Copy the code

But is that consistent with DIP? The answer is no. Why?

Because the RaceCarEngine concrete class is used to initialize engine in the init() method, this is also a dependency. This makes it difficult to use the Vehicle class without the RaceCarEngine class.

So what can be done to solve this problem? Dependency injection makes its debut.

What is dependency injection?

If module A calls module B’s methods, module A is considered dependent on module B, and module A is coupled with module B. In software engineering, the core idea of design is to reduce code coupling as much as possible, and to adopt decoupling technology to minimize associated dependence, rather than affecting the whole body.

How can we improve code through dependency injection in the Vehice class? The code is as follows:

class Vehicle {
    var engine: Propulsion

    init(engine: Propulsion) {
        self.engine = engine
    }

    func forward(a) {
        engine.move()
    }
}
Copy the code

Instead of using RaceCarEngine to initialize engine directly in Vechicle’s init() function, we use this parameter to initialize engine by adding a Propulsion parameter to init.

This change is very small, but the effect is significant because Vehicle no longer needs to have a direct relationship with the RaceCarEngine class.

Then our calling code:

let raceCarEngine = RaceCarEngine(a)var car = Vehicle(engine: raceCarEngine)
car.forward()
Copy the code

The raceCarEngine object is injected externally into the Vehicle object. This is dependency injection. The two classes still depend on each other, but they are not tightly coupled — one can be used without the other.

Dependency injection means giving an object its instance variables.

With dependency injection, the extensibility of the code is obviously improved. We can easily replace the RaceCarEngine engine with the RocketEngine engine:

class RocketEngine: Propulsion {
    func move(a) {
        print("The 3-2-1... RocketEngine")}}let rocket = RocketEngine(a)var car = Vehicle(engine: rocket)
car.forward()
Copy the code

When is dependency injection used?

Dependency injection is useful in the following scenarios:

  • Change the implementation of code that you do not have access to
  • To “simulate” or forge behavior in code during development
  • Unit test your code

Dependency injection methods

  • Constructor injection: Provide dependencies by initializing init()

    let rocket = RocketEngine(a)var car = Vehicle(engine: rocket)
    Copy the code
  • Property injection: Provide dependencies through properties (or setters). There are many property injection modes in the iOS framework, and the Delegate mode is usually implemented this way.

    let rocket = RocketEngine(a)var car = Vehicle()
    car.engine = rocket
    Copy the code
  • Method injection, passing dependencies as method parameters

    let rocket = RocketEngine()
    car.setEngine(rocket)
    Copy the code

In actual combat

Let’s look at an example of a Service class that uses a Repository object to get data:


struct Article: Equatable {
    let title: String
}

class Basket {
    var articles = [Article]()
}


protocol Repository {
    func getAll(a)- > [Article]}class Service {
    private let repository: Repository

    init(repository: Repository) {
        self.repository = repository
    }

    func addArticles(to basket: Basket) {
        let allArticles = repository.getAll()
        basket.articles.append(contentsOf: allArticles)
    }
}
Copy the code

We inject a repository with Service injection so that the Service does not need to know how the articles used are provided. These articles may be read from a local JSON file, retrieved from a local database, or retrieved from the server on a request. We can inject mocked’s Repository to make testing more predictable by using data from mocked.


class MockRepository: Repository {
    var articles: [Article]
    
    init(articles: [Article]) {
        self.articles = articles
    }
    
    override func getAll(a)- > [Article] {
        return articles
    }
}

class ServiceTests: XCTestCase {
    
    func testAddArticles(a) {
        let expectedArticle = Article(title: "Test article")
        let mockRepository = MockRepository(articles: [expectedArticle])
        
        let service = Service(repository: mockRepository)
        let basket = Basket()
        
        service.addArticles(to: basket)
        
        XCTAssertEqual(basket.articles.count, 1)
        XCTAssertEqual(basket.articles[0], expectedArticle)
    }
}
Copy the code

We first created a mock expectedArticle object and then injected it into the MockRepository object, passing 2 XCTAssertEqual to check that our Sercice was working as expected.

Constructor dependency injection is a good way to inject, but there are some inconveniences:

class BasketViewController: UIViewController {
    private let service: Service
    
    init(service: Service) {
        self.service = service
    }
}
Copy the code

With the new constructor, we need to do some extra processing.

But how do you use dependency injection without overriding the default constructor?

We can use property injection:

class BasketViewController: UIViewController {
    var service: Service!
}

class DataBaseRepository: Repository {
    override func getAll(a)- > [Article] {
        // TODO: find data from the database
        return [Article(title: "Test data")]}}let basketViewController = BasketViewController(a)let repository = DataBaseRepository(a)let service = Service(repository: repository)
basketViewController.service = Service(a)Copy the code

The property-injection-based approach also has its drawbacks: access to properties is magnified and they cannot be defined as private.

Both property injection and constructor injection involve two tasks:

  • createServiceBasketViewControllerThe sample of
  • completeServiceBasketViewControllerDependency relation of

Another potential problem is that when a Service needs to be replaced, the code needs to be changed. If there are multiple jumps to BasketViewController, then this kind of code needs to be changed a lot. These two tasks can therefore be handed over to a separate component whose responsibility is to create objects and maintain and manage the dependencies between objects. Many people think that this component could be designed in factory mode, which is desirable, but this article encapsulates a design similar to @Environment in SwiftUI.

Our design objectives are:

class BasketService {
    @Injected(\.repository) var repository: Repository

    func addArticles(to basket: Basket) {
        let allArticles = repository.getAll()
        basket.articles.append(contentsOf: allArticles)
    }
}

class BasketViewController: UIViewController {
    private var basket = Basket(a)@Injected(\.service) var service: BasketService
    
    func loadArticles(a) {
        service.addArticles(to: basket)
        
        print(basket.articles)
    }
}

let vc = BasketViewController()
vc.loadArticles()
Copy the code

Final complete code:

struct Article: Equatable {
    let title: String
}

class Basket {
    var articles = [Article]()
}

protocol Repository {
    func getAll(a)- > [Article]}class DataBaseRepository: Repository {
    override func getAll(a)- > [Article] {
        // TODO: find data from the database
        return [Article(title: "Test data")]}}public protocol InjectionKey {
    associatedtype Value
    static var currentValue: Self.Value {get set}}/// provide access dependencies
struct InjectedValues {
    private static var current = InjectedValues(a)static subscript<K> (key: K.Type) -> K.Value where K : InjectionKey {
        get { key.currentValue }
        set { key.currentValue = newValue }
    }
    
    static subscript<T> (_ keyPath: WritableKeyPath<InjectedValues.T>) -> T {
        get { current[keyPath: keyPath] }
        set { current[keyPath: keyPath] = newValue }
    }
}

@propertyWrapper
struct Injected<T> {
    private let keyPath: WritableKeyPath<InjectedValues.T>
    var wrappedValue: T {
        get { InjectedValues[keyPath] }
        set { InjectedValues[keyPath] = newValue }
    }
    
    init(_ keyPath: WritableKeyPath<InjectedValues.T>) {
        self.keyPath = keyPath
    }
}

private struct RepositoryKey: InjectionKey {
    static var currentValue: Repository = DataBaseRepository()}private struct ServiceKey: InjectionKey {
    static var currentValue: BasketService = BasketService()}extension InjectedValues {
    var repository: Repository {
        get {Self[RepositoryKey.self]}
        set {Self[RepositoryKey.self] = newValue}
    }
    
    var service: BasketService {
        get { Self[ServiceKey.self]}set {Self[ServiceKey.self] = newValue}
    }
}

class BasketService {
    @Injected(\.repository) var repository: Repository

    func addArticles(to basket: Basket) {
        let allArticles = repository.getAll()
        basket.articles.append(contentsOf: allArticles)
    }
}


class BasketViewController: UIViewController {
    private var basket = Basket(a)@Injected(\.service) var service: BasketService
    
    func loadArticles(a) {
        service.addArticles(to: basket)
        
        print(basket.articles)
    }
}

let vc = BasketViewController()
vc.loadArticles()
Copy the code

Result output:

[__lldb_exPR_388.Article(title: "Test data ")]Copy the code

Refer to the

  • Inversion of Control Containers and the Dependency Injection pattern