更多内容欢迎关注公众号: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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~