Writing Better Swift Code: DI (Dependency Injection)

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

This article will focus on several questions to learn this pattern:

  • What is a dependency?
  • What is the Dependency Inversion Principle?
  • What is Dependency Injection?
  • When to use dependency injection?
  • Several common ways of dependency injection?
  • The role of dependency injection

What is a dependency?

Relying on others or things and not being able to be self-reliant or self-sufficient is called dependence

Dependency is a common relationship in programs. For example, an instance of a class Vehicleis used in a CarEngineclass . The engineusual practice is Vehicleto explicitly create CarEnginean instance of the class in the class and assign it to it engine. Such as the following code:

// 赛车引擎
class RaceCarEngine {
    func move() {
        print("CarEngine 开动")
    }
}

// 车
class Vehicle {
    var engine: RaceCarEngine

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

let car = Vehicle()
car.forward()
复制代码

We will use CarEngineas Vehiclethe property of the method we call when carthe method is called .forwardenginemove

There is a problem :

  1. engineIt should not be a concrete class. If we want to switch to other engines, then we must modify Vehicleand enginereplace other classes, which does not conform to the principle of dependency inversion—depending on abstraction, not on concrete implementation.
  2. The class Vehicleassumes redundant responsibilities and is responsible for engineobject creation, which must be coupled.
  3. Extensibility, if we want to modify engineit to a rocket engine, then we must modify Vehiclethis class, which obviously does not conform to the open-closed principle.
  4. Not convenient for unit testing. If you want to test the effect of different enginepairs Vehicleit is difficult because enginethe initialization Vehicleof the

What is the Dependency Inversion Principle (DIP)?

Dependency Inversion Principle, English abbreviation DIP, full name 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。

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象

所以 Vehicle 不能直接依赖 RaceCarEngine,我们需要给引擎定义一个规则,抽象成一个协议:

protocol Propulsion {
    func move()
}

class RaceCarEngine: Propulsion {
    func move() {
        print("CarEngine 开动")
    }
}

// 车
class Vehicle {
    var engine: Propulsion

    init() {
        engine = RaceCarEngine()
    }
   
    func forward() {
        engine.move()
    }
}
复制代码

但是这就符合 DIP 了么?答案是没有,为什么?

因为在 init() 方法中,用 RaceCarEngine 具体类去初始化 engine,这也是一种依赖。这就造成,很难在没有 RaceCarEngine 类的情况下使用 Vehicle 类。

那么怎样才能解决这个问题?依赖注入闪亮登场。

什么是依赖注入?

如果模块A调用了模块B的方法,那么就认为模块A依赖于模块B,模块A与模块B发生了耦合。在软件工程中,设计的核心思想:尽可能减少代码耦合,采取解耦技术把关联依赖降到最低,而不至于牵一发而动全身

Vehice 类中如何通过依赖注入来改进代码?代码如下:

class Vehicle {
    var engine: Propulsion

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

    func forward() {
        engine.move()
    }
}
复制代码

我们现在没有直接在 Vechicle 的 init() 函数中用 RaceCarEngine 去初始化 engine,而是通过给init添加一个Propulsion类型的engine形参,用这个形参去初始化 engine。

虽然这改动非常小,但是效果是非常显著的,因为Vehicle 再也不需要和RaceCarEngine类直接产生关系。

然后我们的调用代码:

let raceCarEngine = RaceCarEngine()
var car = Vehicle(engine: raceCarEngine)
car.forward()
复制代码

raceCarEngine 对象是从外部注入到 Vehicle 对象中。这就是依赖注入。这两个类仍然相互依赖,但它们不在紧密耦合——可以使用其中一个而不需要另一个。

Dependency injection means giving an object its instance variables.(依赖注入就是将实例变量传入到一个对象中去)

通过依赖注入,显然提高了代码的可扩展性。我们可以轻松地将RaceCarEngine引擎换成RocketEngine引擎:

class RocketEngine: Propulsion {
    func move() {
        print("3-2-1... RocketEngine 发动")
    }
}

let rocket = RocketEngine()
var car = Vehicle(engine: rocket)
car.forward()
复制代码

什么时候用到依赖注入?

依赖注入在以下场景中很有用:

  • 更改您无权访问的代码的实现
  • 在开发过程中“模拟”或伪造代码中的行为
  • 对代码进行单元测试

依赖注入的方法

  • 构造函数注入:通过初始化init()提供依赖

    let rocket = RocketEngine()
    var car = Vehicle(engine: rocket)
    复制代码
  • 属性注入:通过属性(或 setter)提供依赖,iOS 框架中有很多属性注入模式,Delegate 模式通常是这样实现的。

    let rocket = RocketEngine()
    var car = Vehicle()
    car.engine = rocket
    复制代码
  • 方法注入,将依赖项作为方法参数传递

    let rocket = RocketEngine()
    car.setEngine(rocket)
    复制代码

实战

让我们看一个使用Repository对象获取数据的Service类的示例:


struct Article: Equatable {
    let title: String
}

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


protocol Repository {
    func getAll() -> [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)
    }
}
复制代码

我们通过给 Service 注入注入了一个 repository,这样 service 就不需要知道所使用的文章是如何提供的。这些文章可能来自从本地JSON文件读取,或从本地数据库检索,又或者是从服务器通过请求获取。我们可以注入mocked的repository,通过使用mocked的数据使得测试更具可预测性。


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

class ServiceTests: XCTestCase {
    
    func testAddArticles() {
        let expectedArticle = Article(title: "测试文章")
        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)
    }
}
复制代码

我们首先创建了一个模拟的expectedArticle对象,然后注入到的 MockRepository对象中,通过2个XCTAssertEqual以检查我们的Sercice是否按预期工作。

构造函数依赖注入确实是一个不错的注入方式,但是也有些不便问题:

class BasketViewController: UIViewController {
    private let service: Service
    
    init(service: Service) {
        self.service = service
    }
}
复制代码

写了新的构造函数,我们需要额外的做些处理。

但是如何在不重写默认构造函数的情况下使用依赖注入呢?

我们可以通过属性注入的方式:

class BasketViewController: UIViewController {
    var service: Service!
}

class DataBaseRepository: Repository {
    override func getAll() -> [Article] {
        // TODO:从数据库中查找数据
        return [Article(title: "测试数据")]
    }
}

let basketViewController = BasketViewController()
let repository = DataBaseRepository()       
let service = Service(repository: repository)
basketViewController.service = Service()
复制代码

基于属性注入的方式也有不完美的地方:属性的访问权限被放大,不能将他们定义为私有了。

不管是属性注入,还是构造函数注入,都包含了2个工作:

  • 创建 ServiceBasketViewController 的实例
  • 完成 ServiceBasketViewController 的依赖关系

因此这里又出现一个潜在的问题,就是当要更换Service的时候,又需要去更改这些创建实例的代码。如果有多处地方跳转到 BasketViewController,那么这类代码就得多处修改。因此可以将这两个工作移交给一个独立组件去完成,它的职责就是完成对象的创建以及对象之间的依赖关系的维护和管理。很多人想到这个组件可以用工厂模式进行设计,这是可取的,但是本文将封装成类似SwiftUI中的 @Environment 的设计。

我们的设计目标就是:

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()
    @Injected(\.service) var service: BasketService
    
    func loadArticles() {
        service.addArticles(to: basket)
        
        print(basket.articles)
    }
}

let vc = BasketViewController()
vc.loadArticles()
复制代码

最终完整代码:

struct Article: Equatable {
    let title: String
}

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

protocol Repository {
    func getAll() -> [Article]
}

class DataBaseRepository: Repository {
    override func getAll() -> [Article] {
        // TODO:从数据库中查找数据
        return [Article(title: "测试数据")]
    }
}

public protocol InjectionKey {
    associatedtype Value
    static var currentValue: Self.Value {get set}
}

/// 提供获取依赖
struct InjectedValues {
    private static var current = InjectedValues()
    
    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()
    @Injected(\.service) var service: BasketService
    
    func loadArticles() {
        service.addArticles(to: basket)
        
        print(basket.articles)
    }
}

let vc = BasketViewController()
vc.loadArticles()
复制代码

结果输出:

[__lldb_expr_388.Article(title: "测试数据")]
复制代码

参阅

Guess you like

Origin juejin.im/post/7007963930706313246