Pure

Swift CocoaPods Build Status Codecov

Pure 使 Swift 中的 Pure DI 变得简单。 此仓库还介绍了一种在 Swift 应用程序中执行 Pure DI 的方法。

目录

背景

Pure DI

Pure DI 是一种在不使用 DI 容器的情况下进行依赖注入的方法。 该术语最早由 Mark Seemann 提出。 Pure DI 的核心概念是不使用 DI 容器,而是在 组合根 (Composition Root) 中组合整个对象依赖图。

组合根 (Composition Root)

组合根是解析整个对象图的地方。 在 Cocoa 应用程序中,AppDelegate 是组合根。

AppDependency

根依赖项是 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

AppDelegate 是 Cocoa 应用程序中最重要的类之一。 它解析 app 依赖项并处理 app 事件。 由于我们分离了 app 依赖项,因此可以轻松地对其进行测试。

这是 AppDelegate 的一个示例测试用例。 它验证 AppDelegateapplication(_: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

但是存在一个问题:在测试时,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)

延迟依赖 (Lazy Dependency)

使用工厂 (Factory)

在 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 属性怎么办?

一种可能的方法是在组合根之外创建一个工厂闭包。 请注意,StoryboardNetworking 来自组合根,而 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

使用配置器 (Configurator)

还有另一种延迟依赖。 Cells 是延迟创建的,但我们不能使用工厂闭包,因为 cells 由框架创建。 我们只需要配置 cells。

假设一个 UICollectionViewCellUITableViewCell 显示一个图像。 有一个 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 使工厂和配置器变得简洁。

开始使用

依赖 (Dependency) 和有效载荷 (Payload)

首先,看看我们在示例代码中使用的工厂和配置器。

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 使用依赖和有效载荷来概括工厂和配置器。

模块 (Module)

Pure 将每个需要依赖和有效载荷的类视为模块 (Module)。 协议 Module 需要两种类型:DependencyPayload

protocol Module {
  /// A dependency that is resolved in the Composition Root.
  associatedtype Dependency

  /// A runtime information for configuring the module.
  associatedtype Payload
}

有两种类型的模块:FactoryModuleConfiguratorModule

工厂模块 (Factory Module)

FactoryModule 是工厂闭包的通用版本。 它需要一个同时接受 dependencypayload 的初始化器。

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 时,它将具有一个嵌套类型 FactoryFactory.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
))

配置器模块 (Configurator Module)

ConfiguratorModule 是配置器闭包的通用版本。 它需要一个 configure() 方法,该方法同时接受 dependencypayload

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 时,它将具有一个嵌套类型 ConfiguratorConfigurator.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))

使用 FactoryModuleConfiguratorModule,可以如下重构该示例

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
      ))
    ))
  )
}

自定义

FactoryConfigurator 是可自定义的。 这是一个自定义工厂的示例

extension Factory where Module == DetailViewController {
  func create(payload: Module.Payload, extraValue: ExtraValue) -> Payload {
    let module = self.create(payload: payload)
    module.extraValue = extraValue
    return module
  }
}

Storyboard 支持

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 支持

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 💖

许可证

Pure 采用 MIT 许可证。 有关更多信息,请参见 LICENSE 文件。