Swift 全局配置模块

一个完全并发安全的 Swift 通用配置和依赖注入解决方案,大致受到 https://www.avanderlee.com/swift/dependency-injection/ 的启发。

基本用法(依赖注入)

声明一个服务

public protocol MyService : Sendable { ... }
public final class DefaultMyService : MyService { ... }

extension ConfKeys {
   #declareServiceKey("myService", MyService.self, defaultValue: DefaultMyService())
}
/* You can have as many injected instances of your service as you want by declaring more keys for it. */

声明一个通过工厂实例化的服务

extension ConfKeys {
   #declareServiceFactoryKey("myServiceFactory", MyService.self, defaultValue: { DefaultMyService() })
}

使用它

struct ServiceClient {
   @InjectedConf(\.myService) var myService: MyService
   @InjectedConf(\.myServiceFactory) var myServiceViaFactory: MyService

   func myFunction() {
      /* The myService variable will always be up-to-date. */
      myService.doAmazingStuff(...)
      /* Using the default definition set above for myServiceFactory, myServiceViaFactory will always be a new instance. */
      myServiceViaFactory.doAmazingStuff(...)
   }
}

更改注入的实例

func myAppInit() {
   Conf.setRootValue(newService, for: \.myService)
}

基本用法(框架配置)

/* Declare the configuration key. */
extension ConfKeys {
   #declareConfKey("myConf", Int.self, defaultValue: 42)
}

/* Use it in your code. */
if Conf[\.myConf] == 42 {...}

/* Optionally you can define an internal convenience on Conf for easier access. */
internal extension Conf {
   static var myConf: Int {Conf[\.myConf]}
}

自动注入服务

自动注入服务是指可以使用 @InjectedConf 而无需在属性包装器初始化程序中指定键路径的服务。
当您的服务有一个“主要”实例,并且每次定义服务变量时都指定键路径是多余的时候,这非常有用。

对于这些服务,您可以使用 AutoInjectable 协议

public final class MyService { ... }

extension ConfKeys {
   #declareServiceKey("myService"/* or nil if you don’t need the ConfKeys keypath */, MyService.self, "MyServiceKey", defaultValue: DefaultMyService())
}
extension MyService : AutoInjectable {
   public typealias AutoInjectionKey = ConfKeys.MyServiceKey
}

现在可以像这样获取服务实例

struct ServiceClient {
   @InjectedConf()
   var myService: MyService
   ...
}

请注意,协议类型的服务不能是自动注入的。

@MainActor 或其他隔离服务

对于 @MainActor 服务,您必须使用 ConfKeyMainActorAutoInjectableMainActor 协议,而不是它们非 MainActor 的等价物。该宏有一个参数,用于传递用于声明配置或服务的全局 actor。

extension ConfKeys {
   #declareConfKey("myConf", Int.self, on: MainActor.self, defaultValue: 42)
}

对于在其他全局(或非全局)actor上隔离的服务,您需要做更多的工作。

对于全局 Actor

基本上,您需要复制所有的 *MainActor 文件,并将 MainActor 实例替换为您自己的全局 actor。

对于非全局 Actor

这可能是一个非常高级的用例。如果您需要它,请问自己为什么,并看看是否真的需要。

像全局 actor 一样,您需要复制和修改 *MainActor 文件。

我还没有尝试过,但很可能您必须删除全局 actor 注释,并在文件中不同方法中添加您需要的 isolated 参数。

服务初始化排序

在极少数情况下,您需要控制在客户端初始化期间何时初始化服务,您可以使用以下模式

struct ServiceClient {
   @InjectedConf
   var myService: MyService

   init() {
      /* Do whatever... */
      _myService = .init()
      /* Do something else... */
   }
}