引言:为什么你的代码需要依赖注入?
每个iOS开发者都会遇到这样的情况:一个类变得“臃肿”,并且与具体实现紧密耦合。你修改一个类,却不得不重写几十个其他类。熟悉吗?这就是经典的紧耦合问题。
依赖注入(Dependency Injection, DI)是一种解决此问题的设计模式。对象不再自己创建依赖,而是从外部“注入”依赖。这使得代码更灵活、可测试,并符合SOLID原则(特别是依赖反转原则)。
在本文中,我们将探讨Swift中的DI是什么、有哪些实现方式,以及它如何简化iOS/macOS开发者的工作。我们将使用简单易懂的示例,即使是初学者也能理解。
什么是依赖注入?一个简单的例子
想象一下你在做披萨。没有DI的情况:你自己种小麦、磨面粉、挤牛奶做奶酪。这是紧耦合。有DI的情况:你只需向供应商订购原料(依赖)。你不知道面粉是怎么做的——你只关心结果。
在编程中也是如此。假设我们有一个通过电子邮件发送订单的OrderService类:
// ❌ 没有DI——紧耦合class OrderService { let mailer = Mailer() // 自己创建依赖
func process(order: Order) { mailer.send(email: "新订单:\\(order.id)") }}问题:我们无法将Mailer替换为测试版本。OrderService类“知道”得太多了。
// ✅ 使用DI——依赖被注入class OrderService { let mailer: MailerProtocol // 依赖抽象,而非具体实现
init(mailer: MailerProtocol) { // 通过初始化器注入 self.mailer = mailer }
func process(order: Order) { mailer.send(email: "新订单:\\(order.id)") }}现在,我们可以传入任何符合MailerProtocol协议的对象——无论是真实的邮件发送器还是用于测试的“替身”。
Swift中依赖注入的三种主要方式
有三种流行的方法可以将依赖传递给类。我们来逐一介绍。
1. 构造函数注入(通过初始化器)
这是最常见且首选的方式。依赖在对象创建时传入。这保证了对象始终处于有效状态。
protocol NetworkServiceProtocol { func fetchData() -> String}
class RealNetworkService: NetworkServiceProtocol { func fetchData() -> String { return "来自服务器的真实数据" }}
class MockNetworkService: NetworkServiceProtocol { func fetchData() -> String { return "测试数据" }}
class DataManager { private let network: NetworkServiceProtocol
// 依赖通过init传入 init(network: NetworkServiceProtocol) { self.network = network }
func load() -> String { return network.fetchData() }}
// 使用let realManager = DataManager(network: RealNetworkService())let mockManager = DataManager(network: MockNetworkService())优点: 明确显示需要哪些依赖。对象立即可用。缺点: 如果依赖很多,初始化器会变得臃肿。
2. 属性注入(通过属性)
依赖在对象创建后设置。常用于依赖是可选的,或者在初始化时难以获取的情况。
class UserViewModel { var apiService: NetworkServiceProtocol? // 可选依赖
func loadUser() { apiServi