依赖注入(一):告别“意大利面条”,从源头理清依赖

从今天起,我们将开启一个全新的技术分享系列------深入浅出依赖注入(Dependency Injection, DI)。我希望通过这个系列,能和大家一起探讨如何编写出更健壮、更灵活、更易于测试的代码,最终提升我们整个团队的工程质量和开发效率。

我们都可能遇到过这样的场景:接手一个"祖传"模块,想修改一个小小的功能,却发现牵一发而动全身。代码像一碗缠绕不清的意大利面条,让你无从下手。这种高耦合、职责不清的代码,正是我们要通过引入DI等优秀设计思想来解决的头号敌人。

今天,作为开篇,我们先不谈高深的框架和理论,就从一个我们最熟悉的痛点开始。

1. 我们的痛点:一个典型的OrderViewController

想象一下,我们有一个负责展示订单详情的页面 OrderViewController。它的代码可能是这样的:

swift 复制代码
// A "traditional" view controller
class OrderViewController: UIViewController {

    private let networkService: NetworkService
    private let databaseManager: DatabaseManager
    private let analyticsTracker: AnalyticsTracker

    // ... 其他属性

    init(orderId: String) {
        // 问题在这里!
        // ViewController 亲自创建了自己的依赖
        self.networkService = NetworkService() // 依赖1:网络服务
        self.databaseManager = DatabaseManager.shared // 依赖2:数据库管理器(单例)
        self.analyticsTracker = AnalyticsTracker(trackingId: "XYZ-123") // 依赖3:分析追踪器

        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        fetchOrderDetails()
    }

    private func fetchOrderDetails() {
        networkService.fetchOrder { [weak self] order in
            self?.databaseManager.save(order)
            self?.analyticsTracker.trackEvent("order_displayed")
            // ... 更新UI
        }
    }
    // ... 其他代码
}

这段代码看起来很"正常",但它隐藏着几个严重的问题:

  • 极难测试: 你想为 OrderViewController 写个单元测试,测试获取订单后的UI展现逻辑。但只要你创建它,它就会创建一个真实的 NetworkService 并可能发出真实的网络请求。你想用一个模拟的MockNetworkService来返回假数据吗?对不起,做不到,因为NetworkService的创建被写死在了init方法内部。
  • 缺乏灵活性: 某天,产品经理说:"对于VIP用户的订单,我们要用一个新的VipNetworkService来请求,它有不同的缓存策略"。怎么办?你只能去修改OrderViewController的内部代码,增加if/else逻辑。如果未来还有SVIPNetworkService呢?这个类会变得越来越臃肿。
  • 依赖关系模糊: 如果不读init的实现,单从类的定义看,你根本不知道OrderViewController到底需要哪些"帮手"(依赖)才能正常工作。这些依赖关系像"幽灵"一样隐藏在实现细节里。

2. 核心思想:从"我主动去拿"到"你从外部给我"

以上问题的根源在于:OrderViewController 主动承担了创建其依赖(NetworkService等)的责任。

这就像一个厨师,不仅要负责炒菜,还得自己去种菜、养猪、打酱油。这显然不合理。专业的厨师只需要告诉采购员:"我需要顶级的牛肉和新鲜的番茄",他拿到食材后直接开始烹饪即可。

控制反转 (Inversion of Control, IoC) 就是这个思想。它将"创建依赖"这个控制权,从类的内部"反转"到了类的外部。

依赖注入 (Dependency Injection, DI) 则是实现IoC最常见、最直接的方式。它的口号就是:"别来问我需要什么,直接把你给我的东西拿来用"。

3. DI的三种基本姿势(Swift版)

那么,外部如何把依赖"喂"给我们的类呢?通常有三种方式:

a. 构造函数注入 (Initializer Injection) - 我们的首选!

这是最推荐、最纯粹的DI方式。我们将所有必需的依赖都通过init方法传入。

swift 复制代码
class OrderViewController: UIViewController {

    private let networkService: NetworkServiceProtocol
    private let databaseManager: DatabaseManagerProtocol
    private let analyticsTracker: AnalyticsTrackerProtocol

    // 依赖通过构造函数"注入"
    init(orderId: String, 
         networkService: NetworkServiceProtocol,
         databaseManager: DatabaseManagerProtocol, 
         analyticsTracker: AnalyticsTrackerProtocol) {
        self.networkService = networkService
        self.databaseManager = databaseManager
        self.analyticsTracker = analyticsTracker
        super.init(nibName: nil, bundle: nil)
    }
    // ...
}

// 在创建它的时候,由外部决定给它什么样的实例
let realService = NetworkService()
let mockService = MockNetworkService() // 用于测试

let vcForProd = OrderViewController(orderId: "123", networkService: realService, ...)
let vcForTest = OrderViewController(orderId: "123", networkService: mockService, ...)

优点:

  • 依赖明确: 一眼就能看出这个类需要哪些依赖才能工作。
  • 保证可用: 实例在创建完成后,所有依赖都已就绪,且不可更改(因为我们用了let)。
  • 非常利于测试: 像上面例子一样,轻松传入Mock对象。

b. 属性注入 (Property Injection)

通过一个可变的属性来注入依赖。

swift 复制代码
class OrderViewController: UIViewController {
    var networkService: NetworkServiceProtocol! // 注意这里的 '!'
    // ...
}

let vc = OrderViewController(orderId: "123")
vc.networkService = NetworkService() // 在创建后,通过属性设置依赖

适用场景:

  • 当框架负责创建对象时,比如从Storyboard或XIB初始化的ViewController,我们无法干预其init方法。
  • 用于解决循环依赖(我们将在后续文章中深入探讨)。

缺点: 依赖是可变的,且在对象生命周期的某个时刻可能是nil,不够安全。

c. 方法注入 (Method Injection)

只在调用某个特定方法时,才把依赖传进去。

swift 复制代码
class OrderLogger {
    func logOrder(_ order: Order, using persister: PersistenceProtocol) {
        let logData = format(order)
        persister.save(data: logData)
    }
}

适用场景: 当这个依赖(persister)与类的核心职责关系不大,仅仅是某一个操作需要时,使用方法注入可以避免让整个类都持有它。

4. 重点辨析:依赖注入 vs. 服务发现 (Service Locator)

这是新手中最常见的误区。很多人会把"服务发现"模式误当成DI,因为它看起来也能解决"硬编码创建实例"的问题。

服务发现模式通常长这样:

swift 复制代码
// 一个全局的服务定位器
class ServiceLocator {
    static let shared = ServiceLocator()
    private lazy var networkService: NetworkServiceProtocol = NetworkService()
    
    func getService<T>() -> T? {
        if T.self == NetworkServiceProtocol.self {
            return networkService as? T
        }
        return nil
    }
}

// 在ViewController中使用
class OrderViewController: UIViewController {
    private let networkService: NetworkServiceProtocol

    init(orderId: String) {
        // 主动去全局的"电话簿"查询服务
        guard let service = ServiceLocator.shared.getService() else {
             fatalError("NetworkService not found!")
        }
        self.networkService = service
        // ...
    }
}

看起来不错,但它其实是"披着羊皮的狼",会带来新的问题:

  1. 隐藏的依赖(幽灵依赖): OrderViewController的依赖关系又被隐藏起来了。它的init签名没有告诉我们它需要NetworkService,我们必须深入其代码,去寻找ServiceLocator.shared的调用。这让代码的可读性和可维护性急剧下降。
  2. 全局状态与耦合: ServiceLocator是一个全局单例。任何地方都可以访问它,任何地方也可能修改它。你的类不再是独立的,而是和一个看不见的全局状态紧紧地绑在了一起。
  3. 测试的痛苦: 当你测试OrderViewController时,你不能简单地传入一个mock对象了。你必须去处理那个全局的ServiceLocator,比如在测试开始前注册一个mock服务,在测试结束后再清理掉。这非常麻烦,且容易导致测试用例之间互相干扰。

一句话总结:

  • 依赖注入(DI): 依赖关系是明确的(Explicit)。类在"被动"地接收依赖。
  • 服务发现(Service Locator): 依赖关系是模糊的(Implicit)。类在"主动"地查询依赖。

作为团队,我们应该始终追求明确的、可预测的代码,因此,依赖注入是远优于服务发现的选择。

总结与展望

今天我们理清了依赖注入的核心思想:通过将依赖的创建权交由外部,实现类与类之间的解耦,从而极大地提升代码的可测试性、灵活性和可维护性。 我们学习了三种注入方式,并重点辨析了DI与服务发现的本质区别。

现在,你可能会想:"如果我的应用有几十上百个依赖,难道都要在AppDelegate里手动创建和注入吗?那也太复杂了!"

你问到了点子上!这正是DI容器(DI Container)要解决的问题。

在下一篇文章中,我们将亲自动手,从零到一写一个我们自己的迷你DI容器。通过这个过程,你将彻底理解那些流行的DI框架(如Swinject)背后的魔法究竟是什么。

敬请期待!

相关推荐
求知摆渡1 小时前
共享代码不是共享风险——公共库解耦的三种进化路径
java·后端·架构
MeteorSeed1 小时前
别让理论成为“紧箍咒”!打破开发教条主义做正确的软件
架构
brzhang1 小时前
前端死在了 Python 朋友的嘴里?他用 Python 写了个交互式数据看板,着实秀了我一把,没碰一行 JavaScript
前端·后端·架构
嘻嘻哈哈开森2 小时前
技术分享:深入了解 PlantUML
后端·面试·架构
不甘打工的程序猿2 小时前
nacos-client模块学习《心跳维持》
后端·架构
来自宇宙的曹先生4 小时前
【视频观看系统】- 技术与架构选型
架构·音视频
小陈phd4 小时前
langchain从入门到精通(四十一)——基于ReACT架构的Agent智能体设计与实现
react.js·架构·langchain
zzywxc7874 小时前
云原生 Serverless 架构下的智能弹性伸缩与成本优化实践
云原生·架构·serverless
帅夫帅夫4 小时前
前端存储入门:Cookie 与用户登录状态管理
前端·架构
前端付豪5 小时前
9、前端日志埋点系统的架构设计
前端·javascript·架构