依赖注入与DITranquillity

依赖注入是一种相当流行的模式,它使您可以灵活地配置系统并正确建立此系统组件之间的依赖关系。 通过键入,Swift允许您使用方便的框架,通过它们可以非常简短地描述依赖关系图。 今天,我想谈一谈这些框架之一DITranquillity


本教程将介绍以下库功能:


  • 类型注册
  • 初始化部署
  • 嵌入变量
  • 组件循环依赖
  • 将库与UIStoryboard一起UIStoryboard

组件说明


该应用程序将包含以下主要组件: ViewControllerRouterPresenterNetworking这些是任何iOS应用程序中非常常见的组件。


组件结构

ViewControllerRouter将循环引入。


准备工作


首先,在Xcode中创建一个Single View应用程序,使用CocoaPods添加DITranquillity 。 创建必要的文件层次结构,然后向Main.storyboard添加第二个控制器,并使用StoryboardSegue连接。 结果,应获得以下文件结构:


档案结构

在类中创建依赖项,如下所示:


组件声明
 protocol Presenter: class { func getCounter(completion: @escaping (Int) -> Void) } class MyPresenter: Presenter { private let networking: Networking init(networking: Networking) { self.networking = networking } func getCounter(completion: @escaping (Int) -> Void) { // Implementation } } 

 protocol Networking: class { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) } class MyNetworking: Networking { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) { // Implementation } } 

 protocol Router: class { func presentNewController() } class MyRouter: Router { unowned let viewController: ViewController init(viewController: ViewController) { self.viewController = viewController } func presentNewController() { // Implementation } } 

 class ViewController: UIViewController { var presenter: Presenter! var router: Router! } 

局限性


与其他类不同, ViewController不是由我们创建的,而是由UIStoryboard.instantiateViewController实现中的UIKit库UIStoryboard.instantiateViewController ,因此,使用情节UIStoryboard.instantiateViewController ,我们无法使用初始化程序将依赖项注入UIViewController的继承人中。 UIViewUITableViewCell的继承人也是如此。


请注意,隐藏在协议后面的对象嵌入在所有类中。 这是实现依赖关系的主要任务之一-使依赖关系不依赖于实现,而依赖于接口。 将来,这将有助于为重用或测试组件提供不同的协议实现。


依赖注入


创建系统的所有组件之后,我们继续进行对象之间的连接。 在DITranquillity中,起点是DIContainer ,它使用container.register(...)方法添加注册。 要将依赖关系分为多个部分,必须DIPart DIFrameworkDIPart 。 为了方便起见,我们将仅创建一个ApplicationDependency类,该类将实现DIFramework并将用作所有依赖项的注册位置。 DIFramework接口仅要求您实现一种方法DIFramework load(container:)


 class ApplicationDependency: DIFramework { static func load(container: DIContainer) { // registrations will be placed here } } 

让我们从没有依赖性的最简单的注册开始MyNetworking


 container.register(MyNetworking.init) 

该注册使用通过初始化程序的实现。 尽管组件本身没有依赖关系,但必须提供初始化程序以使库清楚地知道如何创建组件。


同样,注册MyPresenterMyRouter


 container.register1(MyPresenter.init) container.register1(MyRouter.init) 

注意:请注意,不是register使用,而是register1 。 不幸的是,这是必要的,以指示对象在初始化程序中是否只有一个依赖项。 也就是说,如果存在0个或两个或多个依赖关系,则只需要使用register 。 此限制是Swift 4.0及更高版本的错误。


现在该注册我们的ViewController 。 它不是通过初始化程序而是直接将对象注入变量,因此注册描述会更多。


 container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) 

\.presenter格式的语法为SwiftKeyPath,因此可以简洁地实现依赖关系。 由于RouterViewController周期性地相互依赖,因此必须使用cycle: true明确指出这一点。 库本身可以解决这些依赖关系而无需明确指示,但是引入了此要求,以便阅读该图的人员可以立即了解到依赖关系链中存在循环。 还要注意, 不是使用 ViewController.init而是使用 ViewController.self 。 上面在“ 限制”部分中对此进行了描述。


还必须使用特殊方法注册UIStoryboard


 container.registerStoryboard(name: "Main") 

现在,我们已经描述了一个屏幕的整个依赖图。 但是,尚无访问此图的权限。 您必须创建一个DIContainer ,以允许您访问其中的对象。


 static let container: DIContainer = { let container = DIContainer() // 1 container.append(framework: ApplicationDependency.self) // 2 assert(container.validate(checkGraphCycles: true)) // 3 return container }() 

  1. 初始化容器
  2. 向其添加图形描述。
  3. 我们检查是否做对了所有事情。 如果出错,则应用程序将不会在解决依赖关系期间崩溃,而是在创建图形时立即崩溃

然后,您需要使容器成为应用程序的起点。 为此, AppDelegatedidFinishLaunchingWithOptions实现didFinishLaunchingWithOptions方法,而不是在项目设置Main.storyboard指定为启动点。


 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let storyboard: UIStoryboard = ApplicationDependency.container.resolve() window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true } 

发射


第一次启动时,将发生丢弃,并且由于以下原因,验证将失败:


  • 容器将找不到类型RouterPresenterNetworking ,因为我们仅注册了对象。 如果我们不希望访问实现,而是访问接口,则必须显式指定接口
  • 容器不了解如何解决循环依赖性,因为有必要明确指出每次图解析时都不应重新创建哪些对象。

解决第一个错误很简单-有一个特殊的方法可以让您指定容器中的方法在哪些协议下可用。


 container.register(MyNetworking.init) .as(check: Networking.self) {$0} 

如下描述注册,我们说:可通过Networking协议访问MyNetworking对象。 必须对协议下隐藏的所有对象执行此操作。 {$0}添加用于编译器正确检查类型。


第二个错误要复杂一些。 必须使用所谓的scope ,该scope描述了对象创建的频率和寿命。 对于每个参与循环依赖的注册,您必须指定一个等于objectGraphscope 。 这将使容器清楚地知道,在解析过程中,有必要重用相同的已创建对象,而不是每次都创建。 因此,结果是:


 container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) .lifetime(.objectGraph) container.register1(MyRouter.init) .as(check: Router.self) {$0} .lifetime(.objectGraph) 

重新启动后,容器成功通过验证,并且我们的ViewController随创建的依赖关系一起打开。 您可以在viewDidLoad放置一个断点并确定。


屏幕之间的过渡


接下来,创建两个小类SecondViewControllerSecondPresenter ,将SecondViewController添加到情节SecondViewController中,并在它们之间创建一个标识符为"RouteToSecond"Segue ,使您可以从第一个打开第二个控制器。


为每个新类向我们的ApplicationDependency添加两个注册:


 container.register(SecondViewController.self) .injection(\.secondPresenter) container.register(SecondPresenter.init) 

无需指定.as ,因为我们没有将SecondPresenter隐藏在协议后面,而是直接使用实现。 然后,在第一个控制器的viewDidAppear方法中,我们调用performSegue(withIdentifier: "RouteToSecond", sender: self) ,启动,打开第二个控制器,应在其中附加secondPresenter依赖项。 如您所见,容器看到了从UIStoryboard创建的第二个控制器,并成功放下了依赖项。


结论


该库使您可以方便地使用循环依赖项,情节提要,并充分利用Swift中的自动类型推断,从而提供了一种非常简短而灵活的语法来描述依赖关系图。


参考文献


github库中的完整示例代码


github上的 DITranquillity


英文文章

Source: https://habr.com/ru/post/zh-CN450788/


All Articles