Guise 11

Guise 是一个灵活的、依赖性极小的 Swift 依赖注入框架。

Guise 比其他框架好在哪里?

基本文档

注册

要将依赖项注册到容器,需要四个要素:要注册的类型,注册时使用的标签,解析注册所需的参数,以及注册的生命周期。前三个要素唯一地标识一个注册。我们将在下面讨论第四个要素,生命周期。

让我们从这些要素开始逐一介绍。

类型

let container = Container()
container.register(Service.self) { _ in
  Service()
}

register 的第一个参数告诉 Guise(和 Swift)我们要注册的类型,最后传递的 block 告诉 Guise 如何构造该类型。(暂时忽略 _ 参数。)

由于我们要注册的类型与 block 的返回类型相同,因此我们可以省略 register 的第一个参数

container.register { _ in Service() }

依赖注入的主要目的之一是定位抽象接口的实现,以便可以在单元测试中或其他目的中进行替换。事实上,这就是这个框架被称为 Guise 的原因。

Guisen. 一种外在的形式、外观或呈现方式,通常掩盖事物的真实本质。

protocol Service {
  func performService() async
}

class ConcreteService: Service {
  // Perhaps this one talks to a real database
}

class TestService: Service {
  // This is the one we want in unit tests
}

let container = Container()
container.register(Service.self) { _ in
  ConcreteService()
}

请注意,在此示例中,block 返回 ConcreteService,但注册的类型是 Service。另一种表达方式是

container.register { _ in ConcreteService() as Service }

稍后,当我们解析此注册时,我们将请求 Service,而不是 ConcreteService

对于简单的注册,Guise 有一个方便的重载,它使用 @autoclosure

container.register(instance: ConcreteService() as Service)

标签

标签可以帮助定位、描述和消除注册的歧义。标签可以是任何既是 Hashable 又是 Sendable 的类型,例如 StringUUIDInt 或您自己创建的自定义类型。

enum Types: Hashable, Sendable {
  case plugin
}

container.register(Plugin.self, tags: Types.plugin, 1, instance: PluginImpl1())

标签的顺序并不重要,可以使用任意数量的标签。标签被收集到 Set<AnySendableHashable> 中,因此重复没有效果。

由于注册中使用的标签是标识该注册的唯一 Key 的一部分,因此解析时必须使用相同的标签

let plugin: Plugin = try container.resolve(Plugin.self, tags: 1, Types.plugin) 

标签的顺序与注册时不同,但这没有关系。

当解析依赖项数组时,标签变得非常强大。在这种情况下,只需要指定标签的子集。请参阅本 README 解析部分中关于数组的讨论。

参数

通常需要在解析时将一些状态传递给依赖项。Guise 支持任意数量的参数。

class Service {
  let id: Int
  let state: String
}

container.register { _, id, state in
  Service(id: id, state: state)
}

解析时必须提供这些参数,否则 Guise 将抛出错误

let service: Service = try container.resolve(args: 1, "foo")

生命周期

Guise 支持两种生命周期:瞬态和单例。瞬态是默认值。

在瞬态注册中,每次都创建一个新的依赖项实例并返回。换句话说,调用作为最后一个参数传递给 register 的解析工厂,传递参数,并将其结果返回给调用者。

在单例注册中,工厂在第一次被调用,但每次后续的对该注册的请求都会返回相同的实例。

container.register(lifetime: .singleton, instance: Service())

传递依赖

依赖注入的主要功能之一是定位和解析复杂的依赖关系层次结构。Guise 也可以做到这一点。每个解析 block 的第一个参数是 Resolver 的一个实例,它允许定位和解析注册。

class Database {}
class Service {
  let database: Database
  init(database: Database) {
    self.database = database
  }
}

container.register(lifetime: .singleton, instance: Database())
container.register(lifetime: .singleton) { r in
  Service(database: try r.resolve())
}

每当我们解析 Service 时,其构造函数中的 Database 参数将被定位和解析。

这种模式非常常见,因此 Guise 有一个高阶函数 auto,它可以处理任意数量的依赖项

container.register(lifetime: .singleton, factory: auto(Service.init))

为了使用 auto,子依赖项不能有任何标签或工厂参数。

解析

解析会查找并实例化依赖项,给定其类型、标签和工厂参数,并考虑到其生命周期。

解析比注册更简单,因此几个示例就足够了

class Service {}
container.register(instance: Service())

// Two alternate ways to resolve the above registration
let service: Service = try container.resolve()
let service = try container.resolve(Service.self)

container.register(tags: 2, instance: Service())
let service: Service = try container.resolve(tags: 2)

class Something {
  let id: Int

  init(id: Int) { self.id = id }
}

container.register { _, id in
  Something(id: id)
}
let something = try container.resolve(Something.self, args: 7)

可选解析

Guise 可以将 optional 解析为包装类型

class Service {}
container.register(instance: Service())
let service: Service? = try container.resolve()

幕后发生的事情是 Guise 首先查找确切的注册,即类型为 Service? 的注册。如果找不到,则尝试解析 Service

当解析 optional 时,如果找不到注册,Guise 会返回 nil 而不是抛出错误。

数组解析

想象一个插件架构,我们希望定位和解析同一类型的多个实例。

protocol Plugin {}

container.register(Plugin.self, tags: UUID(), instance: Plugin1())
container.register(Plugin.self, tags: UUID(), instance: Plugin2())
container.register(Plugin.self, tags: UUID(), instance: Plugin3())

我们可以很容易地获得所有这些插件

let plugins: [Plugin] = try container.resolve()

与可选解析一样,Guise 首先查找此注册的确切匹配项。如果没有找到,它会注意到这是在尝试解析数组。然后,它定位类型为 Plugin 的所有注册,并尝试解析它们。如果任何一个失败,则全部失败。

解析数组时,标签的处理方式不同。 Guise 查找包含所有给定标签的所有注册。

container.register(Plugin.self, tags: "type1", UUID(), instance: Plugin1())
container.register(Plugin.self, tags: "type1", UUID(), instance: Plugin2())
container.register(Plugin.self, tags: "type2", UUID(), instance: Plugin3())
container.register(Plugin.self, tags: "type2", UUID(), instance: Plugin4())

在这里,我们注册了四个插件。每个插件都用一个匿名的 UUID 进行区分,并分为两种类型:类型 1 和类型 2。要获取所有类型 1 的注册…

let plugins: [Plugin] = try container.resolve(tags: "type1")

这会得到 Plugin1Plugin2,但不会得到 Plugin3Plugin4。当然,我们仍然可以通过以下咒语获得所有四个插件

let plugins: [Plugin] = try container.resolve()

如果未找到任何注册,Guise 默认返回一个空数组,而不是抛出错误。

延迟解析

有时需要依赖尚未准备好的服务。或者我们希望防止循环依赖,因为两个服务相互依赖。

解决此问题的一种方法是传递 Resolver 本身的一个实例

class Service {
  weak var resolver: Resolver! 

  init(resolver: Resolver) {
    self.resolver = resolver
  }

  func performService() throws {
    let database = try resolver.resolve(Database.self)
    database.doSomething()
  }
}

上述模式的问题在于它打破了依赖注入的基本规则之一:使依赖项显式化。Service 的用户必须阅读源代码才能知道它有哪些其他依赖项。这使得该类更难使用,更难测试。

Guise 使用延迟解析器解决了这个问题。

class Service {}
container.register(tags: "s", instance: Service())

let lr: LazyResolver<Service> = try container.resolve()

延迟解析器不需要注册。 Guise 会根据需要自动构造它们。延迟解析器解析 Service 类型的依赖项。标签和参数在解析时指定

let service = lr.resolve(tags: "s")

异步

Guise 支持 async 注册和解析。

class Service {
  let database: Database

  init(database: Database) async {
    self.database = database
    await database.setup()
  }
}

container.register { r in
  try await Service(database: r.resolve())
}
let service = try await container.resolve(Service.self)

任何同步注册都可以异步解析,但反之则不然。 默认情况下,如果尝试在同步上下文中解析 async 注册,Guise 会抛出 .requiresAsync

程序集 (Assemblies)

在具有许多模块的复杂应用程序中,组织从模块导出的注册会很有帮助。 Guise 为此目的提供了程序集

class AwesomeAssembly: Assembly {
  func register(in registrar: any Registrar) {
    registrar.register(assemblies: CoolAssembly())
    registrar.register(lifetime: .singleton: instance: Service())
  }

  // This method is optional. A default implementation
  // is provided, which does nothing.
  func registered(to resolver: any Resolver) {
    do {
      let service = try resolver.resolve(Service.self)
      service.configure()
    } catch {
      // Handle error
    }
  }
}

程序集组织在一个层次结构中。程序集应使用 register(assemblies:) 注册其依赖程序集。

程序集由其类型键控,因此添加同一程序集两次不会导致双重注册。

组装的行为首先创建一个有序的程序集集合,即按首次注册顺序排列的没有重复项的列表。 然后它遍历此列表并在每个程序集上调用 register(in:)。 之后,它遍历列表并在每个程序集上调用 registered(to:)

registered(to:) 的目的是在注册依赖项后执行额外的初始化,而无需将依赖项暴露在程序集外部。

注册所有依赖项后,在容器上调用 assemble 以使一切正常工作。 可以将其他程序集作为参数传递给 assemble,或者如果它们已经注册,则可以不带参数调用它。

container.assemble(AwesomeAssembly(), CoolAssembly())

或者

container.register(assemblies: AwesomeAssembly(), CoolAssembly())
container.assemble()

嵌套容器

Guise 支持嵌套的 Container。 构造 Container 时,只需在构造函数中传递其父容器

let parent = Container()
let child = Container(parent: parent)

解析时,如果在子容器中找不到条目,则会搜索父容器。 对于匹配的 Key,子条目始终覆盖父条目。 相反的情况则不是:父容器不知道其子容器。 直接搜索父容器不会发现任何子条目。

嵌套容器 & 程序集

程序集只是一种批量进行大量注册的方法。 如果容器找不到注册,它会搜索其父容器。 程序集则不然。 程序集特定于容器。

let parent = Container()
parent.assemble(CoolAssembly())
let child = Container(parent: parent)
child.assemble(AwesomeAssembly())