在 Swift 中使用工厂进行依赖注入

当涉及到使代码更加可测试时,依赖注入是一个重要工具。与其让对象创建自己的依赖关系或作为单例访问它们,不如让对象在工作中需要的一切都从外部传入。这使我们更容易看到一个给定的对象有哪些确切的依赖关系,同时也使测试变得更加简单——因为可以模拟依赖项以捕获和验证状态和值。

然而,尽管它很有用,但如果在一个项目中广泛使用,依赖注入也会成为一个相当大的痛点。随着一个给定对象的依赖数量的增加,初始化它可能成为一个相当麻烦的事情。让代码可测试是件好事,但如果要以这样的初始化器为代价,那就太糟糕了:

class UserManager {
    init(dataLoader: DataLoader, database: Database, cache: Cache,
         keychain: Keychain, tokenManager: TokenManager) {
        ...
    }
}

本周,让我们来看看一种依赖注入技术,它可以让我们实现可测试性,而不强迫我们写这种大规模的初始化器或复杂的依赖管理代码。

传递依赖关系

在使用依赖注入时,我们经常会出现上述情况,主要原因是我们需要传递依赖关系,以便以后使用它们。例如,假设我们正在构建一个消息应用程序,我们有一个视图控制器来显示用户的所有消息:

class MessageListViewController: UITableViewController {
    private let loader: MessageLoader

    init(loader: MessageLoader) {
        self.loader = loader
        super.init(nibName: nil, bundle: nil)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        loader.load { [weak self] messages in
            self?.reloadTableView(with: messages)
        }
    }
}

正如你所看到的,我们将一个MessageLoader注入到MessageListViewController中,然后用它来加载数据。这还不算太糟,因为我们只有一个依赖关系。然而,我们的列表视图很可能不是只有一层,这在某种程度上需要我们实现导航到另一个视图控制器。

假设我们想让用户在点击消息列表中的某个单元格时,能够导航到一个新的视图。对于这个新的视图,我们创建了一个MessageViewController,它既可以让用户查看消息的全文,也可以对其进行回复。为了启用回复功能,我们实现了一个MessageSender类,在创建新的视图控制器时,我们将其注入到新的视图控制器中,像这样:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let message = messages[indexPath.row]
    let viewController = MessageViewController(message: message, sender: sender)
    navigationController?.pushViewController(viewController, animated: true)
}

问题来了。由于MessageViewController需要一个MessageSender的实例,我们也需要让MessageListViewController知道这个类。一个选择是简单地将发送者也添加到列表视图控制器的初始化器中:

class MessageListViewController: UITableViewController {
    init(loader: MessageLoader, sender: MessageSender) {
        ...
    }
}

虽然上面的方法可行,但它开始把我们引向另一个庞大的初始化器,并使MessageListViewController变得更难使用(也相当令人困惑,为什么列表首先需要知道发件人?🤔).

另一个可能的解决方案(在这种情况下很常见)是让MessageSender成为一个单例。这样我们就可以很容易地从任何地方访问它,并通过简单地使用它的共享实例将其注入MessageViewController中:

let viewController = MessageViewController(
    message: message,
    sender: MessageSender.shared
)

然而,就像我们在 "避免在Swift中使用单例 "中看到的那样,单例方法也有一些明显的缺点,可能会导致我们陷入一种难以理解的架构和不明确的依赖关系的局面。

工厂模式来救援

如果我们能跳过上述所有的步骤,让MessageListViewController完全不知道MessageSender,以及其他任何后续视图控制器可能需要的依赖关系,那不是更好吗?

如果我们能有某种形式的工厂,我们可以简单地要求它为给定的消息创建一个MessageViewController,这将是非常方便的(甚至比引入一个单例更方便),而且非常干净,像这样:

let viewController = factory.makeMessageViewController(for: message)

就像我们在 "使用工厂模式来避免Swift中的共享状态 "中看到的那样,我非常喜欢工厂的一点是,它可以让你完全解耦对象的使用和创建。这使得许多对象与它们的依赖关系更加松散,这在你想要重构或改变事物的情况下非常有帮助。

那么,我们如何才能使上述情况发生呢?

我们将首先为我们的工厂定义一个协议,这将使我们能够轻松地创建我们应用程序中需要的任何视图控制器,而不需要实际了解其依赖性或初始化器。

protocol ViewControllerFactory {
    func makeMessageListViewController() -> MessageListViewController
    func makeMessageViewController(for message: Message) -> MessageViewController
}

但我们不会止步于此。我们还将创建额外的工厂协议来创建我们的视图控制器的依赖关系,比如这个,让我们为我们的列表视图控制器创建一个MessageLoader:

protocol MessageLoaderFactory {
    func makeMessageLoader() -> MessageLoader
}

单一的依赖性

一旦我们建立了工厂协议,我们就可以回到MessageListViewController,并重构它,使其不再接受其依赖的实例——它现在只接受一个工厂。

class MessageListViewController: UITableViewController {
    // 这里我们使用协议组合来创建一个工厂类型,
    // 其中包括这个视图控制器需要的所有工厂协议。
    typealias Factory = MessageLoaderFactory & ViewControllerFactory

    private let factory: Factory
     // 我们现在可以使用注入的工厂懒加载我们的 MessageLoader。
    private lazy var loader = factory.makeMessageLoader()

    init(factory: Factory) {
        self.factory = factory
        super.init(nibName: nil, bundle: nil)
    }
}

通过上述操作,我们现在已经完成了两件事。首先,我们将我们的依赖列表缩减为一个工厂,而且我们不再需要让MessageListViewController知道MessageViewController的依赖关系。

创建容器

现在是时候实现我们的工厂协议了。要做到这一点,我们首先要定义一个DependencyContainer,它将包含我们应用程序的所有核心实用对象,这些对象通常作为依赖关系被直接注入。这包括像之前的MessageSender,但也包括更多的低级逻辑类,比如我们可能使用的NetworkManager

class DependencyContainer {
    private lazy var messageSender = MessageSender(networkManager: networkManager)
    private lazy var networkManager = NetworkManager(urlSession: .shared)
}

正如你在上面看到的,我们使用了lazy属性,以便在初始化我们的对象时能够引用同一类别的其他属性。这是一个非常方便和漂亮的设置依赖关系的方法,因为你可以利用编译器来帮助你避免循环依赖等问题。

最后,我们将使我们的新依赖容器遵守我们的工厂协议,这将使我们能够把它作为工厂注入到我们的各种视图控制器和其他对象。

extension DependencyContainer: ViewControllerFactory {
    func makeMessageListViewController() -> MessageListViewController {
        return MessageListViewController(factory: self)
    }

    func makeMessageViewController(for message: Message) -> MessageViewController {
        return MessageViewController(message: message, sender: messageSender)
    }
}

extension DependencyContainer: MessageLoaderFactory {
    func makeMessageLoader() -> MessageLoader {
        return MessageLoader(networkManager: networkManager)
    }
}

分散所有权

现在是拼图的最后一块——我们究竟在哪里存储我们的依赖容器,谁应该拥有它,它应该被设置在哪里?最酷的是:因为我们将注入我们的依赖性容器作为我们的对象所需的工厂的实现,而且这些对象将持有对其工厂的强引用——我们没有必要将容器存储在其他地方。

例如,如果MessageListViewController是我们应用程序的初始视图控制器,我们可以简单地创建一个DependencyContainer的实例并将其传入:

let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()

window.rootViewController = UINavigationController(
    rootViewController: listViewController
)

不需要在任何地方保留任何全局变量,也不需要在应用程序委托中使用可选属性。

小结

使用工厂协议和容器来设置你的依赖注入是一个很好的方法,可以避免传递多个依赖关系,以及不得不创建复杂的初始化器。虽然这不是银弹,但它可以使依赖注入的使用更容易——这将使你更清楚地了解你的对象的实际依赖关系,同时也使测试更简单。

由于我们已经将所有的工厂定义为协议,我们可以通过实现任何给定工厂协议的特定测试版本,在测试中轻松地模拟它们。我将在未来的博文中写更多关于模拟和如何在测试中充分利用依赖注入的内容。

你怎么看?你以前使用过像这样的解决方案吗,或者你会尝试一下吗?

感谢您的阅读 !

\color{orange}{\Large \mathtt{在 Swift 中使用工厂进行依赖注入}}

译自 John SundellDependency injection using factories in Swift

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容