Project Icon

Factory

适用于Swift和SwiftUI的现代依赖注入框架

Factory是为Swift和SwiftUI开发的依赖注入框架。它提供类型安全的依赖管理,支持容器、作用域和参数传递等功能。Factory使用简单,代码简洁,适用于多种架构模式。该框架轻量级、文档完善,为iOS和macOS开发提供了现代化的依赖注入方案。

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可以使用的众多方式之一。

// iOS 17之前使用ObservableObject
class ContentViewModel: ObservableObject {
    @Injected(\.myService) private var myService
    ...
}

// iOS 17之后使用Observation
@Observable class ContentViewModel {
    @ObservationIgnored
    @Injected(\.myService) private var myService
    ...
}

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

这就是核心机制。为了使用属性包装器,你必须在指定的容器中定义一个工厂。当被请求时,该工厂必须返回所需的类型。如果任何一个条件不满足,代码将无法编译。因此,Factory在编译时是安全的。

顺便说一下,如果你担心动态构建Factory,不必担心。像SwiftUI视图一样,Factory结构和修饰符是轻量级和临时的值类型。它们在需要时在计算属性内创建,一旦完成其目的就立即被丢弃。

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

解析工厂

早些时候我们演示了如何使用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)
            }
        }
    }
}

Factory是灵活的,它不会将你限制在特定的依赖注入模式或技术上。

更多示例请参见解析

模拟

如果我们回头看看我们最初的视图模型代码,可能会想知道为什么要费这么大劲?为什么不简单地说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的模拟替换MyService

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

注意我们预览代码中的那行,我们回到了容器并在我们的工厂上注册了一个新的闭包。这个函数覆盖了默认的工厂闭包。

现在当我们的预览显示时,ContentView创建一个ContentViewModel,它反过来使用Injected属性包装器依赖于myService。当包装器向工厂请求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还有很多技巧...

作用域

如果你以前使用过Resolver或其他依赖注入系统,那么你可能已经体验过作用域的好处和力量。

如果没有,这个概念很容易理解:一个对象的实例应该存活多长时间?

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

在Factory中,只需添加一个作用域修饰符就可以做到这一点。

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

现在,无论谁请求networkService的实例,他们都会得到与其他人相同的对象实例。

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

如果没有指定作用域,默认作用域是unique。每次从工厂请求时都会实例化并返回一个新的服务实例。

其他常见的作用域是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预览的上下文,甚至在模拟器中运行UITests或在像BrowserStack这样的服务上运行应用程序时也有上下文。更多信息请参阅文档。

调试

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 2.0 添加了调试跟踪支持
  • Factory 2.0 添加了基于 keyPath 的属性包装器
  • Factory 2.0 为 SwiftUI 视图添加了新的 InjectedObject 属性包装器

讨论论坛

关于 Factory 和 Factory 2.0 的讨论和评论可以在讨论区找到。如果您有什么想说的或想了解最新动态,请前往那里。

许可证

Factory 使用 MIT 许可证。有关更多信息,请参阅 LICENSE 文件。

赞助 Factory!

如果您想支持我在 Factory 和 Resolver 上的工作,请考虑GitHub 赞助!有多个级别可供选择,以提供更多支持,甚至包括指导和公司培训。

或者您也可以请我喝杯咖啡!

非常感谢我当前的赞助商 sueddeutsche!

作者

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

Michael 还是 2021 年 Google 的开源同行奖励获得者之一,以表彰他在 Resolver 上的工作。

其他资源

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

稿定AI

稿定设计 是一个多功能的在线设计和创意平台,提供广泛的设计工具和资源,以满足不同用户的需求。从专业的图形设计师到普通用户,无论是进行图片处理、智能抠图、H5页面制作还是视频剪辑,稿定设计都能提供简单、高效的解决方案。该平台以其用户友好的界面和强大的功能集合,帮助用户轻松实现创意设计。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号