EasyDi
适用于快速开发的200行有效DI库。
要求
Swift 5+, iOS 10.3+
示例
要运行示例项目,请克隆仓库,然后首先从示例目录中运行 pod install
。
安装
EasyDi可通过CocoaPods获得。要安装它,请简单地将在Podfile中添加以下行:
pod "EasyDi"
作者
Andrey Zarembo
电子邮件: [email protected]
twitter: @andreyzarembo
telegram: @andreyzarembo
Alexey Markov
电子邮件: [email protected]
telegram: @big_bada_booooom
许可
EasyDi 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。
关于
如果项目包含超过 5 个屏幕且将被支持超过一年,依赖倒置非常重要。以下是三个使 DI 更好的基本场景:
-
并行开发。一位开发者将处理 UI,另一位开发者将处理数据层。UI 可使用测试数据开发,并且可以从测试 UI 中调用数据层。
-
测试。通过用模拟响应替换网络层,您可以从错误情况到所有 UI 行为选项进行检查。
-
重构。在不更改与 UI 相同的协议的情况下,可以将网络层替换为具有缓存和另一个 API 的新、快速版本。
依赖倒置的本质可以用一句话来描述:对象的依赖关系应由协议关闭,并在创建时从外部传递给对象。而不是
class OrderViewController {
func didClickShopButton(_ sender: UIButton?) {
APIClient.sharedInstance.purchase(...)
}
}
应采用这种方法
protocol IPurchaseService {
func perform(...)
}
class OrderViewController {
var purchaseService: IPurchaseService?
func didClickShopButton(_ sender: UIButton?) {
self.purchaseService?.perform(...)
}
}
有关依赖倒置的原则和 SOLID 架构的更多详细信息,请参阅 这里 和 这里。
EasyDi 包含适用于 Swift 的依赖关系容器。该库的语法专门为快速开发和有效使用而设计。它只占用 200 行代码,因此可以完成让您需要的所有成熟 DI 库功能。
- 使用依赖关系创建对象和将依赖关系注入现有对象
- 容器 - 配置的分离
- 依赖关系解析类型:对象图、单例、原型
- 对象替代和测试的依赖关系上下文
EasyDi 中没有注册/解析方法。相反,依赖关系被描述如下
var apiClient: IAPIClient {
return define(init: APIClient()) {
$0.baseURl = self.baseURL
return $0
}
}
由于这种方法,可以解决循环依赖关系并使用现有对象。
如何使用EasyDi(一个简单示例)
任务:将网络操作从ViewController移至服务,并将它们的创建和依赖项放置在单独的容器中。这是将应用程序分解为层的一个简单而有效的方法。在这个例子中,我们将使用上述示例中的服务和ViewController。
购买服务
protocol IPurchaseService {
func perform(with objectId: String, then completion: (success: Bool)->Void)
}
class PurchaseService {
var baseURL: URL?
var apiPath = "/purchase/"
var apiClient: IAPIClient?
func perform(with objectId: String, then completion: (_ success: Bool) -> Void) {
guard let apiClient = self.apiClient, let url = self.baseURL else {
fatalError("Trying to do something with uninitialized purchase service")
}
let purchaseURL = baseURL.appendingPathComponent(self.apiPath).appendingPathComponent(objectId)
let urlRequest = URLRequest(url: purchaseURL)
self.apiClient.post(urlRequest) { (_, error) in
let success: Bool = (error == nil)
completion( success )
}
}
}
ViewController
class OrderViewController: ViewController {
var purchaseService: IPurchaseService?
var purchaseId: String?
func didClickShopButton(_ sender: UIButton?) {
guard let purchaseService = self.purchaseService, let purchaseId = self.purchaseId else {
fatalError("Trying to do something with uninitialized OrderViewController")
}
self.purchaseService.perform(with: self.purchaseId) { (success) in
self.presenter(showOrderResult: success)
}
}
}
服务依赖配置
class ServiceAssembly: Assembly {
var purchaseService: IPurchaseService {
return define(init: PurchaseService()) {
$0.baseURL = self.apiV1BaseURL
$0.apiClient = self.apiClient
return $0
}
}
var apiClient: IAPIClient {
return define(init: APIClient())
}
var apiV1BaseURL: URL {
return define(init: URL("http://someapi.com/")!)
}
}
这就是我们在ViewController中注入服务的方法
class OrderViewAssembly: Assembly {
lazy var serviceAssembly: ServiceAssembly = self.context.assembly()
func inject(into controller: OrderViewController, purchaseId: String) {
let _:OrderViewController = define(init: controller) {
$0.purchaseService = self.serviceAssembly.purchaseService
$0.purchaseId = purchaseId
return $0
}
}
}
现在您可以在不修改OrderViewController代码的情况下更改服务类。
依赖关系解析类型(平均复杂度示例)
对象图
默认情况下,所有依赖项都通过对象图解析。如果对象已存在于当前对象图的堆栈中,则重新使用它。这允许我们将相同的对象注入到多个对象中,并允许循环依赖项。例如,考虑具有链接A-> B-> C的A、B和C类。(请勿关注RetainCycle)。
class A {
var b: B?
}
class B {
var c: C?
}
class C {
var a: A?
}
这就是配置的外观
class ABCAssembly: Assembly {
var a:A {
return define(init: A()) {
$0.b = self.B()
return $0
}
}
var b:B {
return define(init: B()) {
$0.c = self.C()
return $0
}
}
var c:C {
return define(init: C()) {
$0.a = self.A()
return $0
}
}
}
以下是A类实例两次请求的依赖关系图
var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a
单例
但是,有时您需要创建一个将被到处使用的单个对象,例如:分析系统或存储。我们不推荐使用Singleton类的众所周知的SharedInstance静态属性,因为无法替换它。为此,EasyDi有一个特殊的范围:lazySingleton。带有'lazySingleton'范围的对象创建一次,其依赖项也注入一次。此外,EasyDi不会在创建后改变该对象。例如,我们使B类成为单例。
class ABCAssembly: Assembly {
var a:A {
return define(init: A()) {
$0.b = self.B()
return $0
}
}
var b:B {
return define(scope: .lazySingleton, init: B()) {
$0.c = self.C()
return $0
}
}
var c:C {
return define(init: C()) {
$0.a = self.A()
return $0
}
}
}
var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a
这次,获得了一个对象图,因为 B 实例变成了共享单例。由于我们不使用 'lazySingleton' 范围重新创建(重建)对象,B 实例在 'var a2 = ABCAssembly...' 之后没有改变其依赖项。
原型
有时每个请求都需要一个新的对象。如果我们为我们的示例中的 A 类实例指定 'prototype' 范围,我们将得到
class ABCAssembly: Assembly {
var a:A {
return define(scope: .prototype, init: A()) {
$0.b = self.B()
return $0
}
}
var b:B {
return define(init: B()) {
$0.c = self.C()
return $0
}
}
var c:C {
return define(init: C()) {
$0.a = self.A()
return $0
}
}
}
var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a
结果创建了两个对象图,包含 4 个对象 A 的副本
理解 'prototype' 对象是对象图入口点是非常重要的。如果您在循环中组合原型,依赖关系堆栈将溢出,并且应用程序将崩溃。
针对测试的替代和上下文(一个复杂示例)
在测试时,保持测试独立性非常重要。在 EasyDi 中,汇编上下文提供了这个属性。例如,具有单例对象的集成测试。用法示例
let context: DIContext = DIContext()
let assemblyInstance2 = TestAssembly.instance(from: context)
确保相邻汇编具有相同的上下文非常重要。
class FeedViewAssembly: Assembly {
lazy var serviceAssembly:ServiceAssembly = self.context.assembly()
}
测试的另一个重要部分是模拟和存根,即在对象中定义好的行为。有了已知输入数据,被测试对象会产生已知结果。如果对象没有产生结果,那么测试失败。有关测试的更多信息,请参阅此处。以下是您如何替换要测试对象的依赖项的方法
//Production code
protocol ITheObject {
var intParameter: Int { get }
}
class MyAssembly: Assembly {
var theObject: ITheObject {
return define(init: TheObject()) {
$0.intParameter = 10
return $0
}
}
}
//Test code
let myAssembly = MyAssembly.instance()
myAssembly.addSubstitution(for: "theObject") { () -> ITheObject in
let result = FakeTheObject()
result.intParameter = 30
return result
}
现在 theObject 属性将返回具有另一个 intParameter 的另一种类型的存根对象。
相同的机制可以用于应用程序中的 A / B 测试。例如
let FeatureAssembly: Assembly {
var feature: IFeature {
return define(init: Feature) {
...
return $0
}
}
}
let FeatureABTestAssembly: Assembly {
lazy var featureAssembly: FeatureAssembly = self.context.assembly()
var feature: IFeature {
return define(init: FeatureV2) {
...
return $0
}
}
func activate(firstTest: Bool) {
if (firstTest) {
self.featureAssembly.addSubstitution(for: "feature") {
return self.feature
}
} else {
self.featureAssembly.removeSubstitution(for: "feature")
}
}
}
在这个例子中,为测试创建了一个单独的容器。这个容器创建了功能的第二个变体,并允许启用/禁用功能的替代。
VIPER 中的依赖注入(复杂示例)
有时需要在现有对象中注入依赖项,而其他对象依赖于它。最简单的例子是 VIPER,此时 Presenter 应该添加到 ViewController 中,并应获得对 ViewController 本身的指针。
对于这个案例,EasyDi 有 'keys',你可以用不同的方法返回相同的对象。它看起来像这样
сlass ModuleAssembly: Assembly {
func inject(into view: ModuleViewController) {
return define(key: "view", init: view) {
$0.presenter = self.presenter
return $0
}
}
var view: IModuleViewController {
return definePlaceholder()
}
var presenter: IModulePresenter {
return define(init: ModulePresenter()) {
$0.view = self.view
$0.interactor = self.interactor
return $0
}
}
var interactor: IModuleInteractor {
return define(init: ModuleInteractor()) {
$0.presenter = self.presenter
...
return $0
}
}
}
在这里,为了在 ViewController 中实现依赖关系,使用了 inject 方法,它与 'view' 属性相关联。现在,这个属性返回传递给 'inject' 方法的对象。因此,VIPER 模块组装通过 'inject' 方法启动。
路线图
- 弃用旧的 Swift 版本
- Swift5+
- 弱单例
- 更新文档
- SPM
- 待续...