EasyDi 1.6.1

EasyDi 1.6.1

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2019年11月
SPM支持 SPM

Maintained by Andrey Zarembo, Alex Markov, dvi.



EasyDi 1.6.1

  • By
  • Andrey Zarembo 和 Alexey Markov

EasyDi

CI Status Version Carthage Compatible License Platform Swift Version

适用于快速开发的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
  • 待续...