Pure 使 Swift 中的 Pure DI 变得简单。 此仓库还介绍了一种在 Swift 应用程序中执行 Pure DI 的方法。
Pure DI 是一种在不使用 DI 容器的情况下进行依赖注入的方法。 该术语最早由 Mark Seemann 提出。 Pure DI 的核心概念是不使用 DI 容器,而是在 组合根 (Composition Root) 中组合整个对象依赖图。
组合根是解析整个对象图的地方。 在 Cocoa 应用程序中,AppDelegate
是组合根。
根依赖项是 app delegate 的依赖项和根视图控制器的依赖项。 注入这些依赖项的最佳方法是创建一个名为 AppDependency
的结构体,并将两个依赖项都存储在其中。
struct AppDependency {
let networking: Networking
let remoteNotificationService: RemoteNotificationService
}
extension AppDependency {
static func resolve() -> AppDependency {
let networking = Networking()
let remoteNotificationService = RemoteNotificationService()
return AppDependency(
networking: networking
remoteNotificationService: remoteNotificationService
)
}
}
将生产环境与测试环境分开非常重要。 我们必须在生产环境中使用实际对象,并在测试环境中使用模拟对象。
AppDelegate 由系统使用 init()
自动创建。 在此初始化器中,我们将使用 AppDependency.resolve()
初始化实际的 app 依赖项。 另一方面,我们将提供一个 init(dependency:)
来在测试环境中注入模拟 app 依赖项。
class AppDelegate: UIResponder, UIApplicationDelegate {
private let dependency: AppDependency
/// Called from the system (it's private: not accessible in the testing environment)
private override init() {
self.dependency = AppDependency.resolve()
super.init()
}
/// Called in a testing environment
init(dependency: AppDependency) {
self.dependency = dependency
super.init()
}
}
app 依赖项可以像下面的代码一样使用
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// inject rootViewController's dependency
if let viewController = self.window?.rootViewController as? RootViewController {
viewController.networking = self.dependency.networking
}
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// delegates remote notification receive event
self.dependency.remoteNotificationService.receiveRemoteNotification(userInfo)
}
AppDelegate
是 Cocoa 应用程序中最重要的类之一。 它解析 app 依赖项并处理 app 事件。 由于我们分离了 app 依赖项,因此可以轻松地对其进行测试。
这是 AppDelegate
的一个示例测试用例。 它验证 AppDelegate
在 application(_:didFinishLaunchingWithOptions)
中正确注入根视图控制器的依赖项。
class AppDelegateTests: XCTestCase {
func testInjectRootViewControllerDependencies() {
// given
let networking = MockNetworking()
let mockDependency = AppDependency(
networking: networking,
remoteNotificationService: MockRemoteNotificationService()
)
let appDelegate = AppDelegate(dependency: mockDependency)
appDelegate.window = UIWindow()
appDelegate.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
// when
_ = appDelegate.application(.shared, didFinishLaunchingWithOptions: nil)
// then
let rootViewController = appDelegate.window?.rootViewController as? RootViewController
XCTAssertTrue(rootViewController?.networking === networking)
}
}
您可以编写测试来验证远程通知事件、打开 url 事件,甚至 app 终止事件。
但是存在一个问题:在测试时,AppDelegate
仍然由系统创建。 这会导致调用 AppDependency.resolve()
,因此我们必须在测试环境中使用一个假的 app delegate 类。
首先,在测试目标中创建一个新文件。 定义一个名为 TestAppDelegate
的新类,并实现委托协议的基本要求。
// iOS
class TestAppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?
}
// macOS
class TestAppDelegate: NSObject, NSApplicationDelegate {
}
然后,将另一个名为 main.swift
的文件添加到您的应用程序目标。 此文件将替换应用程序的入口点。 我们将在此文件中提供不同的 app delegates。 不要忘记将 "MyAppTests.TestAppDelegate"
替换为您的项目目标和类名。
// iOS
UIApplicationMain(
CommandLine.argc,
CommandLine.unsafeArgv,
NSStringFromClass(UIApplication.self),
NSStringFromClass(NSClassFromString("MyAppTests.TestAppDelegate") ?? AppDelegate.self)
)
// macOS
func createAppDelegate() -> NSApplicationDelegate {
if let cls = NSClassFromString("AllkdicTests.TestAppDelegate") as? (NSObject & NSApplicationDelegate).Type {
return cls.init()
} else {
return AppDelegate(dependency: AppDependency.resolve())
}
}
let application = NSApplication.shared
application.delegate = createAppDelegate()
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
最后,从 AppDelegate
中删除 @UIApplicationMain
和 @NSApplicationMain
。
// iOS
- @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
// macOS
- @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate
添加一个测试用例来验证应用程序是否在测试环境中使用 TestAppDelegate
也是一个好习惯。
XCTAssertTrue(UIApplication.shared.delegate is TestAppDelegate)
在 Cocoa 应用程序中,视图控制器是延迟创建的。 例如,在用户点击 ListViewController
上的一个项目之前,不会创建 DetailViewController
。 在这种情况下,我们必须将 DetailViewController
的工厂闭包 (factory closure) 传递给 ListViewController
。
/// A root view controller
class ListViewController {
var detailViewControllerFactory: ((Item) -> DetailViewController)!
func presentItemDetail(_ selectedItem: Item) {
let detailViewController = self.detailViewControllerFactory(selectedItem)
self.present(detailViewController, animated: true)
}
}
static func resolve() -> AppDependency {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let networking = Networking()
let detailViewControllerFactory: (Item) -> DetailViewController = { selectedItem in
let detailViewController = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
detailViewController.networking = networking
detailViewController.item = selectedItem
return detailViewController
}
return AppDependency(
networking: networking,
detailViewControllerFactory: detailViewControllerFactory
)
}
但这存在一个严重的问题:我们无法测试工厂闭包。 因为工厂闭包是在组合根中创建的,但我们不应该在测试环境中访问组合根。 如果我忘记注入 DetailViewController.networking
属性怎么办?
一种可能的方法是在组合根之外创建一个工厂闭包。 请注意,Storyboard
和 Networking
来自组合根,而 Item
来自先前的视图控制器,因此我们必须分离范围。
extension DetailViewController {
static let factory: (UIStoryboard, Networking) -> (Item) -> DetailViewController = { storyboard, networking in
return { selectedItem in
let detailViewController = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
detailViewController.networking = networking
detailViewController.item = selectedItem
return detailViewController
}
}
}
static func resolve() -> AppDependency {
let storyboard = ...
let networking = ...
return .init(
detailViewControllerFactory: DetailViewController.factory(storyboard, networking)
)
}
现在我们可以测试 DetailViewController.factory
闭包。 每个依赖项都在组合根中解析,并且可以在运行时将选定的项目从 ListViewController
传递到 DetailViewController
。
还有另一种延迟依赖。 Cells 是延迟创建的,但我们不能使用工厂闭包,因为 cells 由框架创建。 我们只需要配置 cells。
假设一个 UICollectionViewCell
或 UITableViewCell
显示一个图像。 有一个 imageDownloader
在生产环境中下载实际图像,并在测试环境中返回模拟图像。
class ItemCell {
var imageDownloader: ImageDownloaderType?
var imageURL: URL? {
didSet {
guard let imageDownloader = self.imageDownloader else { return }
self.imageView.setImage(with: self.imageURL, using: imageDownloader)
}
}
}
此 cell 显示在 DetailViewController
中。 DetailViewController
应该将 imageDownloader
注入到 cell 中,并设置 image
属性。 就像我们在工厂中所做的那样,我们可以为它创建一个配置器闭包。 但是此闭包接受一个现有实例,并且没有返回值。
class ItemCell {
static let configurator: (ImageDownloaderType) -> (ItemCell, UIImage) -> Void = { imageDownloader
return { cell, image in
cell.imageDownloader = imageDownloader
cell.image = image
}
}
}
DetailViewController
可以拥有配置器,并在配置 cell 时使用它。
class DetailViewController {
var itemCellConfigurator: ((ItemCell, UIImage) -> Void)?
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
self.itemCellConfigurator?(cell, image)
return cell
}
}
DetailViewController.itemCellConfigurator
从工厂注入。
extension DetailViewController {
static let factory: (UIStoryboard, Networking, (ItemCell, UIImage) -> Void) -> (Item) -> DetailViewController = { storyboard, networking, imageCellConfigurator in
return { selectedItem in
let detailViewController = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
detailViewController.networking = networking
detailViewController.item = selectedItem
detailViewController.imageCellConfigurator = imageCellConfigurator
return detailViewController
}
}
}
组合根最终看起来像
static func resolve() -> AppDependency {
let storybard = ...
let networking = ...
let imageDownloader = ...
let listViewController = ...
listViewController.detailViewControllerFactory = DetailViewController.factory(
storyboard,
networking,
ImageCell.configurator(imageDownloader)
)
...
}
理论上,它可以工作。 但是正如您在 DetailViewController.factory
中看到的那样,当存在许多依赖项时,它将非常复杂。 这就是我创建 Pure 的原因。 Pure 使工厂和配置器变得简洁。
首先,看看我们在示例代码中使用的工厂和配置器。
static let factory: (UIStoryboard, Networking, (ItemCell, UIImage) -> Void) -> (Item) -> DetailViewController
static let configurator: (ImageDownloaderType) -> (ItemCell, UIImage) -> Void
这些是返回另一个函数的函数。 外部函数在组合根中执行以注入静态依赖项 (例如 Networking
),内部函数在视图控制器中执行以传递运行时信息 (例如 selectedItem
)。 外部函数的参数是依赖 (Dependency)。 内部函数的参数是有效载荷 (Payload)。
static let factory: (UIStoryboard, Networking, (ItemCell, UIImage) -> Void) -> (Item) -> DetailViewController
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^
Dependency Payload
static let configurator: (ImageDownloaderType) -> (ItemCell, UIImage) -> Void
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
Dependency Payload
Pure 使用依赖和有效载荷来概括工厂和配置器。
Pure 将每个需要依赖和有效载荷的类视为模块 (Module)。 协议 Module
需要两种类型:Dependency
和 Payload
。
protocol Module {
/// A dependency that is resolved in the Composition Root.
associatedtype Dependency
/// A runtime information for configuring the module.
associatedtype Payload
}
有两种类型的模块:FactoryModule
和 ConfiguratorModule
。
FactoryModule 是工厂闭包的通用版本。 它需要一个同时接受 dependency
和 payload
的初始化器。
protocol FactoryModule: Module {
init(dependency: Dependency, payload: Payload)
}
class DetailViewController: FactoryModule {
struct Dependency {
let storyboard: UIStoryboard
let networking: Networking
}
struct Payload {
let selectedItem: Item
}
init(dependency: Dependency, payload: Payload) {
}
}
当一个类符合 FactoryModule
时,它将具有一个嵌套类型 Factory
。 Factory.init(dependency:)
接受模块的依赖项,并具有一个 create(payload:)
方法来创建一个新实例。
class Factory<Module> {
let dependency: Module.Dependency
func create(payload: Module.Payload) -> Module
}
// In AppDependency
let factory = DetailViewController.Factory(dependency: .init(
storyboard: storyboard
networking: networking
))
// In ListViewController
let viewController = factory.create(payload: .init(
selectedItem: selectedItem
))
ConfiguratorModule 是配置器闭包的通用版本。 它需要一个 configure()
方法,该方法同时接受 dependency
和 payload
。
protocol ConfiguratorModule: Module {
func configure(dependency: Dependency, payload: Payload)
}
class ItemCell: ConfiguratorModule {
struct Dependency {
let imageDownloader: ImageDownloaderType
}
struct Payload {
let image: UIImage
}
func configure(dependency: Dependency, payload: Payload) {
self.imageDownloader = dependency.imageDownloader
self.image = payload.image
}
}
当一个类符合 ConfiguratorModule
时,它将具有一个嵌套类型 Configurator
。 Configurator.init(dependency:)
接受模块的依赖项,并具有一个 configure(_:payload:)
方法来配置现有实例。
class Configurator<Module> {
let dependency: Module.Dependency
func configure(_ module: Module, payload: Module.Payload)
}
// In AppDependency
let configurator = ItemCell.Configurator(dependency: .init(
imageDownloader: imageDownloader
))
// In DetailViewController
configurator.configure(cell, payload: .init(image: image))
使用 FactoryModule
和 ConfiguratorModule
,可以如下重构该示例
static func resolve() -> AppDependency {
let storybard = ...
let networking = ...
let imageDownloader = ...
return .init(
detailViewControllerFactory: DetailViewController.Factory(dependency: .init(
storyboard: storyboard,
networking: networking,
itemCellConfigurator: ItemCell.Configurator(dependency: .init(
imageDownloader: imageDownloader
))
))
)
}
Factory
和 Configurator
是可自定义的。 这是一个自定义工厂的示例
extension Factory where Module == DetailViewController {
func create(payload: Module.Payload, extraValue: ExtraValue) -> Payload {
let module = self.create(payload: payload)
module.extraValue = extraValue
return module
}
}
FactoryModule
可以使用自定义功能支持 Storyboard 实例化的视图控制器。 以下代码是 DetailViewController
的 storyboard 支持示例
extension Factory where Module == DetailViewController {
func create(payload: Module.Payload) -> Payload {
let module = self.dependency.storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! Module
module.networking = dependency.networking
module.itemCellConfigurator = dependency.itemCellConfigurator
module.selectedItem = payload.selectedItem
return module
}
}
URLNavigator 是一个优雅的库,用于深层链接支持。 Pure 也可以用于将视图控制器注册到 navigator。
class UserViewController {
struct Payload {
let userID: Int
}
}
extension Factory where Module == UserViewController {
func create(url: URLConvertible, values: [String: Any], context: Any?) -> Module? {
guard let userID = values["id"] else { return nil }
return self.create(payload: .init(userID: userID))
}
}
let navigator = Navigator()
navigator.register("myapp://user/<id>", UserViewController.Factory().create)
欢迎任何讨论和 pull requests 💖
开发
$ make project
测试
$ swift test
Pure 采用 MIT 许可证。 有关更多信息,请参见 LICENSE 文件。