Stitcher

License Release

Stitcher 是一个用于 Swift 项目的依赖管理和注入库。

目录

✔️ 最低要求

Stitcher 至少需要 Swift 5.9 版本

Stitcher 依赖于 FoundationDispatch,因此可以在它们可用的每个平台上使用,包括非 Apple 平台,例如 Linux 和 Windows。请注意,目前不支持 WebAssembly。

任何公开暴露的引用发布者的 API 都在其可用的平台上使用 Combine,或者在其他平台上使用 OpenCombine

⏱ 版本历史

版本 变更
0.9.1 预发布版本。
1.0.0 初始版本。
1.1.0 性能和稳定性改进,放宽了最低要求和宏支持。

🧰 特性

🧩 扩展

  1. StitcherMacros

    一个支持包,定义了使用 Swift 宏的元编程实用程序,为函数和初始化器启用自动参数注入,以及用于自动依赖项注册的实用程序。

📦 安装

Swift 包

您可以使用 Xcode 11.0 或更高版本将 Stitcher 作为 Swift Package 依赖项添加,方法是选择 File > Swift Packages > Add Package Dependency... 或 Xcode 13.0 及更高版本中的 File > Add packages...,并添加以下 URL

https://github.com/athankefalas/Stitcher.git

手动安装

您也可以通过下载 Stitcher 项目并将其包含在您的项目中来手动安装此库。

⚡️ 快速开始

通过使用 @Depenendecies 属性包装器在您的 App 结构体或 UIApplication 委托中定义依赖项

@Dependencies
private var container = DependencyContainer {
    // Dependency with no parameters
    AuthenticationService()
    
    // Dependency with parameters
    Dependency { location in
        ImageUploadService(targeting: location)
    }
    .scope(.instance)
}

使用 @Injected 属性包装器注入依赖项

class LoginSceneViewModel: ObservableObject {
    
    @Injected
    private var authenticationService: AuthenticationService
    
    func login(email: Email, password: Password) async {
        await authenticationService.attemptLogin(email: email, password: Password)
    }
}

class ProfileSceneViewModel: ObservableObject {
    
    @Injected(ImageUploadLocation.profileAvatar)
    private var avatarUploadService: ImageUploadService
    
    func upload(image: UIImage) async {
        await avatarUploadService.upload(image)
    } 
}

📋 库概览

依赖容器

DependencyContainer 是一个拥有依赖项集合的数据结构。可以通过注册依赖项或合并多个其他依赖项容器来创建容器。可以使用提供程序闭包在容器中注册依赖项,该闭包构建容器的注册器。提供程序闭包使用结果构建器来组合容器的依赖项,并支持条件语句和分组。构建器支持不同类型的组件,最常见的组件是 DependencyGroupDependency

DependencyContainer {

    Dependency {
        LocalStorageService()
    }
    
    if AppModel.shared.isPrincipalPresent {
        Dependency {
            UserAccountService()
        }
    }
    
    DependencyGroup {
    
        Dependency {
            AuthenticationService()
        }
        
    }
    .enabled(AppModel.shared.canUseAuthentication)

}

为了在值更改后使依赖容器的内容无效,您可以直接使用带有 @Observable 宏的对象属性,或者使用 ObservableObject 或任何发布者来手动使其无效。手动失效使用称为 invalidated 的伪修饰符方法。

手动失效容器

// Invalidate the dependency container when the shared instance of AppModel changes:
DependencyContainer {

    if ObservableModel.shared.isLoggedIn {
        Dependency {
            LogoutService()
        }
    }

}
.invalidated(tracking: ObservableModel.shared)

// Invalidate the dependency container when the authenticationStateChangedPublisher receives an event:
DependencyContainer {

    if ObservableModel.shared.isLoggedIn {
        Dependency {
            LogoutService()
        }
    }

}
.invalidated(tracking: authenticationStateChangedPublisher)

依赖容器是引用类型,因此只要我们有对它的引用,失效就可以附加到任何容器,即使它已经被 @Dependencies 属性包装器激活或管理。在使用可观察对象或发布者手动失效时,请记住,持续或频繁地使依赖容器失效可能会导致性能下降。

定义依赖容器后,必须先激活它,其包含的依赖项才能用于注入。托管依赖容器的生命周期会自动管理,而未托管容器必须手动管理其激活和停用。

托管依赖容器可以使用 @Dependencies 属性包装器定义,并且只要包装属性未被释放,它们就会处于活动状态。更改属性的值将停用旧容器并激活新容器。创建一个托管容器只需要使用属性包装器包装一个依赖容器

@Dependencies
var container = DependencyContainer {}

未托管容器是不使用 @Dependencies 属性包装器定义的依赖容器。定义未托管容器后,必须使用 DependencyGraph 中定义的激活/停用方法手动激活它。还必须手动管理停用,特别是如果容器具有非常特定的生命周期,例如,如果它仅在用户未登录时才应处于活动状态。

let container = DependencyContainer {}

// Manually activate a container
DependencyGraph.activate(container)

// Manually deactivate a container
DependencyGraph.deactivate(container)

依赖注册

可以使用 Dependency 结构体注册依赖项,这是一个用于定义单个依赖项的原始组件。可以使用不同的初始化器来表示依赖项的定位方式,而可以通过使用依赖项结构体上的类似修饰符的方法来修改依赖项的范围和急切性。

为了初始化 Dependency 结构体,至少必须提供一个工厂函数,该函数用于实例化依赖项。该函数可以具有任意数量的参数,并且由于该定义在幕后使用参数包,因此参数的数量没有具体的上限。

Dependency {
    Service()
}

Dependency { cache in
    RemoteRepository(cachedBy: cache)
}

Dependency { firstParameter, secondParameter in
    SomeService(firstParameter, secondParameter)
}

可选的配置参数,例如设置依赖项的定位方式、它的范围和它的急切性将在以下章节中讨论。

按名称注册依赖

默认情况下,依赖项按其类型注册和定位。或者,依赖项也可以按名称定位,该名称可以是任何字符串值。如果在同一容器中为同一名称定义了多个依赖项,则只会使用一个,其余的将被丢弃。

// Setting a name via initializer

Dependency(named: "service") {
    Service()
}

Dependency(named: "repository") { cache in
    RemoteRepository(cachedBy: cache)
}

Dependency(named: "some-service") { firstParameter, secondParameter in
    SomeService(firstParameter, secondParameter)
}

// Setting a name via modifier

Dependency {
    Service()
}
.named("service")

Dependency { cache in
    RemoteRepository(cachedBy: cache)
}
.named("repository")

Dependency { firstParameter, secondParameter in
    SomeService(firstParameter, secondParameter)
}
.named("some-service")

名称初始化器和 named 依赖项修饰符也具有可用于符合 RawRepresentableCustomStringConvertible 的类型的重载,以避免直接使用原始字符串值。在必须按名称定位依赖项的情况下,但表示名称的类型不易转换为字符串,则可以使用关联的值,这要求表示类型符合 Hashable

按类型注册依赖

当默认情况下使用 Dependency 结构体时,依赖项按其类型注册和定位。但是,有时可能需要不按其确切类型,而是按其符合的协议或其继承的超类来定位依赖项。可以通过使用适当的 Dependency 结构体初始化器或修饰符方法将相关的类型定义添加到依赖项注册中。

// Setting a related type via initializer

Dependency(conformingTo: ServiceProtocol.self) {
    Service()
}

Dependency(inheritingFrom: ServiceSuperclass.self) {
    Service()
}

// Setting a related type via modifier

Dependency {
    Service()
}
.conforms(to: ServiceProtocol.self)

Dependency {
    Service()
}
.inherits(from: ServiceSuperclass.self)

将不相关类型的 conformance 或 inheritance 添加到依赖项,在尝试注入依赖项时会导致错误。

按关联值注册依赖

与按名称注册依赖项类似,也可以按关联的值注册依赖项。关联的值必须符合 Hashable 协议。如果在同一容器中为同一哈希值定义了多个依赖项,则只会使用一个,其余的将被丢弃。

// Setting an associated value via initializer

Dependency(for: Services.service) {
    Service()
}

// Setting an associated value via modifier

Dependency {
    Service()
}
.associated(with: Services.service)

在使用关联值定位的依赖项时,具有快速且无冲突的哈希实现可以显着提高性能。

依赖范围

依赖项的范围控制着它的生命周期在实例化后如何由依赖关系图管理。以下四个范围可用

范围 生命周期
Instance 每次注入时都会使用不同的实例。
Shared 只要存在对其的强引用,每次注入时都会使用依赖项的同一实例。
Singleton 每次注入时都会使用依赖项的同一实例。
Managed 每次注入时都会使用依赖项的同一实例,直到手动使给定范围无效。

默认情况下,依赖项的范围会根据依赖项的类型是值类型还是引用类型自动解析。为值类型自动选择的范围是 .instance,而对于引用类型,则使用 .shared。此外,由于值类型无法进行引用计数,因此将 .shared 范围与值类型一起使用等效于使用 .instance 范围。

可以使用 scope 依赖项修饰符设置依赖项的范围

Dependency {
    SomeService()
}
.scope(.instance)

Dependency {
    EventTrackingService()
}
.scope(.shared)

Dependency {
    Repository()
}
.scope(.singleton)

Dependency {
    UserAccountManager()
}
.scope(.managed(by: principalChangedPublisher))
托管依赖范围

托管依赖项范围可以是任何符合 ManagedDependencyScopeProviding 协议的类。该协议具有一个要求,表示为一个名为 onScopeInvalidated 的函数,该函数允许自定义类型注册一个回调,当范围失效时应调用该回调。此函数返回一个符合 ManagedDependencyScopeReceipt 协议的接收类型,可用于取消观察。此外,Stitcher 支持直接使用发布者作为托管范围,这将使给定发布者触发时依赖项的范围失效。

class PrincipalDatasource: ManagedDependencyScopeProviding {
    
    class Observation: ManagedDependencyScopeReceipt {
        typealias Handler = () -> Void
        
        private(set) var callback: Handler?
        
        var isCancelled: Bool {
            callback == nil
        }
        
        init(callback: @escaping Handler) {
            self.callback = callback
        }
        
        func cancel() {
            callback = nil
        }
    }
    
    private var observations: [Observation]
    
    var principal: UserAccount? {
        didSet {
            guard oldValue?.id != principal?.id else {
                return
            }
            
            principalChanged()
        }
    }
    
    init(principal: UserAccount?) {
        self.principal = principal
        self.observations = []
    }
    
    private func principalChanged() {
        observations.removeAll(where: { $0.isCancelled })
        observations.forEach({ $0.callback?() })
    }
    
    func onScopeInvalidated(perform action: @escaping () -> Void) -> any ManagedDependencyScopeReceipt {
        let observation = Observation(callback: action)
        observations.append(observation)
        
        return observation
    }
}
依赖急切性

默认情况下,依赖项在首次需要注入时会被延迟实例化。在某些情况下,例如单例事件跟踪服务,需要依赖项在激活其依赖项容器时进行实例化,以便能够立即接收事件。要启用此行为,可以使用 eagerness 依赖项修饰符,以便 DependencyGraph 在激活依赖项容器时实例化依赖项。

Dependency {
    EventTracker()
}
.eagerness(.eager)
依赖组

如前一节所述,条件依赖项注册可以根据系统的状态有条件地提供依赖项。但是,如果基于同一状态有条件地启用了多个依赖项,则将它们组合在一起并有条件地启用整个组可能会有所帮助。可以通过传递一个提供程序闭包来初始化依赖项组,该闭包构建组的注册器。

DependencyContainer {

    DependencyGroup {
        
        MotionDetectionService()
        
    }
    .enabled(System.isGyroscopeSupported)

}

可以使用 enabled 依赖项组修饰符启用或禁用依赖项组。

其他注册表示

除了在构建依赖项容器时使用 DependencyDependencyGroup 结构体之外,还可以使用两个以上的注册表示组件来注册依赖项。

Autoclosure 注册组件

第一个注册表示组件是提供程序 autoclosure,可以用作注册具有零参数初始化器的类型定位依赖项的便捷方法。

DependencyContainer {
    
    SomeService()
    
    Dependency {
        SomeService()
    }

}

上面的两个注册是等效的。第一个依赖项中的 autoclosure 在评估提供程序闭包时不会被调用,而是在依赖关系图实例化依赖项时被调用。

DependencyRepresenting 注册组件

完全自定义的依赖项注册类型也可以与依赖项容器一起使用。自定义注册类型必须符合 DependencyRepresenting 协议。该协议有四个要求来定义依赖项的特征,其中三个是可选的,定义了依赖项定位器、范围和急切性。第四个要求是一个名为 dependencyProvider 的属性,它必须提供一个将用于实例化依赖项的函数。

struct RepositoryDependency: DependencyRepresenting {
    
    var locator: DependencyLocator {
        .name(Self.name)
    }
    
    var scope: DependencyScope {
        .singleton
    }
    
    var eagerness: DependencyEagerness {
        .eager
    }
    
    var dependencyProvider: DependencyFactory.Provider<Repository> {
        DependencyFactory.Provider { cache in
            Repository(cachedBy: cache)
        }
    }
    
    static let name = "repository"
}

@Dependencies
var container = DependencyContainer {
    RepositoryDependency()
}

@Injected(name: RepositoryDependency.name)
var repository: Repository

多个依赖容器

在模块化应用中,可以将系统的不同部分隔离成更小的子系统。为了支持这种范式,可以使用多个依赖注入容器,可以通过同时激活多个(托管或非托管)容器,或者将每个子系统的容器合并到一个组合依赖注入容器中。

let containers: [DependencyContainer] = composeFeatureContainers()
let container = DependencyContainer(merging: containers)

请注意,组合依赖注入容器会强引用合并后的容器,以便正确地传播观察结果。

依赖图

依赖关系图表示所有活动依赖注入容器的组合,以及用于存储依赖实例的额外存储空间,并且是注入依赖关系的主要组件。此外,它还负责处理依赖注入容器的激活、索引和停用。

激活依赖注入容器时,根据StitcherConfiguration中定义的选项,依赖关系图可以索引容器的注册器,以便在注入期间最大限度地缩短搜索依赖关系的时间。在索引期间,任何急切依赖项都会被实例化并存储以供将来使用。如果禁用索引,则急切依赖项会在激活后立即实例化。

请注意,索引和急切依赖项初始化是异步执行的,因为该操作直接取决于依赖注入容器的大小。如果必须等待此操作完成,则可以使用activate方法的异步变体。对于托管容器,请使用@Dependencies属性包装器的setContainer方法。一般来说,为了提高索引期间的性能,建议使用多个独立激活的小型容器。

自动注入

自动注入是通过使用@Injected属性包装器来实现的。使用此属性包装器时,依赖关系在首次请求时才会被延迟注入,这对于定义依赖关系之间的循环关系很有帮助。

注入的属性包装器将在首次请求其包装值时尝试注入依赖关系。如果找不到依赖关系或其类型不匹配,则会导致运行时前提条件失败,这将打印作为失败源头的包装属性的文件和行号。

按名称注入

可以通过使用与它们在依赖注入容器中注册时相同的名称来注入依赖项。注册的依赖项类型必须可以通过类型转换转换为包装属性的类型。

按名称注入依赖项需要使用适当的 Injected 初始化程序。第一个参数具有 name 标签,并定义将要定位依赖项的名称。在初始化程序的第一个参数之后,其余参数将被解析为用于实例化依赖项的实例化参数。

enum Services: String {
    case service
    case repository
}

@Dependencies
var container = DependencyContainer {
    
    Dependency {
        Service()
    }
    .named(Services.service)
    
    Dependency { context in
        Repository(managedObjectContext: context)
    }
    .named(Services.repository)
}

@Injected(name: Services.service)
var service: Service

@Injected(name: Services.repository, ManagedObjectContexts.repositoryContext)
var repository: Repository
按类型注入

注册和注入依赖关系的默认方法是按其类型,或者通过协议一致性或继承相关的超类型。除了直接使用依赖类型之外,还支持一些常见类型

  1. Optional 注入与 Wrapped 类型匹配的依赖项,如果未找到此类依赖项,则注入 nil。

  2. Arrays 注入与 Element 类型匹配的 *所有* 依赖项,如果未找到此类依赖项,则注入空集合。

class AccountRepository: PrincipalAware {}

class AccountSettingsRepository: PrincipalAware {}

@Dependencies
var container = DependencyContainer {
    Dependency {
        AccountRepository()
    }
    .conforms(to: PrincipalAware.self)
    
    Dependency {
        AccountSettingsRepository()
    }
    .conforms(to: PrincipalAware.self)
}

@Injected
var accountRepository: AccountRepository

@Injected
var accountRepository: AccountRepository?

@Injected
var principalAwareServices: [PrincipalAware]

可选数组是一种支持的附加类型,但应尽可能避免使用可选集合。相反,它们可以用非可选集合代替。

按关联值注入

可以通过使用与它们在依赖注入容器中注册时相同的可哈希值来注入依赖项。注册的依赖项类型必须可以通过类型转换转换为包装属性的类型。

按关联值注入依赖项需要使用适当的 Injected 初始化程序。第一个参数具有 value 标签,并定义将要定位的值。在初始化程序的第一个参数之后,其余参数将被解析为用于实例化依赖项的实例化参数。给定值的哈希值对于表示相同依赖项的不同实例不得更改。

enum UploadLocation: Hashable {
    case avatar
    case banner
}

struct Entity<T>: Hashable {}

@Dependencies
var container = DependencyContainer {
    
    Dependency {
        ProfileAvatarUploadService()
    }
    .associated(with: UploadLocation.avatar)
    
    Dependency {
        ProfileBannerUploadService()
    }
    .associated(with: UploadLocation.banner)
    
    
    Dependency { context in
        UserRepository(managedObjectContext: context)
    }
    .associated(
        with: Entity(
            of: User.self
        )
    )
}

@Injected(value: UploadLocation.avatar)
var avatarUploadService: UploadServiceProtocol

@Injected(value: UploadLocation.banner)
var bannerUploadService: UploadServiceProtocol

@Injected(value: Model(of: User.self), ManagedObjectContexts.usersContext)
var userRepository: UserRepository

手动注入

手动注入遵循与自动注入相同的原则,但允许在注入期间处理错误,而不是运行时错误。与自动注入相比,手动注入是急切的,这意味着依赖关系将在请求时立即实例化。通过使用 DependencyGraphinject 系列方法可以实现注入任意依赖项。

@Dependencies
var container = DependencyContainer {
    
    Dependency {
        AccountService()
    }
    .named("account-service")
    
    Dependency { context in
        UserRepository(managedObjectContext: context)
    }
    
    Dependency {
        ImageUploadService()
    }
    .associated(with: UploadServices.images)
}

let accountService: AccountService = try DependencyGraph.inject(byName: "account-service")
let userRepository: UserRepository = try DependencyGraph.inject(byType: UserRepository.self, ManagedObjectContexts.usersContext)
let imageUploadService: ImageUploadService = try DependencyGraph.inject(byValue: UploadServices.images) 

依赖循环

循环依赖关系是两种类型之间的关系,它们在初始化期间相互依赖。例如,给定一个名为 Root 的主类型和一个名为 Leaf 的二级类型,root 具有一个 leaf 类型的属性,该属性必须在初始化期间设置,反之,leaf 类型具有一个 root 类型的属性,该属性必须在初始化期间设置。尝试初始化这两种类型时,将发生无限递归循环,因为为了实例化 root,您必须实例化 leaf,而为了实例化 leaf,您必须实例化 root。

为了避免这些循环,建议通过使用 @Injected 属性包装器或在首次访问属性时调用 DependencyGraph 的手动注入方法来延迟注入依赖关系。 Stitcher 具有运行时依赖循环检测功能,可以检测到这些循环,并发出带有整个循环映射的描述性错误,无论其深度如何,因此可以轻松检测和解决它们。

// InjectionError description when a cycle is detected:
Dependency cycle detected, Type[Root] -> Type[Leaf] -> Type[Root].

上面的错误具有根类型和在同一上下文中执行的所有依赖项实例化,因此可以轻松跟踪和纠正循环。请注意,为了提高注入性能,默认情况下仅在 DEBUG 构建中条件性地启用运行时依赖循环检测功能。

互操作性

Stitcher 具有一些互操作性访问点,以便配置库的行为或接收有关状态更改的更新。

PostInstantiationAware Hook

PostInstantiationAware 协议可用于挂钩到 DependencyGraph 对依赖项的初始化,以便执行各种操作,例如资源加载、注入延迟依赖项等。它有一个单一的要求,一个名为 didInstantiate 的函数,该函数由依赖关系图在依赖项实例化之后但在注入之前调用。

class EventTrackingService: PostInstantiationAware {
    
    init() {}
    
    func didInstantiate() {
        sendEvent(named: "App started")
    }
    
    func sendEvent(named: String) {}
}

@Dependencies
var container = DependencyContainer {
    Dependency {
        EventTrackingService()
    }
    .scope(.singleton)
    .eagerness(.eager)
}

请注意,*不能保证* 调用 didInstantiate 方法的线程,因此使用此挂钩在没有首先调度到主线程的情况下执行 UI 更新可能会导致意外的行为甚至崩溃。

DependencyGraph 变更观察

由于依赖注入容器被激活、失效或停用,因此依赖关系图中可用的依赖关系可能会发生变化。为了在该时间重新加载任何依赖关系,需要进行观察。依赖关系图具有一个发布者,每当可用依赖关系失效或更改时,该发布者都会触发,称为 graphChangedPublisher

为了在依赖关系图发生更改时更改任何自动注入的依赖关系,在 @Injected 属性包装器中定义了以下有用的辅助函数

  1. loadIfNeeded 函数在尚未加载注入的依赖项,或者加载的值为 nil 或空集合时,加载注入的依赖项。
  2. reload 函数重新加载注入的依赖项。
  3. autoreload 函数在依赖关系图的 *每次* 更改后自动重新加载注入的依赖项。

请注意,重新加载依赖项时必须格外小心,例如取消或等待先前注入的实例拥有的任何正在运行的任务。

配置

可以使用 StitcherConfiguration 枚举中定义的属性来配置 Stitcher 的行为。

选项 行为
isIndexingEnabled 控制是否激活依赖注入容器的索引。未索引的容器在查找依赖项时可能性能较慢
approximateDependencyCount 已定义依赖项数量的近似计数,用于优化索引期间的内存分配
autoCleanupEnabled 控制当应用程序最小化时是否自动清除依赖关系图的实例存储。
runtimeCycleDetectionAvailability 控制运行时依赖循环检测功能的可用性。

🐞 问题和功能请求

如果您在使用该库时遇到问题或有功能请求,请务必提交 issue。