为Swift和SwiftUI提供的基于容器的依赖注入的新方法。
Factory 2.3
Factory深受SwiftUI影响,在我看来,它非常适合在该环境中使用。Factory是...
- 适应性强:Factory不会使您局限于某个单一的依赖注入策略或技术。
- 功能强大:Factory支持容器、作用域、传递参数、上下文、装饰器、单元测试、SwiftUI预览以及更多。
- 性能出色:对于您的大多数服务而言,基本上不需要设置时间,解析速度非常快,并且无需编译时脚本或构建阶段。
- 安全可靠:Factory在编译时是安全的;给定类型的工厂必须存在,否则代码将无法编译。
- 简洁明了:定义注册通常只需一行代码。解析也是如此。
- 灵活多样:无论您是在使用UIKit或SwiftUI,iOS还是macOS,使用MVVM、MVP、Clean还是VIPER,都没问题。Factory与所有这些以及更多都兼容。
- 文档完善:Factory 2.0具有广泛的DocC文档和示例,涵盖了其类、方法和用例。
- 轻量级:尽管如此,Factory依然精简,小于800行可执行代码。
- 已测试:100%代码覆盖率的单元测试有助于确保注册、解析和作用域的正确运行。
- 免费开源:Factory是免费的开源软件,MIT许可证下发布。
听起来太好了?让我们一探究竟。
一个简单的示例
大多数基于容器的依赖注入系统都需要您以某种方式定义给定服务类型是否可用于注入,许多系统还需要某种机制或工厂来当需要时提供一个新实例。
Factory也不例外。以下是一个简单的依赖注册示例,它返回符合MyServiceType
的服务。
extension Container {
var myService: Factory<MyServiceType> {
Factory(self) { MyService() }
}
}
与Resolver不同,它通常需要定义大量的嵌套注册函数,或者在SwiftUI中,定义新的环境变量需要创建新的EnvironmentKey以及添加额外的getter和setter,这里我们只需在默认容器中添加一个新的Factory
计算变量。当它被调用时,我们的Factory被创建,其闭包被评估,并且当我们需要依赖项时,我们得到一个依赖项实例。
注入服务实例同样简单直接。以下是Factory可以使用的一种方式。
class ContentViewModel: ObservableObject {
@Injected(\.myService) private var myService
...
}
这个特定的视图模型使用了Factory的@Injected
属性包装器来请求所需的依赖项。类似于SwiftUI中的@Environment
,我们向属性包装器提供一个指向所需类型工厂的keyPath,当ContentViewModel
创建时,它会解析该类型。
这就是核心机制。为了使用属性包装器,您必须在指定的容器中定义工厂。该工厂必须被要求时返回所需的类型。未能做到这两个中的任何一个,代码将无法编译。因此,Factory在编译时是安全的。
顺便说一句,如果你担心动态构建工厂,那就不要担心。与 SwiftUI 视图一样,工厂结构体和修饰符是轻量级且短暂的值类型。只有在需要时,它们才在计算变量内部创建,一旦其目的已经实现,就会立即丢弃。
要了解更多定义作用域、构造函数注入和参数传递的工厂定义示例,请参阅 注册 页面。
解析工厂
之前我们演示了如何使用 Injected
属性包装器。但也可以绕过属性包装器,直接与工厂通信。
class ContentViewModel: ObservableObject {
private let myService = Container.shared.myService()
private let eventLogger = Container.shared.eventLogger()
...
}
只需将所需的工厂作为函数调用,你将获得其管理的依赖项的实例。就这么简单。
如果你对基于容器的依赖注入感兴趣,请注意,你也可以将容器的一个实例传递给视图模型,并直接从该容器中获取你的服务实例。
class ContentViewModel: ObservableObject {
let service: MyServiceType
init(container: Container) {
service = container.service()
}
}
或者如果你想使用组合根结构,只需使用容器为构造函数提供所需的依赖。
extension Container {
var myRepository: Factory<MyRepositoryType> {
Factory(self) { MyRepository(service: self.networkService()) }
}
var networkService: Factory<Networking> {
Factory(self) { MyNetworkService() }
}
}
@main
struct FactoryDemoApp: App {
let viewModel = MyViewModel(repository: Container.shared.myRepository())
var body: some Scene {
WindowGroup {
NavigationView {
ContentView(viewModel: viewModel)
}
}
}
}
工厂非常灵活,它不会限制你使用特定的依赖注入模式或技术。
有关更多示例,请参阅 解析。
模拟
如果我们回顾一下原始的视图模型代码,人们可能会想知道我们为什么要付出这么多麻烦?为什么不能简单地说 let myService = MyService()
然后结束呢?
或者保持容器思想,但写些类似的代码...
extension Container {
static var myService: MyServiceType { MyService() }
}
嗯,使用基于容器的依赖注入系统获得的主要好处是我们能够根据需要更改系统的行为。考虑以下代码
struct ContentView: View {
@StateObject var model = ContentViewModel()
var body: some View {
Text(model.text())
.padding()
}
}
我们的 ContentView
使用我们的视图模型,它被分配给一个 StateObject
。很好。但现在我们想要预览我们的代码。我们如何更改 ContentViewModel
的行为,以便它在开发期间不会进行现场 API 调用?
很容易。只需将 MyService
替换为一个符合 MyServiceType
的模拟。
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let _ = Container.shared.myService.register { MockService2() }
ContentView()
}
}
注意我们预览代码中的那一行,我们回到了我们的容器,并在工厂中注册了一个新的闭包。这个函数覆盖了默认的工厂闭包。
现在当我们的预览显示时,ContentView
创建了一个 ContentViewModel
,它依赖于 myService
,使用的是 Injected
属性包装器。当包装器请求 MyServiceType
的实例时,它现在得到的是原来的 MyService
类型而不是定义的 MyService
类型。
这是一个强大的概念,让我们能够深入依赖链并按需更改系统的行为。
测试
相同的概念也可以用于编写单元测试。考虑以下。
final class FactoryCoreTests: XCTestCase {
override func setUp() {
super.setUp()
Container.shared.reset()
}
func testLoaded() throws {
Container.shared.accountProvider.register { MockProvider(accounts: .sampleAccounts) }
let model = Container.shared.someViewModel()
model.load()
XCTAssertTrue(model.isLoaded)
}
func testEmpty() throws {
Container.shared.accountProvider.register { MockProvider(accounts: []) }
let model = Container.shared.someViewModel()
model.load()
XCTAssertTrue(model.isEmpty)
}
func testErrors() throws {
Container.shared.accountProvider.register { MockProvider(error: .notFoundError) }
let model = Container.shared.someViewModel()
model.load()
XCTAssertTrue(model.errorMessage = "Some Error")
}
}
再次,工厂使我们能够深入依赖链,并按需对系统进行具体更改,这使得测试加载状态、空状态和错误条件变得简单。
但我们还没有完成。
Factory 还有更多技巧...
范围
如果你之前使用过 Resolver 或其他依赖注入系统,那么你可能已经体验过范围的好处和强大功能。
如果没有,这个概念很简单:一个对象实例的生存期应该有多长?
你无疑在职业生涯的某个时刻将一个类的实例放入变量中并创建了一个单例。这是一个范围示例。一个单独的实例被创建,然后被所有应用程序中的方法和函数使用和共享。
只需在 Factory 中添加一个范围修饰符即可做到这一点。
extension Container {
var networkService: Factory<NetworkProviding> {
self { NetworkProvider() }
.singleton
}
var myService: Factory<MyServiceType> {
self { MyService() }
.scope(.session)
}
}
现在每次有人请求一个 networkService
的实例时,他们都将获得与其他人相同的对象实例。
请注意,客户端既不知道也不关心作用域。它也不应该知道。客户端只是在其需要的时候得到它所需的。
如果未指定作用域,则默认作用域是唯一的。每次从工厂请求服务时,都将实例化并返回一个新的服务实例。
其他常见的范围有 cached
和 shared
。缓存的条目会在缓存重置之前持久化,而共享的条目将一直存在,直到有人持有对它们的强引用。当最后一个引用消失时,弱引用的共享引用也将消失。
Factory还有其他作用域类型,并且可以添加更多您自己的。有关更多示例,请参阅作用域。
作用域和作用域管理是依赖注入工具箱中的强大工具。
简化的语法
您可能已经注意到,在先前的示例中,Factory还提供了一点点语法糖,这使得我们的定义更加简洁。我们只是要求封装的容器使用self.callAsFunction { ... }
为我们创建一个正确绑定的Factory。
extension Container {
var sugared: Factory<MyServiceType> {
self { MyService() }
}
var formal: Factory<MyServiceType> {
Factory(self) { MyService() }
}
}
这两个定义提供了相同的精确结果。加糖的函数甚至被内联,因此两个版本之间几乎没有性能差异。
上下文
Factory 2.1的新特性之一是上下文。假设出于逻辑原因,每当您的应用程序以调试模式运行时,您永远不希望它调用您的应用程序的分析引擎。
很简单。只需为该特定上下文注册一个覆盖。
container.analytics.onDebug {
StubAnalyticsEngine()
}
还有用于单元测试、SwiftUI预览以及当在模拟器或使用BrowserStack等服务上运行UITests时运行的上下文。有关更多信息,请参阅文档。
调试
Factory还可以帮助您调试代码。在DEBUG模式下运行Factory时,您可以跟踪注入过程,并在给定解析周期内查看每个实例化或从缓存返回的对象。
0: Factory.Container.cycleDemo<CycleDemo> = N:105553131389696
1: Factory.Container.aService<AServiceType> = N:105553119821680
2: Factory.Container.implementsAB<AServiceType & BServiceType> = N:105553119821680
3: Factory.Container.networkService<NetworkService> = N:105553119770688
1: Factory.Container.bService<BServiceType> = N:105553119821680
2: Factory.Container.implementsAB<AServiceType & BServiceType> = C:105553119821680
这可以使您更容易看到特定对象或服务的整个依赖关系树。
有关更多信息和其他功能,请参阅调试。
文档
单个README文件只是触及了表面。幸运的是,Factory有详细的文档。
当前的DocC文档可以在项目本身以及GitHub Pages上找到。
安装
Factory支持CocoaPods和Swift Package Manager。
pod "Factory"
或者下载源文件,并将Factory文件夹添加到您的项目中。
请注意,Factory当前版本(2.1)要求至少使用Swift 5.1,并且当前支持的最小iOS版本是iOS 11。
Factory 2.0迁移
如果您从Factory 1.x开始,可以在这里找到迁移文档。
- Factory 2.0添加了基于容器的依赖关系解析的真实Factory容器。
- Factory 2.0添加了基于容器的范围。
- Factory 2.0向容器和Factory添加了装饰器。
- Factory 2.0添加了调试跟踪支持。
- Factory 2.0添加了基于keyPath的属性包装器。
- Factory 2.0为SwiftUI视图添加了一个新的
InjectedObject
属性包装器。
讨论论坛
关于Factory和Factory 2.0的讨论和评论可以在讨论中找到。如果您有话要说或想保持最新,请前往那里。
许可证
Factory可在MIT许可下使用。有关更多信息,请参阅LICENSE文件。
赞助Factory!
如果您想支持我在Factory和Resolver上的工作,请考虑GitHub Sponsorship!有多个层级的支持,甚至包括导师培训和公司培训。
或者您可以简单地为我买一杯咖啡!
衷心感谢我的新赞助商:sueddeutsche,doozMen。
作者
本产品由Michael Long设计、实施、编写文档并维护,他是一位高级iOS软件工程师,同时也是Medium平台上排名前1,000的技术作家。
- LinkedIn: @hmlong
- Medium: @michaellong
- Twitter: @hmlco
Michael还曾是Google 2021年开源同伴奖励获奖者之一,因其对Resolver的贡献而获奖。