依赖注入

一个基于服务定位器模式并利用 Swift 属性包装器的依赖注入微框架。

用法

在 App 启动时,通过重写 AppDelegate 的初始化器来注册依赖项

import DependencyInjection
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    @LazyInject private var config: AppConfiguration
    @LazyInject private var router: Router

    override init() {
        super.init()

        DIContainer.register {
            Shared(AppConfigurationImpl() as AppConfiguration)
            Shared(RouterImpl() as Router)
        }
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // use `config` and `router`

        return true
    }
}

延迟求值 vs 立即求值

由于上面示例中注入属性的初始化器会在 AppDelegate 的初始化器之前被调用,因此有必要将注入属性的使用和初始化分开。@LazyInject 只会在其属性首次被访问时解析。要立即解析属性,请改用 @Inject

可变 vs 不可变注入属性

通过 @Inject@LazyInject 注入的属性是不可变的,这是由编译器强制执行的。要解析可变属性,请使用 @MutableInject@MutableLazyInject

可选依赖项

在某些情况下无法解析的依赖项可以简单地标记为 Optional

@Inject var player: MediaPlayer?

如果没有注册 MediaPlayer 实例,则 player 解析为 nil。注意:如果注册的是 Optional,player 也将解析为 nil,因为在这种情况下,注册的类型不是预期的 MediaPlayer,而是 Optional<MediaPlayer>

共享实例 vs 新实例

将依赖项注册为 Shared 将始终解析为相同的(相同的)实例。要在每个属性中获取新实例,请使用 New

DIContainer.register(New(MockRouter() as Router))

通过这样做,在生产代码中所做的注册可以被测试中的模拟对象覆盖,而这些模拟对象不会在对象之间共享。

别名

实例也可以使用多个别名协议进行注册,每个协议仅公开其功能的某些部分

DIContainer.register(Shared(RouterImpl.init, as: Router.self, DeeplinkHandler.self))

注册本身具有依赖项的依赖项

如果注册的依赖项本身依赖于其他依赖项,并且这些依赖项应该通过初始化器注入传递,则可以使用重载来注册 SharedNew 实例,这些重载在闭包中传递一个 Resolver 对象

DIContainer.register {
    Shared { resolve in RouterImpl(config: resolve()) }
}

模块

为了对依赖项进行分组或避免在 Swift 模块外部暴露具体类型,可以使用 DI 模块。这些模块是注册的便捷包装器,可以定义在代码库的不同部分,然后自行注册,例如在 AppDelegate 中

// Feature 1

struct FeatureOneDependencyInjection {
    static let module = Module {
        Shared(FeatureOneImplementation() as FeatureOne)
        New(FeatureOneViewModelImplementation() as FeatureOneViewModel)
    }
}

// Feature 2

struct FeatureTwoDependencyInjection {
    static let module = Module(Shared(FeatureTwoImplementation() as FeatureTwo))
}

// AppDelegate

override init() {
    super.init()

    DIContainer.register {
        FeatureOneDependencyInjection.module
        FeatureTwoDependencyInjection.module
    }
}

或者,模块也可以在中心位置内联使用

DIContainer.register {
    Module {
        Shared(FeatureOneImplementation() as FeatureOne)
        New(FeatureOneViewModelImplementation() as FeatureOneViewModel)
    }
    Module {
        Shared(FeatureTwoImplementation() as FeatureTwo)
    }
}

在 AppDelegate 外部进行替代注册

可以通过使 DIContainer 遵循 DependencyRegistering 协议并实现 registerDependencies 方法来注册依赖项。依赖项将在首次解析依赖项时注册。

extension DIContainer: DependencyRegistering {
    public static func registerDependencies() {
        register(Shared(RouterImpl() as Router))
    }
}

不使用属性包装器的用法

由于属性包装器目前不能在函数体内部使用,因此可以“手动”解析依赖项

func foo() {
    DIContainer.resolve(Router.self)
}

或者,如果编译器可以推断要解析的类型

func foo() {
    bar(router: DIContainer.resolve())
}

func bar(router: Router) {
    // …
}

参数化解析

如果控制反转也应应用于某些或所有参数在稍后提供的类型,则可以注册一个闭包,该闭包接收参数并返回所需的对象。在这种情况下,注册 New 实例最有意义,这意味着每次解析依赖项时都会创建一个新对象。如果注册的是 Shared 实例,则解析的实例将始终是为相应类型首次解析的实例,并且参数将被忽略。

func register() {
    DIContainer.register {
        New({ resolver, id in ConcreteViewModel(id: id) }, as: ViewModelProtocol.self)

        // alternatively:
        New { _, id in ConcreteViewModel(id: id) as ViewModelProtocol }
    }
}

需要提供 id 参数才能使用实现 ViewModelProtocol 的实例。第一个参数 resolver 可以用于或忽略以通过依赖注入解析更多参数。参数的提供是在闭包中完成的

func resolve() -> ViewModelProtocol {
    DIContainer.resolve(ViewModelProtocol.self, arguments: { "id_goes_here" })
}

func resolve() -> PresenterProtocol {
    // multiple arguments are provided as tuple:
    DIContainer.resolve(PresenterProtocol.self) { ("argument 1", 23, "argument 3") }
}

由于属性(以及属性包装器)的初始化器在 Swift 中 self 可用之前被调用,因此只能在 @Inject@LazyInject 的初始化器中提供硬编码的参数。因此,参数化解析目前仅限于如上所示的 DIContainerresolve 方法。

线程安全

注册和解析依赖项都在专用的同步和可重入队列上处理。