Guise 是一个灵活的、依赖性极小的 Swift 依赖注入框架。
throw
抛出异常和 async
异步初始化和解析Injectable
或 Component
这样的特殊接口需要实现。不需要添加特殊的初始化方法或属性。任何类型都可以直接注册。要将依赖项注册到容器,需要四个要素:要注册的类型,注册时使用的标签,解析注册所需的参数,以及注册的生命周期。前三个要素唯一地标识一个注册。我们将在下面讨论第四个要素,生命周期。
让我们从这些要素开始逐一介绍。
let container = Container()
container.register(Service.self) { _ in
Service()
}
register
的第一个参数告诉 Guise(和 Swift)我们要注册的类型,最后传递的 block 告诉 Guise 如何构造该类型。(暂时忽略 _
参数。)
由于我们要注册的类型与 block 的返回类型相同,因此我们可以省略 register
的第一个参数
container.register { _ in Service() }
依赖注入的主要目的之一是定位抽象接口的实现,以便可以在单元测试中或其他目的中进行替换。事实上,这就是这个框架被称为 Guise 的原因。
Guise,n. 一种外在的形式、外观或呈现方式,通常掩盖事物的真实本质。
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
的类型,例如 String
、UUID
、Int
或您自己创建的自定义类型。
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")
这会得到 Plugin1
和 Plugin2
,但不会得到 Plugin3
和 Plugin4
。当然,我们仍然可以通过以下咒语获得所有四个插件
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
。
在具有许多模块的复杂应用程序中,组织从模块导出的注册会很有帮助。 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())