Guise 是一个优雅、灵活、类型安全的 Swift 依赖项解析框架。
- 灵活的依赖项解析,可选缓存
- 优雅、直观的注册
- 线程安全
- 简化单元测试
- 支持容器、命名依赖项和任意类型
- 在解析时传递任意状态
- 类型安全的
KeyPath
注入 - 惰性解析
- 支持任意元数据
- Swift 5.x(在 Swift 4.x 中使用 v8.0)
- 支持 iOS 8.0+、macOS 10.9+、watchOS 2+、tvOS 9+
Guise比其他框架好在哪?
- Guise不需要对您注册的类型进行任何修改。没有像
Injectable
或Component
这样要实现的特殊接口。也没有要添加的特殊初始化器或属性。任何类型都可以直接注册。 - Guise是为Swift设计的。其他Swift的DI框架似乎是其他语言(特别是C♯和Java)框架的翻译。这些语言有不同的优缺点,这些优缺点在框架的设计中得到了体现。这使得它们在Swift中显得笨拙。
- 这些框架中的许多直接注册 类型。Guise直接注册 块,间接注册 类型。这个简单的区别减少了大量的复杂度,同时增加了编译时的安全性。当与Swift的
@autoclosure
属性结合使用时,它可以使注册变得优雅且最小化。(请参阅有关工厂和实例注册的部分。) - Guise被设计得简洁而非简单,实际上却两者兼具。
展示
这里快速展示了Guise能做什么以及它是如何做到的。
注册
// The `factory` parameter is an @autoclosure.
Guise.register(factory: Implementation() as Plugin)
// The `instance` parameter is also an @autoclosure. Guise is lazy wherever possible.
Guise.register(instance: Singleton() as Service)
// Register a block directly and parameterize it.
Guise.register{ (x: Int) in Something(x: x) }
// Use a name to disambiguate registrations. (Otherwise the last one wins.)
// A name can be any `Hashable` type.
Guise.register(factory: Implementation1() as Plugin, name: Name.implementation1)
Guise.register(factory: Implementation2() as Plugin, name: Name.implementation2)
class Controller {
var service: Service!
init(something: Something, plugin: Plugin) {
// blah blah
}
}
// Initializer injection
Guise.register{ (resolver: Resolving) in
// The types of the parameters determine what is resolved.
return Controller(something: resolver.resolve()!, plugin: resolver.resolve(name: Name.implementation2)!)
}
// Register Controller's `service` property for injection.
Guise.into(injectable: Controller.self).inject(\.service).register()
解析
// Use the many overloads of `resolve` to resolve dependencies.
let plugin1 = Guise.resolve(type: Plugin.self, name: Name.implementation1)!
let plugin2: Plugin = Guise.resolve(name: Name.implementation2)!
let controller = Guise.resolve()! as Controller
// Resolve KeyPath injections
Guise.resolve(into: controller)
重要变更
Guise 9.1 引入了 Swift Package Manager 的支持。
心理准备
为了简洁,本文档假设您了解依赖解析和注入。它还假设您对 Swift 语言有深入的了解。特别是:泛型、闭包、自动闭包、值类型和引用类型的区别以及闭包的捕获语义。
用法样式
Guise 可以用两种不同的方式使用。最简单的方式是使用 Guise
类的静态方法,这正是本文档所采用的方法。
另一种方式是创建 Resolver
类的实例,并使用其实例方法。例如,可以这样说法:
Guise.register{ Plink() }
或者可以说:
let resolver = Resolver()
resolver.register { Plink() }
注册
注册是告诉 Guise 如何创建或定位依赖的行为。有许多方法可以完成这项任务,但我们只介绍四个最重要的。
工厂注册
在工厂注册中,我们告诉Guise,在解析时,我们总是想要已注册的任何东西的新实例。
Guise.register(factory: StringFormatter())
factory
参数是一个@autoclosure
,因此它是延迟评估的。实际上,因为一个@autoclosure
是一个代码块,所以它被简单地存储起来以便稍后执行,并在每次我们向Guise请求StringFormatter
时调用。
let formatter: StringFormatter = Guise.resolve()!
实例注册
在实例注册中,我们告诉Guise,在解析时,我们总是想要已注册的任何东西的相同实例。
Guise.register(instance: Api())
instance
参数是一个@autoclosure
,所以它也会被存储起来以便稍后评估。但是,它只会在请求Guise的第一个Api
实例时调用一次。之后,将返回相同的实例。
如果注册的类型是引用类型,Guise将返回相同的引用。如果是值类型,我们将获得一个副本,但初始化程序将不会再次被调用。
实际上是,实例注册在解析器中创建了一个单例。
代码块注册
代码块注册是构建其他所有注册类型的基础。然而,它的语法比其他注册形式更为复杂,因此除非在特殊情况下,否则应避免使用。
如果需要在解析时传递参数或者希望在解析时执行更复杂的逻辑,代码块注册是有用的。
Guise.register{ (x: Int) in Something(x: x) }
let something: Something = Guise.resolve(parameter: 3)!
代码块注册可以接受零个或一个参数。如果需要将更复杂的状态传递给代码块,请使用结构化类型,如结构体、元组或枚举。
如果代码块的单一参数是Resolving
的类型,解析器将在解析时自动传递到代码块中。不需要明确传递它。
Guise.register{ (resolver: Resolving) in
Api(database: resolver.resolve()!)
}
let api = Guise.resolve()!
当调用let api = Guise.resolve()!
时,当前解析器将自动传递到代码块的resolver
参数中。
弱引用注册
当缓存时,Guise总是保留对正在缓存的内容的强引用。由于Guise解析器通常在整个应用程序生命周期中存在,因此在注册如视图控制器这类瞬态实体时可能会出现问题。
解决方案是弱引用注册。与其他注册方式不同,weak:
参数不是@autoclosure
。
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Guise.register(weak: self)
}
}
这里我们使用Guise注册了一个MyViewController
实例。在解析弱引用时,切勿强制展开
guard let myViewController: MyViewController = Guise.resolve() else {
return
}
抽象
Guise,名词。指外部的形态、外观或呈现方式,通常掩盖了事物的真实本质。
依赖解析器的一个功能是找到依赖项。另一个功能是抽象它们,以便可以用替代实现进行替换,无论是在单元测试还是其他地方。这通常通过协议实现。
protocol DatabaseLayer {
func retrieveSomething() -> [Something]
}
class Database: DatabaseLayer {
func retrieveSomething() -> [Something] {
return connection.retrieve("SELECT * FROM something")
}
}
Guise.register(instance: Database() as DatabaseLayer)
这里的魔法是as DatabaseLayer
。这使得注册的类型成为DatabaseLayer
而不是Database
。在解析此注册时,我们必须要求一个DatabaseLayer
,而不是一个Database
。
let database: DatabaseLayer = Guise.resolve()!
由于我们不想在单元测试中与真实数据库通信,我们可以现在在我们的单元测试中使用DatabaseLayer
的不同实现。
名称
如果我们两次使用Guise注册相同的类型,第二次注册将静默覆盖第一次。
Guise.register(instance: Database() as DatabaseLayer)
Guise.register(instance: FakeDatabase() as DatabaseLayer)
由于这两个都注册了DatabaseLayer
,第二个覆盖了第一个。为了区分这些,我们可以使用一个名称。
enum Name {
case real
case fake
}
Guise.register(instance: Database() as DatabaseLayer, name: Name.real)
Guise.register(instance: FakeDatabase() as DatabaseLayer, name: Name.fake)
任何Hashable
类型——包括String
——都可以用作名称。我建议使用枚举值。
在解析时必须传递名称,否则注册将找不到。
let database: DatabaseLayer = Guise.resolve(name: Name.real)!
容器
容器允许将注册分组在一起。容器只是另一种类型的名称,并且就像名称一样,它可以是任何Hashable
类型。
enum Container {
case plugins
}
Guise.register(factory: Plugin1() as Plugin, name: Plugin.plugin1, container: Container.plugins)
Guise.register(factory: Plugin2() as Plugin, name: Plugin.plugin2, container: Container.plugins)
Guise.register(factory: Plugin3() as Plugin, name: Plugin.plugin3, container: Container.plugins)
这里我们进行了三个 Plugin
注册,每个都使用不同的实现。它们通过名称进行区分,并放置在 Container.plugins
中。
当解析容器中的注册时,必须提及容器名称,否则注册将无法找到。
let plugin: Plugin = Guise.resolve(name: Plugin.plugin1, container: Container.plugins)!
虽然容器可以被用于任何您希望的目的,但它们的主要目的是允许成批丢弃一组注册。
Guise.unregister(container: Container.plugins)
键
所有的 register
重载都返回一个 Key<RegisteredType>
实例。注册类型是注册块的返回类型。键的其他元素包括注册名称和注册容器。所有注册都有名称和容器,即使未明确提及。默认名称是 Guise.Name.default
。默认容器是 Guise.Container.default
。
注册类型、名称和容器的组合创建了键,并使注册唯一。其他信息,如缓存、元数据和使用的 register
重载,都不会对此产生影响。
Guise.register(instance: Foo())
Guise.register(factory: Foo())
由于上述两个注册在相同的容器中使用相同的名称注册了相同的类型,第二个注册会静默地覆盖第一个。
键可用于解析或丢弃注册、过滤它们等。在大多数情况下,没有必要保存注册键,因为它们可以在任何时候轻松构造。
let key = Key<Plugin>(name: Plugin.plugin1, container: Container.plugins)
键有两种类型:Key<RegisteredType>
和 AnyKey
。前者是最常见的。后者虽然类型擦除,但仍包含对注册类型的内部引用。当需要非同质的键列表时使用。
解析
许多解析的示例已经看到了。解析本身比注册简单。
如果无法找到注册,resolve
重载将返回 nil
。这包括已经被归零的弱注册。如果您的程序在没有注册依赖项的情况下无效,您应该在使用解析时强制展开。
let database: DatabaseLayer = Guise.resolve()!
这是最常见的情况。然而,如果注册仅作为可选项注册或为弱注册,应在使用之前检查注册是否为 nil
。
if let logger: XCGLogger = Guise.resolve() {
logger.error("Something bad happened.")
}
缓存
Guise具有缓存解析块调用结果的能力,可以简单地重复使用该结果,而不是再次调用块。一些register
重载提供了cached:
参数,允许用户选择是否执行缓存。其他register
重载隐式地总是缓存或从不缓存。
所有的resolve
重载都提供了一个cached:
参数,允许调用者覆盖在创建注册时所做的缓存决策。然而,这个参数只适用于那些具有显式cached:
参数的register
重载。(这是Guise之前版本的一个破坏性变更,其中缓存可以在解析时始终被覆盖。)
这意味着工厂注册总是不缓存的,而实例和弱注册总是缓存的。
Guise.register(instance: Singleton())
let singleton: Singleton = Guise.resolve(cached: false)!
在上面的示例中,由于register(instance:)
不允许进行非缓存注册,因此忽略了cached
参数。
Guise.register(cached: true) { Singleton() }
let singleton: Singleton = Guise.resolve(cached: false)!
在这个示例中,块注册允许注册者显式决定是否应该执行缓存,因此可以覆盖。在第二行,即使已经存在缓存的实例,也会再次调用解析块。
初始化器注入
依赖解析器最重要的功能之一是组合依赖关系。实现这一点最简单、最直接的方法是使用初始化器注入。在这个模式中,依赖项的依赖关系在其注册时注入到其初始化器中。
struct Api {
init(database: DatabaseLayer) {}
}
Guise.register(instance: Database() as DatabaseLayer)
Guise.register(instance: Api(database: Guise.resolve()!))
元数据
可以给任何注册附加任意元数据。这些元数据可以是任何类型。
Guise.register(factory: Plugin1() as Plugin, metadata: PluginMetadata(granularity: 4))
元数据主要用于过滤。它允许调用者在实例化之前找到注册项并查询其功能。想象一个拥有大量插件的应用程序,这些插件由于可能持有和处理外部资源而难以实例化。我们希望能够找到插件并查询其功能,然后再决定是否实例化它。这就是元数据让我们能够做到的。
有关更多信息,请参阅过滤部分。
过滤
过滤使用各种标准查找注册项,但不解析它们。
let pluginRegistrations = Guise.filter(type: Plugin.self, container: Container.plugins)
此过滤器正在查找Container.plugins
中所有类型为Plugin
的注册项,无论名称如何。大多数过滤方法的返回类型是[Keyed: Registration]
,其中Keyed
是Key<RegisteredType>
或AnyKey
。
尽管 filter
载荷直接返回 Registration
实例,但最好不要尝试使用这些来解析。相反,将键传递给一个接受一个或多个键的 resolve
载荷。
let plugins = Guise.resolve(keys: pluginRegistrations.keys)
除了类型、名称和容器之外,还可以查询元数据。
let pluginRegistrations = Guise.filter(type: Plugin.self, container: Container.plugins) { (metadata: PluginMetadata)
metadata.granularity > 2
}
这返回了在 Container.plugins
中类型为 Plugin
的所有注册项,其元数据类型为 PluginMetadata
,其“粒度”大于 2。
如果元数据实现了 Equatable
并且需要进行相等的比较,存在一个方便的覆盖。
let pluginRegistrations = Guise.filter(type: Plugin.self, container: Container.plugins, metadata: PluginMetadata(granularity: 2))
这返回了在 Container.plugins
中类型为 Plugin
的所有注册项,其元数据 == PluginMetadata(granularity: 2)
。
KeyPath
注射
尽可能的时候,应该使用如上所述的初始化器注入来注入依赖,但是有时候我们无法控制类型的实例化。这尤其适用于从故事板等实例化的视图控制器。
在这种情况下,Guise 提供了一种名为 KeyPath
注射的强大技术。在 KeyPath
注射中,第一步是将依赖指定为应将它们注入的类型上的属性。
class MyViewController: UIViewController {
var api: Api!
var database: DatabaseLayer!
}
由于这些依赖在 MyViewController
已经实例化后才会解析,因此它们应该是可选的,正如这里所示。
这些属性 不能 是私有的。否则,Swift 将不会在正确的范围内为它们生成 KeyPath
常量,而且 Guise 将无法找到它们。依赖应该是明确的,不管是作为初始化器参数还是作为依赖于它们的类型的属性暴露。
KeyPath
注射的下一步是在所有其他应用程序加载之前,在 AppDelegate
中注册一些依赖关系,以便它们可以被解析。
Guise.register(instance: Database() as DatabaseLayer)
Guise.register(instance: Api(database: Guise.resolve()!))
在 AppDelegate
中,这样做是最好的位置。
接下来,我们告诉 Guise 如何将 MyViewController
的属性与已注册的依赖项关联起来。Swift 的类型系统在这里非常有帮助,因为 keypaths 是类型安全的。
Guise.into(injectable: MyViewController.self).inject(\.api).inject(\.database).register()
简单地说,这意味着,“对于类型 MyViewController
,属性 api
和 database
分别由相应类型的注册项满足。”换句话说,因为 \MyViewController.api
的 KeyPath
是类型 KeyPath
,所以它可以通过找到类型为 Api
的注册项来解决。
最后一步是解决注入。
class MyViewController: UIViewController {
// These two ivars are "hydrated" by Guise.resolve(into:).
var api: Api!
var database: DatabaseLayer!
override func viewDidLoad() {
super.viewDidLoad()
// This line gives values to the api and database ivars above.
Guise.resolve(into: self)
}
}
Guise.resolve(into: self)
这行代码为 MyViewController
的 api
和 database
属性提供了值。
抽象
当使用 KeyPath
注射时,一种最佳实践是创建一个包含将要注入的属性的协议,而不是使用具体类型。由于 KeyPath
注射仅适用于引用类型,因此必须将协议标记为引用类型,使用 : class
。
protocol UsesApi: class {
var api: Api! { get set }
}
protocol UsesDatabaseLayer: class {
var database: DatabaseLayer! { get set }
}
class MyViewController: UIViewController, UsesApi, UsesDatabaseLayer {
var api: Api!
var database: DatabaseLayer!
override func viewDidLoad() {
super.viewDidLoad()
Guise.resolve(into: self)
}
}
class AppDelegate {
func applicationDidFinishLaunching() {
Guise.register(instance: Database() as DatabaseLayer)
Guise.register(instance: Api(database: Guise.resolve()!))
// Without ": class" on the protocols above, the following two lines
// will not compile.
Guise.into(UsesApi.self).inject(\.api).register()
Guise.into(UsesDatabaseLayer.self).inject(\.database).register()
}
}
因为MyViewController
实现了UsesApi
和UsesDatabaseLayer
,所以当调用resolve(into:)
时,其依赖项将会得到适当解决。
名称和容器
注入支持名称和容器。
Guise.register(instance: Database() as DatabaseLayer, name: Name.real)
Guise.into(injectable: UsesDatabase.self).inject(\.database, name: Name.real).register()
这表示当满足UsesDatabase
的database
依赖项时,必须使用命名为Name.real
的注册。
显式注入
与底层的普通注册一样,KeyPath
注入也是如此。可以使用一个块来进行注入。
protocol Arbiter {
var rank: Int { get }
var judge: Judge? { get set }
}
Guise.into(injectable: Arbiter.self).inject { (target, resolver) in
if target.rank < 7 { return target }
target.judge = resolver.resolve()
return target
}
显式注入传递两个参数。第一个是目标类型本身的实例,即target
是Arbiter
的实例。第二个参数是当前 resolving 实例。注入块必须始终返回其目标。如果是引用类型,则必须返回相同的引用。你不应该创建这个类型的新实例。对于值类型,虽然没有这种限制,但必须返回相同类型的实例。另外,虽然不是绝对必要的,但强烈建议使用传入的 resolver 而不是外部 resolver 来解决任何依赖关系。
去势伪装
使用Guise.resolve(into:)
有点问题。这里在我们的一个类型中有一个对Guise的显式引用。这最好避免,因为它会创建对resolving器自身的依赖。如果我们想在应用中使用Guise,但不希望在单元测试中用,那么出路是使用ImpotentResolver
。
// Somewhere in our unit tests, before they run.
Guise.resolver = ImpotentResolver()
在这里,我们用ImpotentResolver
替换了Guise的默认resolve实。器ImpotentResolver
不执行任何操作。忽略所有注册。所有解决返回nil
。resolve(into:)
不执行任何操作。现在我们必须在单元测试这个控制器时显式设置我们的依赖项。
let controller = MyViewController()
controller.database = FakeDatabase()
controller.api = Api(database: controller.database)
_ = controller.view
当调用controller.view
时,会隐式调用viewDidLoad
,但由于我们使用了ImpotentResolver
,Guise.resolve(into: self)
已经变成了一个no-op(无操作)。
高级Guise
Guise是一个功能强大的框架,具有许多特性。并非所有这些特性在本文中都有讨论。请查阅代码和单元测试以了解更多。Guise被设计得更加简单而不是容易。它是一个相当底层的框架,旨在作为其他框架的基础。例如,我计划创建一个带有自定义转场等功能的UI框架,这样就可以在视图控制器中无需显式调用 Guise.resolve(into:)
就可以解决依赖项。这个框架的创建超出Guise的范围,但将需要Guise的底层功能来执行其工作。我是个很忙的人,所以不要期望这个框架很快就能推出。
Guise还旨在易于扩展。查阅代码你就会看到每个组件都是通过很多更简单的组件构建起来的。我已经提供了一大堆有用的重载,但请根据你的需要创建自己的扩展。
其他框架
我还有一些其他可能对你有帮助的框架。
Core Data Query Interface
CDQI允许使用流畅的语法以简洁的方式来表达Core Data查询。
let isabellas =
try! moc
.from(User.self)
.filter(User.e.firstName == "Isabella")
.orderDesc(by: User.e.lastName)
.all()
DateMath
DateMath是一个小巧的框架,通过使用Calendar.Component
值来进行日期数学运算。
let today = Date()
let tomorrow = today + .day * 1
可以指定进行计算的时间区域。
let today = Date()
let oneDayLessOneSecondInGMT = TimeZone(identifier: "GMT")! ⁝ today + .day * 1 - .second * 1
AbsoluteDate
Swift中的Date
类型表示一个不依赖于时区的时间点。与之相比,AbsoluteDate
类似于许多数据库中可用的日期表示:它包含人类可读的字符串表示形式,但如此表示不包含时区,因此无法确定确切的时间点。
绝对日期依赖于DateMath框架。可以在绝对日期
、绝对时间
和绝对天数
实例上执行日期数学运算。