Swift 和 SwiftUI 基于容器的依赖注入新方法。

Factory 2.4.4

Factory 很大程度上受到了 SwiftUI 的影响,并且在我看来非常适合在该环境中使用。 Factory 是...

听起来好得令人难以置信? 让我们来看看。

一个简单的例子

大多数基于容器的依赖注入系统要求您以某种方式定义给定的服务类型可用于注入,并且许多系统需要某种工厂或机制,以便在需要时提供该服务的新实例。

Factory 也不例外。 这是一个简单的依赖注册,它返回一个符合 MyServiceType 的服务。

extension Container {
    var myService: Factory<MyServiceType> { 
        Factory(self) { MyService() }
    }
}

与 Resolver 经常需要定义大量的嵌套注册函数,或者 SwiftUI,定义一个新的环境变量需要创建一个新的 EnvironmentKey 并添加额外的 getter 和 setter 不同,这里我们只是向默认容器添加一个新的 Factory 计算变量。当调用它时,我们的 Factory 被创建,它的闭包被评估,并且我们在需要时获得依赖的实例。

注入我们服务的实例同样简单明了。 这里只是 Factory 可以使用的众多方法之一。

// Pre iOS 17 with ObservableObject
class ContentViewModel: ObservableObject {
    @Injected(\.myService) private var myService
    ...
}

// Post iOS 17 with Observation
@Observable class ContentViewModel {
    @ObservationIgnored
    @Injected(\.myService) private var myService
    ...
}

这个特定的视图模型使用 Factory 的 @Injected 属性包装器之一来请求所需的依赖项。 类似于 SwiftUI 中的 @Environment,我们为属性包装器提供了一个 keyPath,指向所需类型的 Factory,它会在 ContentViewModel 创建时立即解析该类型。

这就是核心机制。 为了使用属性包装器,您必须在指定的容器中定义一个 Factory。 该 Factory 必须在被请求时返回所需的类型。 否则代码将无法编译。 因此,Factory 是编译时安全的。

顺便说一句,如果您担心动态构建 Factory,请不要担心。 就像 SwiftUI Views 一样,Factory 结构体和修饰符是轻量级的瞬态值类型。 它们在需要时在计算变量内部创建,并在完成其目的后立即被丢弃。

有关定义作用域、使用构造器注入以及传递参数的 Factory 定义的更多示例,请参阅 注册 页面。

解析 Factories

前面我们演示了如何使用 Injected 属性包装器。 但是也可以绕过属性包装器并直接与 Factory 对话。

class ContentViewModel: ObservableObject {
    private let myService = Container.shared.myService()
    private let eventLogger = Container.shared.eventLogger()
    ...
}

只需将所需的 Factory 作为函数调用,您将获得其管理的依赖项的实例。 就这么简单。

如果您正在研究基于容器的依赖注入,请注意您还可以将容器的实例传递给视图模型,并直接从该容器获取服务的实例。

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)
            }
        }
    }
}

Factory 非常灵活,并且不会将您束缚于特定的依赖注入模式或技术。

有关更多示例,请参见 解析

模拟 (Mocking)

如果我们回头看看我们原来的视图模型代码,人们可能会想知道我们为什么要费这么大的周折? 为什么不简单地说 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 的行为,使其 MyService 依赖项在开发过程中不进行实时 API 调用?

很简单。 只需用一个也符合 MyServiceType 的 mock 替换 MyService

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let _ = Container.shared.myService.register { MockService2() }
        ContentView()
    }
}

请注意我们预览代码中的一行,我们在容器中注册了 Factory 的新闭包。 这个函数会覆盖默认的 Factory 闭包。

现在,当显示我们的预览时,ContentView 创建一个 ContentViewModel,它依次依赖于 myService,使用 Injected 属性包装器。 当包装器向 Factory 请求 MyServiceType 的实例时,它现在会得到 MockService2,而不是最初定义的 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 可以轻松地深入到依赖链中,并根据需要对系统进行特定更改。 这使得测试加载状态、空状态和错误条件变得简单。

Factory 还适用于 Xcode 16 的新 Swift 测试框架。

import Testing

@Suite(.serialized) struct AppTests {
  @Test(arguments: Parameters.allCases) func testA(parameter: Parameters) {
    // This function will be invoked serially, once per parameter, because the
    // containing suite has the .serialized trait.
    Container.shared.someService.register { MockService(parameter: parameter) }
    let service = Container.shared.someService()
    #expect(service.parameter == parameter)
  }


  @Test func testB() async throws {
    // This function will not run while testA(parameter:) is running. One test
    // must end before the other will start.
    Container.shared.someService.register { ErrorService() }
    let service = Container.shared.someService()
    #expect(service.error == "Oops")
  }
}

但我们还没有完成。

Factory 还有很多技巧...

作用域

如果您以前使用过 Resolver 或其他一些依赖注入系统,那么您可能已经体验过作用域的好处和强大之处。

如果不是,这个概念很容易理解:一个对象的实例应该存在多久?

您无疑已将一个类的实例塞入变量中,并在您职业生涯的某个时刻创建了一个单例。 这是一个作用域的例子。 创建单个实例,然后由应用程序中的所有方法和函数使用和共享。

这可以在 Factory 中通过添加作用域修饰符来完成。

extension Container {
    var networkService: Factory<NetworkProviding> { 
        self { NetworkProvider() }
            .singleton
    }
    var myService: Factory<MyServiceType> { 
        self { MyService() }
            .scope(.session)
    }
}

现在,每当有人请求 networkService 的实例时,他们都会得到与其他所有人相同的对象实例。

请注意,客户端既不知道也不关心作用域。 也不应该。 客户端只是在需要时获得它需要的东西。

如果未指定作用域,则默认作用域是唯一的。 每次从 Factory 请求服务时,都会实例化并返回该服务的新实例。

其他常见的作用域是 cachedshared。 缓存的项目会一直存在,直到缓存被重置,而共享的项目只在有人持有对它们的强引用时才存在。 当最后一个引用消失时,弱持有的共享引用也会消失。

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.4.3) 至少需要 Swift 5.10,并且此版本当前支持的 iOS 最低版本是 iOS 13。

Factory 2.0 迁移

如果您从 Factory 1.x 开始,此处提供迁移文档

Factory 2.4 迁移

Factory 2.4 在严格并发准则下与 Xcode 16 一起使用。

讨论论坛

有关 Factory 和 Factory 2.0 的讨论和评论可以在 讨论区 中找到。 如果您有什么要说的或者想了解最新信息,请去那里。

许可证

Factory 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件。

赞助 Factory!

如果您想支持我在 Factory 和 Resolver 上的工作,请考虑 GitHub 赞助! 存在许多级别,可以增加支持,甚至可以进行指导和公司培训。

或者您可以给我买一杯咖啡!

作者

Factory 由 Michael Long 设计、实现、记录和维护,Michael Long 是一位首席 iOS 软件工程师,也是 Medium 上排名前 1,000 位的技术作家。

Michael 也是 Google 在 2021 年的 开源同行奖励 获奖者之一,因为他在 Resolver 上的工作。

其他资源