[已弃用] Cleanse - Swift 依赖注入

Documentation/cleanse_logo_small.png

https://travis-ci.org/square/Cleanse.svg?branch=master https://coveralls.io/repos/github/square/Cleanse/badge.svg?branch=master&asdf

Cleanse 是一个为 Swift 设计的 依赖注入 框架。它的设计从一开始就以开发者体验为中心。它的灵感来源于 DaggerGuice

弃用通知

亲爱的 Cleanse 社区,

我们在此宣布,Cleanse 仓库将于 2024 年 6 月 13 日正式弃用。 多年来,Cleanse 一直致力于为 Swift 依赖注入提供强大的解决方案,我们非常感谢社区的支持和贡献。

此决定基于 Square 决定使用不同的依赖注入框架。我们相信这一步将使我们能够专注于其他项目和创新,从而更好地满足用户和更广泛的开源生态系统的需求。

这对您意味着什么

推荐的替代方案

我们鼓励您 fork 这个仓库,或探索以下可能更好地满足您需求的替代方案

联系我们

如果您有兴趣维护此仓库,或有任何其他疑问,请通过 opensource+external@squareup.com 联系我们。

感谢您

我们衷心感谢每一位为 Cleanse 做出贡献的人,无论是通过代码、文档还是社区支持。您的努力是无价的,我们深表感谢。感谢您成为 Cleanse 旅程的一部分。

衷心感谢,Square 的 Cleanse 团队

入门指南

这是一个关于如何在您的应用程序中开始使用 Cleanse 的快速指南。

Examples/CleanseGithubBrowser 中可以找到一个使用 Cleanse 和 Cocoa Touch 的完整示例

安装

使用 CocoaPods

您可以使用以下命令将最新的 Cleanse 版本拉取到您的 Podfile

pod 'Cleanse'

使用 Xcode

可以将 Cleanse.xcodeproj 拖放到 Xcode 中现有的项目或工作区中。可以将 Cleanse.framework 添加为目标依赖项并嵌入它。

使用 Carthage

Cleanse 应该能够通过 Carthage 进行配置。应该能够按照 向应用程序添加 Frameworks 中的说明,从 Carthage 的 README 中成功完成此操作。

使用 Swift Package Manager

Cleanse 可以与 Swift Package Manager 一起使用。以下是可以添加到 Project 声明的依赖项中的定义。Xcode 11 中添加 Cleanse 作为包依赖项受 v4.2.5 及更高版本支持。

特性

特性 Cleanse 实现状态
多重绑定 支持 (.intoCollection())
覆盖 支持
Objective-C 兼容层 支持
属性注入 [1] 支持
类型限定符 通过 类型标签 支持
辅助注入 支持
子组件 通过 组件 支持
服务提供者接口 支持
cleansec (Cleanse 编译器) 实验性
[1] 属性注入在其他 DI 框架中被称为 字段注入

DI 框架的另一个非常重要的部分是它如何处理错误。快速失败是理想的。Cleanse 的设计旨在支持快速失败。它目前支持一些常见错误的快速失败,但尚未完成

错误类型 Cleanse 实现状态
缺少提供者 支持 [2]
重复绑定 支持
循环检测 支持
[2] 当缺少提供者时,错误会显示提供者需要的行号等信息。Cleanse 还会在失败前收集所有错误

使用 Cleanse

Cleanse API 位于名为 Cleanse 的 Swift 模块中(令人惊讶吗?)。要在文件中使用其任何 API,必须在顶部导入它。

import Cleanse

定义组件和根类型

Cleanse 负责构建一个图(或更具体地说是一个 有向无环图),它表示您的所有依赖项。此图以根对象开始,根对象连接到其直接依赖项,而这些依赖项又持有到其依赖项的边,依此类推,直到我们获得应用程序对象图的完整图景。

使用 Cleanse 管理依赖项的入口点是从定义一个“根”对象开始的,该对象在构造时返回给您。在 Cocoa Touch 应用程序中,我们的根对象可以是我们在应用程序的 UIWindow 上设置的 rootViewController 对象。(更符合逻辑的是根对象是 App Delegate,但是由于我们不控制它的构造,我们将不得不使用属性注入。您可以在 高级设置 指南中阅读更多相关信息)

让我们从定义 RootComponent 开始

struct Component : Cleanse.RootComponent {
    // When we call build(()) it will return the Root type, which is a RootViewController instance.
    typealias Root = RootViewController

    // Required function from Cleanse.RootComponent protocol.
    static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController> {

    }

    // Required function from Cleanse.RootComponent protocol.
    static func configure(binder: Binder<Unscoped>) {
        // We will fill out contents later.
    }
}

创建根组件后,我们发现我们需要实现两个函数:static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController>static func configure(binder: Binder<Unscoped>)。这些函数非常重要,因为它们将包含关于我们如何在应用程序中构造每个对象/依赖项的逻辑。参数和返回类型现在令人困惑,但随着我们的深入,它们会变得更有意义。

第一个函数是任何 Component 都需要的,因为它告诉 Cleanse 如何构造根对象。让我们填写内容以配置我们将如何构造 RootViewController

static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController> {
    return bind.to(factory: RootViewController.init)
}

现在,让我们创建我们的 RootViewController

class RootViewController: UIViewController {
    init() {
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .blue
    }
}

我们已经成功连接了我们的根组件!我们的根对象 RootViewController 已正确配置,因此在我们的 App Delegate 中,我们现在可以构建组件(和图)来使用它。

重要提示:您必须保留从 ComponentFactory.of(:) 返回的 ComponentFactory<E> 的实例。否则,子组件可能会意外地被释放。

// IMPORTANT: We must retain an instance of our `ComponentFactory`.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var factory: ComponentFactory<AppDelegate.Component>?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) ->  Bool {
        // Build our root object in our graph.
        factory = try! ComponentFactory.of(AppDelegate.Component.self)
        let rootViewController = factory!.build(())

        // Now we can use the root object in our app.
        window!.rootViewController = rootViewController
        window!.makeKeyAndVisible()

        return true
    }

满足依赖项

现在运行应用程序将显示带有蓝色背景的 RootViewController。但这不是很令人兴奋,也不现实,因为我们的 RootViewController 可能需要许多依赖项来设置我们的应用程序。因此,让我们创建一个简单的依赖项 RootViewProperties,它将保存根视图的背景颜色(以及其他未来的属性)。

struct RootViewProperties {
    let backgroundColor: UIColor
}

然后将 RootViewProperties 注入到我们的 RootViewContoller 中并设置背景颜色。

class RootViewController: UIViewController {
    let rootViewProperties: RootViewProperties
    init(rootViewProperties: RootViewProperties) {
        self.rootViewProperties = rootViewProperties
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = rootViewProperties.backgroundColor
    }
}

现在运行应用程序将产生一个新的错误,提示缺少 RootViewProperties 的提供者。这是因为我们从 RootViewController 类中引用了它,但 Cleanse 没有找到 RootViewProperties 类型的绑定。所以让我们创建一个!我们将在根组件中之前讨论过的 static func configure(binder: Binder<Unscoped>) 函数中执行此操作。

static func configure(binder: Binder<Unscoped>) {
      binder
          .bind(RootViewProperties.self)
          .to { () -> RootViewProperties in
              RootViewProperties(backgroundColor: .blue)
          }
  }

现在我们已经满足了 RootViewProperties 依赖项,我们应该能够成功启动并看到与之前相同的蓝色背景。

随着此应用程序的功能增长,可以向 RootViewController 添加更多依赖项,并添加更多 模块 来满足它们。

可能值得查看我们的 示例应用程序,以查看更完整功能的示例。

核心概念和数据类型

Provider/ProviderProtocol

包装其包含类型的值。提供与 Java 的 javax.inject.Provider 相同的功能。

ProviderTaggedProvider(见下文)实现 ProviderProtocol 协议,该协议定义为

public protocol ProviderProtocol {
    associatedtype Element
    func get() -> Element
}

类型标签

在给定的组件中,可能希望提供或需要具有不同意义的常见类型的不同实例。也许我们需要区分 API 服务器的基本 URL 和临时目录的 URL。

在 Java 中,这是通过注解完成的,特别是用 @Qualifier 注解的注解。在 Go 中,这可以通过字段的 结构体标签 来完成。

在 Cleanse 的系统中,类型注解等同于 Tag 协议的实现

public protocol Tag {
    associatedtype Element
}

关联类型 Element 指示标签对其有效的类型。这与 Java 中用作 Dagger 和 Guice 中的限定符的注解非常不同,后者无法通过它们应用的类型来约束。

在 Cleanse 中,实现了 Tag 协议以区分类型,并且 TaggedProvider 用于包装 Tag.Element 的值。由于库的大部分内容都引用 ProviderProtocol,因此几乎在任何 Provider 的地方都接受 TaggedProvider

除了额外的泛型参数外,它的定义与 Provider 几乎相同

struct TaggedProvider<Tag : Cleanse.Tag> : ProviderProtocol {
    func get() -> Tag.Element
}

示例

假设有人想要指示 URL 类型,可能是 API 端点的基本 URL,可以这样定义标签

public struct PrimaryAPIURL : Tag {
    typealias Element = NSURL
}

然后,可以使用类型请求此特殊 URL 的 TaggedProvider

TaggedProvider<PrimaryAPIURL>

如果我们有一个类需要此 URL 来执行功能,则可以像这样定义构造函数

class SomethingThatDoesAnAPICall {
    let primaryURL: NSURL
    init(primaryURL: TaggedProvider<PrimaryAPIURL>) {
        self.primaryURL = primaryURL.get()
    }
}

模块

Cleanse 中的模块与 Dagger 或 Guice 等其他 DI 系统中的模块用途相似。模块是对象图的构建块。在 Cleanse 中使用模块可能与熟悉 Guice 的人非常相似,因为配置是在运行时完成的,并且绑定 DSL 非常受 Guice 的启发。

Module 协议只有一个方法 configure(binder:),其定义如下

protocol Module {
    func configure<B : Binder>(binder: B)
}

示例

提供基本 API URL
struct PrimaryAPIURLModule : Module {
  func configure(binder: Binder<Unscoped>) {
    binder
      .bind(NSURL.self)
      .tagged(with: PrimaryAPIURL.self)
      .to(value: NSURL(string: "https://connect.squareup.com/v2/")!)
  }
}
使用主 API URL(例如 "https://connect.squareup.com/v2/")

注意:通常,最好将配置 X 的 Module 嵌入为 X 的名为 Module 的内部结构体。为了消除 Cleanse 的 Module 协议与正在定义的内部结构体之间的歧义,必须使用 Cleanse.Module 限定协议

class SomethingThatDoesAnAPICall {
    let primaryURL: NSURL
    init(primaryURL: TaggedProvider<PrimaryAPIURL>) {
        self.primaryURL = primaryURL.get()
    }
    struct Module : Cleanse.Module {
        func configure(binder: Binder<Unscoped>) {
            binder
                .bind(SomethingThatDoesAnAPICall.self)
                .to(factory: SomethingThatDoesAnAPICall.init)
        }
    }
}

组件

Cleanse 具有 Component 的概念。Component 表示依赖项的对象图,该对象图在构造时返回 Root 关联类型,并用作 Cleanse 的“入口点”。但是,我们也可以使用 Component 在父对象图内部创建子图,称为子组件。子组件与 作用域 密切相关,并用于限定依赖项的作用域。组件内部的对象只允许注入存在于同一组件(或作用域)或祖先组件中的依赖项。父组件不允许深入子组件并检索依赖项。使用组件限定依赖项作用域的一个示例是拥有从应用程序的根组件继承的 LoggedInComponent。这允许您在 LoggedInComponent 中绑定特定于登录的对象,例如会话令牌或帐户对象,这样您就不会意外地将这些依赖项泄漏到登录会话外部使用的对象中(即欢迎流程视图)。

基本组件协议定义如下

public protocol ComponentBase {
  /// This is the binding required to construct a new Component. Think of it as somewhat of an initialization value.
  associatedtype Seed = Void

  /// This should be set to the root type of object that is created.
  associatedtype Root

  associatedtype Scope: Cleanse._ScopeBase = Unscoped

  static func configure(binder: Binder<Self.Scope>)

  static func configureRoot(binder bind: ReceiptBinder<Root>) -> BindingReceipt<Root>
}

对象图的最外层组件(例如根组件)是通过 ComponentFactory 上的 build(()) 方法构建的。这定义为以下协议扩展

public extension Component {
    /// Builds the component and returns the root object.
    public func build() throws -> Self.Root
}

示例

定义子组件
struct RootAPI {
    let somethingUsingTheAPI: SomethingThatDoesAnAPICall
}

struct APIComponent : Component {
    typealias Root = RootAPI
    func configure(binder: Binder<Unscoped>) {
        // "include" the modules that create the component
        binder.include(module: PrimaryAPIURLModule.self)
        binder.include(module: SomethingThatDoesAnAPICall.Module.self)
        // bind our root Object
        binder
            .bind(RootAPI.self)
            .to(factory: RootAPI.init)
    }
}
使用组件

通过调用 binder.install(dependency: APIComponent.self),Cleanse 将在您的对象图中自动创建 ComponentFactory<APIComponent> 类型。

struct Root : RootComponent {
    func configure(binder: Binder<Unscoped>) {
        binder.install(dependency: APIComponent.self)
    }
    // ...
}

然后,您可以通过将 ComponentFactory<APIComponent> 实例注入到对象中并调用 build(()) 来使用它。

class RootViewController: UIViewController {
    let loggedInComponent: ComponentFactory<APIComponent>

    init(loggedInComponent: ComponentFactory<APIComponent>) {
        self.loggedInComponent = loggedInComponent
        super.init(nibName: nil, bundle: nil)
    }

    func logIn() {
        let apiRoot = loggedInComponent.build(())
    }
}

辅助注入

总结 (RFC #112)

辅助注入用于组合种子参数和预绑定依赖项。与子组件具有用于构建对象图的 Seed 类似,辅助注入允许您通过创建具有定义的 Seed 对象的 Factory 类型,以便通过 build(_:) 函数进行构造,从而消除样板代码。

示例

创建工厂

假设我们有一个详细信息视图控制器,它根据用户从列表视图控制器中的选择来显示特定客户的信息。

class CustomerDetailViewController: UIViewController {
    let customerID: String
    let customerService: CustomerService
    init(customerID: Assisted<String>, customerService: CustomerService) {
        self.customerID = customerID.get()
        self.customerService = customerService
    }
    ...
}

在我们的初始化程序中,我们有 Assisted<String>,它表示基于从列表视图控制器中选择的客户 ID 的辅助注入参数,以及一个预绑定依赖项 CustomerService

为了创建我们的工厂,我们需要定义一个符合 AssistedFactory 的类型来设置我们的 SeedElement 类型。

extension CustomerDetailViewController {
    struct Seed: AssistedFactory {
        typealias Seed = String
        typealias Element = CustomerDetailViewController
    }
}

一旦我们创建了 AssistedFactory 对象,我们就可以通过 Cleanse 创建工厂绑定。

extension CustomerDetailViewController {
    struct Module: Cleanse.Module {
        static func configure(binder: Binder<Unscoped>) {
            binder
              .bindFactory(CustomerDetailViewController.self)
              .with(AssistedFactory.self)
              .to(factory: CustomerDetailViewController.init)
        }
    }
}
使用我们的工厂

创建绑定后,Cleanse 将 Factory<CustomerDetailViewController.AssistedFactory> 类型绑定到我们的对象图中。因此,在我们的客户列表视图控制器中,使用此工厂可能如下所示

class CustomerListViewController: UIViewController {
    let detailViewControllerFactory: Factory<CustomerDetailViewController.AssistedFactory>

    init(detailViewControllerFactory: Factory<CustomerDetailViewController.AssistedFactory>) {
        self.detailViewControllerFactory = detailViewControllerFactory
    }
    ...

    func tappedCustomer(with customerID: String) {
        let detailVC = detailViewControllerFactory.build(customerID)
        self.present(detailVC, animated: false)
    }
}

服务提供者接口

总结 (RFC #118)

Cleanse 提供了一个插件接口,开发人员可以使用该接口挂钩到生成的对象图中,以创建自定义验证和工具。

创建插件可以通过 3 个步骤完成

1. 通过符合协议 CleanseBindingPlugin 来创建您的插件实现

您将被要求实现函数 func visit(root: ComponentBinding, errorReporter: CleanseErrorReporter),该函数将为您提供 ComponentBindingCleanseErrorReporter 的实例。

第一个参数 ComponentBinding 是根组件的表示形式,可用于遍历整个对象图。第二个参数 CleanseErrorReporter 用于在验证完成后向用户报告错误。

2. 使用 CleanseServiceLoader 实例注册您的插件

创建 CleanseServiceLoader 的实例后,您可以通过 register(_:) 函数注册您的插件。

3. 将您的服务加载器传递到 RootComponent 工厂函数中

RootComponent 工厂函数 public static func of(_:validate:serviceLoader:) 接受 CleanseServiceLoader 实例,并将运行在该对象中注册的所有插件。

注意:只有当您在工厂函数中将 validate 设置为 true 时,您的插件才会运行。

示例插件实现在上面链接的 RFC 中可用。

Binder

Binder 实例是传递给 Module.configure(binder:) 的实例,模块实现使用它来配置其提供程序。

Binder 有两个核心方法,通常会与它们交互。第一个也是更简单的一个是 install 方法。将要安装的模块的实例传递给它。它的用法如下

binder.include(module: PrimaryAPIURLModule.self)

它本质上告诉 binder 在 PrimaryAPIURLModule 上调用 configure(binder:)

Binder 公开的另一个核心方法是 bind<E>(type: E.Type)。这是配置绑定的入口点。bind 方法接受一个参数,即正在配置的元素的 元类型bind() 返回一个 BindingBuilder,必须在其上调用方法才能完成已启动的绑定的配置。

bind() 和后续的 builder 方法(不是终止方法)都用 @warn_unused_result 注解,以防止由于仅部分配置绑定而导致的错误。

bind()type 参数具有默认值,可以在某些常见情况下推断和省略。在本文档中,我们有时会显式指定它以提高可读性。

BindingBuilder 和配置您的绑定

BindingBuilder 是用于配置绑定的流畅 API。它的构建方式旨在通过代码完成来指导用户完成配置绑定的过程。BindingBuilder 的 DSL 的简化语法为

binder
  .bind([Element.self])                // Bind Step
 [.tagged(with: Tag_For_Element.self)] // Tag step
 [.sharedInScope()]                    // Scope step
 {.to(provider:) |                     // Terminating step
  .to(factory:)  |
  .to(value:)}

绑定步骤

这将启动绑定过程,以定义如何创建 Element 的实例

标签步骤 (可选)

一个可选步骤,指示提供的类型实际上应该是 TaggedProvider<Element>,而不仅仅是 Provider<Element>

参见:类型标签 以获取更多信息

作用域步骤

默认情况下,每当请求对象时,Cleanse 都会构造一个新的对象。如果指定了可选的 .sharedInScope(),则 Cleanse 将在配置它的 Component 的作用域内记住并返回相同的实例。每个 Component 都需要自己的 Scope 类型。因此,如果将其配置为 RootComponent 中的单例,则将为整个应用程序返回相同的实例。

Cleanse 为您提供了两个作用域:UnscopedSingletonUnscoped 是默认作用域,它始终构造一个新对象,而 Singleton 是为了方便而提供的,但不是必须使用的。它最常用于应用程序的 RootComponent 的作用域类型。

终止步骤

要完成绑定的配置,必须调用 BindingBuilder 上的终止方法之一。有多种方法被认为是终止步骤。下面描述了常见的步骤。

无依赖终止方法

这是一类终止方法,用于配置如何实例化不依赖于对象图中配置的其他实例的元素。

终止方法:to(provider: Provider<E>)

其他终止方法都汇集到此方法。如果 Element 的绑定以此变体终止,则当请求 Element 的实例时,将在 provider 参数上调用 .get()

终止方法:to(value: E)

这是一个便捷方法。它在语义上等同于 .to(provider: Provider(value: value)).to(factory: { value })。它将来可能会提供性能优势,但目前没有。

终止方法:to(factory: () -> E) (0 阶数)

这需要一个闭包而不是 provider,但在其他方面是等效的。等同于 .to(provider: Provider(getter: factory))

依赖请求终止方法

这就是我们定义绑定需求的方式。Dagger 2 通过查看 @Provides 方法和 @Inject 构造函数的参数在编译时确定需求。Guice 也做了类似的事情,但使用反射来确定参数。可以通过 getProvider() 方法从 Guice 的 binder 显式请求依赖项。

与 Java 不同,Swift 没有注解处理器在编译时执行此操作,也没有稳定的反射 API。我们也不想公开类似 getProvider() 的方法,因为它允许用户执行危险的操作,并且还会丢失关于哪些 provider 依赖于其他 provider 的重要信息。

但是,Swift 确实有一个非常强大的泛型系统。我们利用这一点在创建绑定时提供安全性和简洁性。

终止方法:to<P1>(factory: (P1) -> E) (1 阶数)

这将 E 的绑定注册到工厂函数,该函数接受一个参数。

它是如何工作的

假设我们有一个汉堡包定义为

struct Hamburger {
   let topping: Topping
   // Note: this actually would be created implicitly for structs
   init(topping: Topping) {
     self.topping = topping
   }
 }

当引用初始化程序而不调用它时(例如 let factory = Hamburger.init),表达式的结果是 函数类型

(topping: Topping) -> Hamburger

因此,在模块中配置其创建时,调用

binder.bind(Hamburger.self).to(factory: Hamburger.init)

将导致调用 .to<P1>(factory: (P1) -> E) 终止函数,并将 Element 解析为 Hamburger,将 P1 解析为 Topping

to(factory:) 的伪实现

public func to<P1>(factory: (P1) -> Element) {
  // Ask the binder for a provider of P1. This provider
  // is invalid until the component is constructed
  // Note that getProvider is an internal method, unlike in Guice.
  // It also specifies which binding this provider is for to
  // improve debugging.
  let dependencyProvider1: Provider<P1> =
      binder.getProvider(P1.self, requiredFor: Element.self)

  // Create a Provider of Element. This will call the factory
  // method with the providers
  let elementProvider: Provider<Element> = Provider {
      factory(dependencyProvider1.get())
  }

  // Call the to(provider:) terminating function to finish
  // this binding
  to(provider: elementProvider)
}

由于依赖 provider 的请求发生在配置时,因此对象图在配置时知道所有绑定和依赖项,并将快速失败。

终止方法:to<P1, P2, … PN>(factory: (P1, P2, … PN) -> E) (N 阶数)

好吧,我们可能需要多个需求才能构造给定的实例。Swift 中没有 可变参数泛型。但是,我们使用了一个小脚本来生成各种阶数的 to(factory:) 方法。

集合绑定

有时希望将同一类型的多个对象提供到一个集合中。一个非常常见的用法是将拦截器或过滤器提供给 RPC 库。在应用程序中,可能希望添加到标签栏控制器的视图控制器集合中,或设置页面中的设置。

此概念在 DaggerGuice 中被称为多重绑定

提供给 Set 或 Dictionary 不是不需要的功能,并且可能会构建为在提供给 Arrays 之上的扩展。

将元素绑定到集合与标准 绑定步骤 非常相似,但增加了一个步骤:在 builder 定义中调用 .intoCollection()

binder
  .bind([Element.self])                // Bind Step
  .intoCollection()   // indicates that we are providing an
                    // element or elements into Array<Element>**
 [.tagged(with: Tag_For_Element.self)]   // Tag step
 [.asSingleton()]                        // Scope step
 {.to(provider:) |                       // Terminating step
  .to(factory:)  |
  .to(value:)}

此 builder 序列的 终止步骤 可以是单个 ElementElement 数组的工厂/值/provider。

属性注入

在某些情况下,用户不控制对象的构造,但依赖注入被认为是很有用的。其中一些更常见的情况是

Cleanse 为此提供了一个解决方案:属性注入(在 Guice 和 Dagger 中称为成员注入)。

在 Cleanse 中,属性注入在设计上是二等公民。工厂/构造函数注入应尽可能使用,但在不可能的情况下可以使用属性注入。属性注入具有类似于 BindingBuilder 的 builder 语言

binder
  .bindPropertyInjectionOf(<metatype of class being injected into>)
  .to(injector: <property injection method>)

终止函数有两种变体,一种是签名

(Element, P1, P2,  ..., Pn) -> ()

另一种是

(Element) -> (P1, P2, ..., Pn) -> ()

前者允许简单的注入方法,这些方法不是实例方法,例如

binder
  .bindPropertyInjectionOf(AClass.self)
  .to {
     $0.a = ($1 as TaggedProvider<ATag>).get()
  }

binder
  .bindPropertyInjectionOf(BClass.self)
  .to {
      $0.injectProperties(superInjector: $1, b: $2, crazyStruct: $3)
  }

可以使用的后一种类型的注入方法 (Element -> (P1, P2, …, Pn) -> ()) 在引用目标上的瞬时方法进行注入时很方便。

假设我们有

class FreeBeer {
  var string1: String!
  var string2: String!

  func injectProperties(
    string1: TaggedProvider<String1>,
    string2: TaggedProvider<String2>
  ) {
    self.string1 = string1.get()
    self.string2 = string2.get()
  }
}

可以通过执行以下操作来绑定 FreeBeer 的属性注入

binder
  .bindPropertyInjectionOf(FreeBeer.self)
  .to(injector: FreeBeer.injectProperties)

表达式 FreeBeer.injectProperties 的结果类型是 FreeBeer -> (TaggedProvider<String1>, TaggedProvider<String2>) -> ()

在绑定 Element 的属性注入器之后,用户将能够在工厂参数中请求类型 PropertyInjector<Element>。这有一个定义为

func injectProperties(into instance: Element)

的单个方法,然后将对 Element 执行属性注入。

注意: 非旧版 API 中的属性注入器不知道类层次结构。如果用户希望属性注入在类层次结构中级联,则绑定的注入器可以为父类调用 inject 方法,或者请求 PropertyInjector<Superclass> 作为注入器参数并使用它。

高级设置

我们可以通过 属性注入 使 App Delegate 成为 Cleanse 对象图的根。我们必须在此处使用属性注入,因为我们不控制 app delegate 的构造。现在,我们可以将我们的“根”建模为 PropertyInjector<AppDelegate> 的实例,然后使用此对象将属性注入到我们已构造的 App Delegate 中。

让我们从重新定义 RootComponent 开始

extension AppDelegate {
  struct Component : Cleanse.RootComponent {
    // When we call build() it will return the Root type, which is a PropertyInjector<AppDelegate>.
    // More on how we use the PropertyInjector type later.
    typealias Root = PropertyInjector<AppDelegate>

    // Required function from Cleanse.RootComponent protocol.
    static func configureRoot(binder bind: ReceiptBinder<PropertyInjector<AppDelegate>>) -> BindingReceipt<PropertyInjector<AppDelegate>> {
        return bind.propertyInjector(configuredWith: { bind in
            bind.to(injector: AppDelegate.injectProperties)
        })
    }

    // Required function from Cleanse.RootComponent protocol.
    static func configure(binder: Binder<Unscoped>) {
        // Binding go here.
    }
  }
}

在我们的 app delegate 内部,我们添加函数 injectProperties

func injectProperties(_ window: UIWindow) {
  self.window = window
}

现在要连接我们的新根对象,我们可以在 app delegate 中对自身调用 injectProperties(:)

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Build our component, and make the property injector
    let propertyInjector = try! ComponentFactory.of(AppDelegate.Component.self).build(())

     // Now inject the properties into ourselves
    propertyInjector.injectProperties(into: self)

    window!.makeKeyAndVisible()

    return true
}

现在运行应用程序将产生一个新的错误,提示缺少 UIWindow 的提供者,但是在绑定 UIWindow 及其依赖项的实例后,我们应该一切顺利!

extension UIWindow {
  struct Module : Cleanse.Module {
    public func configure(binder: Binder<Singleton>) {
      binder
        .bind(UIWindow.self)
        // The root app window should only be constructed once.
        .sharedInScope()
        .to { (rootViewController: RootViewController) in
          let window = UIWindow(frame: UIScreen.mainScreen().bounds)
          window.rootViewController = rootViewController
          return window
        }
    }
  }
}

贡献

我们很高兴您对 Cleanse 感兴趣,我们很乐意看到您将其发展到什么程度。

对 master Cleanse 仓库的任何贡献者都必须签署 个人贡献者许可协议 (CLA)。这是一个简短的表格,涵盖了我们的基础知识,并确保您有资格做出贡献。

许可证

Apache 2.0