[Swift设计模式] 依赖注入

341 阅读5分钟
更多内容欢迎关注公众号:Swift花园

依赖注入(以下简称 DI )指的是为一个对象添加实例变量。

以我之见,完整的故事会稍微复杂一点。但是如果你把问题拆解到根本,你会发现实现 DI 很简单,就如同给对象添加实例变量一样。没有开玩笑,很多开发者其实把 DI 用错了地方。 💉

学习 DI 无关乎实现细节,只关乎你如何运用这个模式。DI 有四种小变体,让我们用例子逐一介绍,以便你能掌握何时该使用 DI 。💻

依赖注入基础

如我之前提过的,DI 其实是一个简单概念的高端表达。如果你想要开始使用 DI ,其实不需要任何外部的库或者框架。想象一下,你现在有两个独立的对象,对象A想要使用对象B。

如果你将对象B硬编码进A,事情就不好办了,因为从A的视角来看,这样一来A离了B就会无法工作。把这种耦合关系放大到100个对象的级别,如果你无所作为,最后事情就会变成一锅粥。🍝

因此,DI 的主要目标是:尽可能创建互相独立的对象,或者说尽可能松散地组织代码,以便提高代码的可复用性和可测试性。把关注点分离和解耦声明为DI的目标在这里也是合适的,因为多少情况下你就是需要把各种逻辑功能分隔到独立的对象中。 🤐

理论上A和B各自都应该只做一件事情,两者之间的通过一个协议来实现依赖,而非硬编码特定的实例。利用DI,我们可以增强代码的质量,因为依赖代码本身可以随意替换而不影响其他对象的实现。对于模拟,测试和重用,这样做都有益处。😎

Swift中如何实现DI

Swift是门令人惊叹的语言,对于协议和面向对象准则都有着出色的支持,并且它还有着令人满意的函数功能。DI在Swift中的实现方式有好几种,在这篇教程中我聚焦在一些基本的方式,它们都不需要用到外部的依赖注入。😂

好的,让我们从一个协议开始。之所以要从协议开始,是因为我将用到一个叫 Encoder 的协议,Swift 并未将其对外开放,我们这里只是用它来演示。

protocol Encoder {
    func encode<T>(_ value: T) throws -> Data where T: Encodable
}
extension JSONEncoder: Encoder { }
extension PropertyListEncoder: Encoder { }

Property 列表和 JSON 编码器已经实现这个协议的方法。我们只需要扩展我们的对象,令扩展的部分遵循这个协议。

构造器注入

DI的最常见形式是构造器注入或者基于构造器的注入,其理念是,你通过构造器传入依赖,然后把依赖存在一个内部私有的只读属性变量中。这种方式的好处是,你的对象与依赖相关的工作逻辑总是能正常工作,因为当它被创建的时候,依赖也被创建了。 🔨

class Post: Encodable {
    
    var title: String
    var content: String

    private var encoder: Encoder
    
    private enum CodingKeys: String, CodingKey {
        case title
        case content
    }

    init(title: String, content: String, encoder: Encoder) {
        self.title = title
        self.content = content
        self.encoder = encoder
    }
    
    func encoded() throws -> Data {
        return try self.encoder.encode(self)
    }
}

let post = Post(title: "Hello DI!", content: "构造器注入", encoder: JSONEncoder())

if let data = try? post.encoded(), let encoded = String(data: data, encoding: .utf8) {
    print(encoded)
}

你也可以为构造器中的 encoder 提供一个默认值,但请千万小心"私生子注入“这种反面模式。这种模式在这里特指如果默认自来自另外一个模块,你的代码将不得不同那个模块耦合。三思而后行!🤔

属性注入

有的时候构造器注入不容易实现,因为你的类需要继承自系统类,如果这个类还要跟视图或者控制器一起工作,注入将变得十分困难。这种情况下更好的解决方案是使用基于属性的注入模式。也许你无法完全掌控初始化过程,但你总是可以控制属性。这种模式唯一的缺点是,每次使用之前,你需要检查目标属性是否存在。🤫

class Post: Encodable {

    var title: String
    var content: String

    var encoder: Encoder?

    private enum CodingKeys: String, CodingKey {
        case title
        case content
    }

    init(title: String, content: String) {
        self.title = title
        self.content = content
    }

    func encoded() throws -> Data {
        guard let encoder = self.encoder else {
            fatalError("Encoding is only supported with a valid encoder object.")
        }
        return try encoder.encode(self)
    }
}

let post = Post(title: "Hello DI!", content: "属性注入")
post.encoder = JSONEncoder()

if let data = try? post.encoded(), let encoded = String(data: data, encoding: .utf8) {
    print(encoded)
}

iOS框架中大量应用了属性注入模式。委托代理模式也经常通过这种方式实现。✈️

方法注入

如果只需要用到某个依赖一次,你并不需要将它存储为对象的变量。与其用构造器参数或者对外暴露的可变属性,不如将依赖作为方法参数传入。这种方法被称为方法注入或者说基于参数的注入。👍

class Post: Encodable {

    var title: String
    var content: String

    init(title: String, content: String) {
        self.title = title
        self.content = content
    }

    func encode(using encoder: Encoder) throws -> Data {
        return try encoder.encode(self)
    }
}

let post = Post(title: "Hello DI!", content: "方法注入")

if let data = try? post.encode(using: JSONEncoder()), let encoded = String(data: data, encoding: .utf8) {
    print(encoded)
}

每当你的方法被调用时,你的注入都可以改变。由于不持有依赖的引用,这种依赖只能被用于本地方法内部。

即时上下文

我们的最后一种模式有一点危险,它只应当用于多个对象实例间共享的通用依赖,比如说日志、分析或者缓存等。🚧

class Post: Encodable {
    
    var title: String
    var content: String
    
    init(title: String, content: String) {
        self.title = title
        self.content = content
    }
    
    func encoded() throws -> Data {
        return try Post.encoder.encode(self)
    }
    
    
    private static var _encoder: Encoder = PropertyListEncoder()
    
    static func setEncoder(_ encoder: Encoder) {
        self._encoder = encoder
    }
    
    static var encoder: Encoder {
        return Post._encoder
    }
}

let post = Post(title: "Hello DI!", content: "即时上下文")
Post.setEncoder(JSONEncoder())

if let data = try? post.encoded(), let encoded = String(data: data, encoding: .utf8) {
    print(encoded)
}

即时上下文有一些缺陷,它可能很好地解决了某些横跨多个关注点的情形,但同时也隐式地创造了可以全局修改的依赖状态。因此,情况允许的话你应该优先考虑其他的依赖注入方式。

我的公众号
这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~