Pure 1.1.4

Pure 1.1.4

Suyeol Jeon 维护。



Pure 1.1.4

  • 作者:
  • Suyeol Jeon

Pure

Swift CocoaPods Build Status Codecov

Pure 让在 Swift 中使用 Pure DI 变得更加容易。该仓库还介绍了一种在 Swift 应用程序中实现 Pure DI 的方法。

目录

背景

Pure DI

Pure DI 是一种不使用 DI 容器进行依赖注入的方法。这个术语首先由 Mark Seemann 提出。Pure DI 的核心概念是不使用 DI 容器,而是在 组合根 中组合整个对象依赖图。

组合根

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

应用依赖

根依赖包括应用代理的依赖和根视图控制器的依赖。注入这些依赖的最佳方式是创建一个名为`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()`初始化实际的应用依赖。另一方面,我们还将提供一个`init(dependency:)`来在测试环境中注入模拟应用依赖。

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

下面的代码示例展示了如何使用应用依赖

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应用程序中最重要的类之一。它解析应用依赖并处理应用事件。由于我们隔离了应用依赖,它很容易进行测试。

下面是一个`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事件甚至应用终止事件编写测试。

分离AppDelegate

但是有一个问题:在测试时,系统仍然创建了`AppDelegate`。这导致`AppDependency.resolve()`被调用,因此我们在测试环境中必须使用一个假的应用代理类。

首先,在测试目标中创建一个新的文件。定义一个新的名为`TestAppDelegate`的类,并实现代理协议的基本要求。

// iOS
class TestAppDelegate: NSObject, UIApplicationDelegate {
  var window: UIWindow?
}

// macOS
class TestAppDelegate: NSObject, NSApplicationDelegate {
}

然后在应用程序目标中创建另一个名为 main.swift 的文件。此文件将替换应用程序的入口点。我们将在此文件中提供不同的应用程序代理。不要忘记将 "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)

最后,将 @UIApplicationMain@NSApplicationMainAppDelegate 中移除。

  // iOS
- @UIApplicationMain
  class AppDelegate: UIResponder, UIApplicationDelegate

  // macOS
- @NSApplicationMain
  class AppDelegate: NSObject, NSApplicationDelegate

在测试环境中验证应用程序是否正在使用 TestAppDelegate 也是一个很好的实践。

XCTAssertTrue(UIApplication.shared.delegate is TestAppDelegate)

惰性依赖

使用工厂

在 Cocoa 应用程序中,视图控制器是懒加载的。例如,只有当用户在 ListViewController 中点击项目时,才会创建 DetailViewController。在这种情况下,我们必须将 DetailViewController 的工厂闭包传递给 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

使用配置器

还有一个惰性依赖。单元格是懒加载的,但我们不能使用工厂闭包,因为单元格是由框架创建的。我们只能配置单元格。

想象一下,一个 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)
    }
  }
}

这个单元格在 DetailViewController 中显示。DetailViewController 应该将 imageDownloader 注入到单元格中并设置 image 属性。就像我们在工厂中做的那样,我们可以为此创建一个配置器闭包。但这个闭包接受一个现有的实例而没有返回值。

class ItemCell {
  static let configurator: (ImageDownloaderType) -> (ItemCell, UIImage) -> Void = { imageDownloader
    return { cell, image in
      cell.imageDownloader = imageDownloader
      cell.image = image
    }
  }
}

DetailViewController 可以拥有配置器并在配置单元格时使用它。

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。外部函数的参数是依赖。内部函数的参数是有效载荷

static let factory: (UIStoryboard, Networking, (ItemCell, UIImage) -> Void) -> (Item) -> DetailViewController
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^      ^^^^
                                        Dependency                             Payload

static let configurator: (ImageDownloaderType) -> (ItemCell, UIImage) -> Void
                          ^^^^^^^^^^^^^^^^^^^      ^^^^^^^^^^^^^^^^^
                              Dependency                Payload

Pure使用依赖和有效载荷概括工厂和配置器。

模块

Pure将需要依赖和有效载荷的每个类视为一个模块。协议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

工厂模块

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

配置模块

ConfiguratorModule 是配置闭包的泛型版本。它需要一个接收 dependencypayloadconfigure() 方法。

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 是一个用于支持 deeplink 的优雅库。Pure 也可以用于将视图控制器注册到导航器。

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)

安装

贡献

欢迎任何讨论和拉取请求💖

  • 向开发

    $ make project
  • 向测试

    $ swift test

许可

Pure是在MIT许可下。详见LICENSE文件了解详细信息。