引言:为什么依赖注入是必要的
在现代iOS和macOS开发中,代码的整洁性和可测试性变得至关重要。实现这些目标的关键原则之一是依赖注入(DI)。没有DI,类通常会变得“高度耦合”(tightly coupled),这阻碍了它们的重用、修改,最重要的是,编写单元测试变得困难。
在实践中,Swift中的依赖注入意味着对象从外部获取其依赖项,而不是自己创建它们。这看起来可能是一个微小的改变,但它会彻底改变应用程序的架构。在本文中,我们将探讨Swift中主要的DI模式:构造函数注入、属性注入以及使用Swinject作为DI容器。每种方法都将通过iOS/macOS的实际示例进行说明。
准备好让你的代码更灵活、更易于测试了吗?那我们开始吧。
1. 构造函数注入:最可靠的方式
构造函数注入是通过类的初始化器(init)注入依赖项。这种方法被认为是最优选的,因为它保证了对象始终处于有效状态:其所有依赖项都是必需且不可变的。
来看一个例子。假设我们有一个负责HTTP请求的类NetworkService,以及一个使用该服务的类UserViewModel。
// 用于抽象的协议protocol NetworkServiceProtocol { func fetchUsers() async throws -> [User]}
// 真实实现class RealNetworkService: NetworkServiceProtocol { func fetchUsers() async throws -> [User] { // 真实的HTTP请求 return [] }}
// 使用构造函数注入的ViewModelclass UserViewModel { private let networkService: NetworkServiceProtocol
// 依赖项通过初始化器传递 init(networkService: NetworkServiceProtocol) { self.networkService = networkService }
func loadUsers() async { do { let users = try await networkService.fetchUsers() print("Loaded \\(users.count) users") } catch { print("Error: \\(error)") } }}
// 使用let service = RealNetworkService()let viewModel = UserViewModel(networkService: service)优点:
- 依赖项被显式声明且是必需的。
- 没有依赖项就无法创建对象。
- 非常适合单元测试:可以轻松地将真实实现替换为Mock。
缺点:
- 如果依赖项很多,初始化器会变得臃肿。
- 不适用于可选依赖项。
这种方法是DI的“黄金标准”。尽可能在任何地方使用它。
2. 属性注入:可选依赖项的灵活性
属性注入是通过对象的属性注入依赖项。通常用于依赖项不是对象运行所必需的,或者无法通过初始化器传递(例如,在Storyboard/UIKit中)的情况。
一个经典的例子是iOS中的ViewController,它从Storyboard创建。你无法更改它的init,因此你使用属性。
protocol LoggerProtocol { func log(message: String)}
class ConsoleLogger: LoggerProtocol { func log(message: String) { print("LOG: \\(message)") }}
class MyViewController: UIViewController { // 可选依赖项 var logger: LoggerProtocol?
override func viewDidLoad() { super.viewDidLoad() logger?.log(message: "View did load") // 如果logger未设置,则什么也不会发生 }}
// 配置(例如