学习依赖注入

尽管这种模式已经存在了十多年了,并且有很多文章(和翻译),但是,仍然有越来越多的争议,评论,问题和各种实现。

背景知识
2004年,Martin Fowler撰写了著名的文章“ 控制容器的反转和依赖注入模式 ”,其中描述了上述模式及其在Java中的实现。 从那时起,这种模式就引起了广泛的争论和实施。 在移动开发中,尤其是在iOS上,这存在很大的延迟。 哈伯(Habré)上很好的翻译了他们的作者,祝他们好运和灿烂的业力。

即使在集线器上也有足够的信息,但是到处都有讨论如何做的事实,但是实际上却无处可寻- 为什么,这启发了我写这篇文章。 如果您不知道它的用途和作用,是否有可能创建一个好的架构? 可以考虑某些原则和明确的趋势-这将有助于最大程度地减少不可预见的问题,但理解会更好。

依赖注入是一种设计模式,其中用于创建对象的字段或参数是在外部配置的。

知道许多内容仅限于阅读第一段,因此我更改了这篇文章。
尽管在许多来源中都可以找到DI的这种“定义”,但它是模棱两可的,因为它使用户认为注入可以代替对象的创建/初始化,或者至少是非常积极地参与到此过程中。 当然,没有人会禁止这样做DI的实现。 但是,DI可以成为创建提供输入参数的对象的被动包装。 在这样的实现中,我们获得了另一个抽象层次和出色的职责分离:对象本身负责其初始化,注入实现了数据的存储并为它们提供了应用程序模块。

现在按顺序详细介绍所有内容。
我将从一个简单的例子开始,为什么需要新的模式,为什么某些旧模式的范围变得非常有限?

在我看来,大部分更改是通过大量引入自检引入的。 对于那些积极编写自动测试的人来说,这篇文章显然是白色的一天,您无法进一步阅读。 只有您无法想象有多少人不写它们。 我了解到,小公司和初创公司没有这些资源,但是不幸的是,大公司经常遇到更多的优先问题。

这里的推理非常简单。 假设您正在使用参数ab测试一个函数,并且希望得到结果x 。 在某些时候,您的期望没有实现,该函数返回结果y ,并且花费了一些时间后,您在该函数内部发现了一个单例,这在某些状态下将函数的结果带到另一个值。 这个单身人士被称为隐性成瘾 ,并且在这种情况下以各种可能的方式拒绝使用它。 不幸的是,您不会在这首歌中扔掉任何单词,否则它将是一首完全不同的歌曲。 因此,我们将单例作为函数中的输入变量。 现在我们有3个输入变量abs 。 一切似乎都很明显:我们更改了参数-我们得到了明确的结果。

虽然我不会举一些例子。 而且,我们不仅在谈论类中的函数,它还是一个示意性的参数,也可以用于创建类,模块等。

单例笔记
注1.如果由于对单例模式的批评而决定例如用UserDefaults替换它,那么就此情况而言,相同的隐式依赖关系也隐约可见。

注2:仅由于自动测试而在函数体内使用单调是不值得的,这并不是完全正确的说法。 通常,从编程的角度来看,使用相同的输入功能会产生不同的结果并不完全正确。 只是在自动测试中,这个问题更加明显了。

补充上面的示例。 您有一个包含9个用户设置(变量)的对象,例如,读取/编辑/签名/打印/转发/删除/锁定/执行/复制文档的权限。 您的函数仅使用这些设置中的三个变量。 您传递给该函数什么:将9个变量作为一个参数的整个对象,或仅将三个单独的参数作为三个必需的设置? 很多时候,我们放大传输的对象,以便不设置很多参数,也就是说,我们选择第一个选项。 该方法将被视为“不合理的广泛依赖关系”的转移。 正如您已经猜到的那样,出于自我测试的目的,最好使用第二个选项并仅传递使用的那些参数。

我们得出2个结论:
-函数应在输入处接收所有必要的参数
-函数不应接收不必要的输入参数

我们想要最好的-但是有一个带有6个参数的函数。 假设一切都在函数内部井然有序,但是有人应该负责为函数提供输入参数。 正如我已经写的,我的推理是粗略的。 我的意思不仅是普通的类函数,还包括模块初始化/创建函数(vip,viper,数据对象等)。 在这种情况下,我们改述以下问题:谁应提供用于创建模块的输入参数?

一种解决方案是将这种情况转移到调用模块。 但是,事实证明,调用模块需要传递子代的参数。 这带来以下并发症:

首先,我们早些时候决定避免“过分依赖”。 其次,您不必费力去理解会有很多参数,并且每次添加子模块时都要对其进行编辑非常繁琐,甚至考虑删除子模块也很麻烦。 顺便说一下,在某些应用程序中,根本不可能建立模块的层次结构:查看任何社交网络:个人资料->朋友->朋友的个人资料->朋友的朋友等。 第三,可以在此主题上回顾SOLI D原则:“顶层模块独立于底层模块”

这引起了在单独的结构中进行模块的创建/初始化的想法。 然后是时候写几行作为示例了:

class AccountList { public func showAccountDetail(account: String) { let accountDetail = AccountDetail.make(account: account) // to do something with accountDetail } } class AccountDetail { private init(account: String, permission1: Bool, permission2: Bool) { print("account = \(account), p1 = \(permission1), p2 = \(permission2)") } } extension AccountDetail { public static func make(account: String) -> AccountDetail? { let p1 = ... let p2 = ... return AccountDetail(account: account, permission1: p1, permission2: p2) } } 

在该示例中,帐户列表AccountList中有一个模块,该模块调用有关帐户AccountDetail的详细信息模块。

要初始化AccountDetail模块,需要3个变量。 变量Account AccountDetail从父模块收到,注入了变量Permission1,Permission2。 由于注入,带有发票详细信息的模块调用如下所示:

 let accountDetail = AccountDetail.make(account: account) 

代替

 let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2) 

帐户列表的父模块AccountList将免去传递带有他不了解的权限的参数的义务。

我在类扩展中将注入(汇编)实现呈现为静态函数。 但是实施方式可以由您任意决定。

如我们所见:

  1. 模块收到了必要的参数。 可以在所有值集上安全地测试其创建和执行。
  2. 这些模块是独立的,不需要为孩子转移任何东西或仅需要最小的转移。
  3. 模块不执行提供数据的工作;它们使用现成的数据(p1,p2)。 因此,如果要更改存储或数据提供中的某些内容,则不必更改模块的功能代码(以及其自动测试),而只需要更改装配系统本身或更改装配扩展名即可。

依赖项注入的本质是流程的构建,其中,当从另一个模块调用一个模块时,一个独立的对象/机制将数据传输(注入)到被调用的模块。 换句话说,被调用的模块是在外部配置的。

有几种配置方法:
构造注入属性注入界面注入
对于Swift:
初始化注入属性注入方法注入

最常见的是构造函数(初始化)注入和属性。
重要提示:在几乎所有来源中,建议首选构造函数注入。 比较构造函数/初始化注入和属性注入:

 let account = .. let p1 = ... let p2 = ... let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2) 

优于

 let accountDetail = AccountDetail() accountDetail.account = .. accountDetail.permission1 = ... accountDetail.permission2 = ... 

第一种方法的优点似乎很明显,但是出于某种原因,有些人将注入理解为配置了已创建的对象并使用了第二种方法。 我是第一种方法:

  1. 设计者的创造保证了有效的物品;
  2. 对于“属性注入”,尚不清楚是否有必要在创建以外的地方测试属性的更改;
  3. 在使用可选性的语言中,要实现属性注入,您需要将字段设置为可选,或提供巧妙的初始化方法(惰性方法并不总是有效)。 过多的可选项增加了不必要的代码和不必要的测试套件。

但是,直到我们摆脱了一些依赖,我们才将它们从一个肩膀转移到另一个肩膀。 一个逻辑问题是从程序集本身中获取数据(示例中的make函数)。

在汇编机制中使用单调不再导致具有隐藏依赖性的上述问题,因为 您可以使用任何数据集测试模块的创建。
但是在这里,我们面临着单身人士的另一个缺点:处理不善(您可能会带来很多可恶的论点,但懒惰)。 与任何人类比,将您存储的/单调分散在程序集中是不好的,因为它们分散在功能模块中。 但是,即使这样的重构也已经是实现卫生的第一步,因为这样您就可以在几乎不影响代码和模块测试的情况下恢复程序集中的顺序。

如果您想进一步简化架构,以及测试过渡和组装工作,您将需要做更多的工作。

DI概念使我们能够将所有必要的数据存储在容器中。 这很方便。 首先,保存(注册)和接收(解析)数据分别通过单个容器对象进行,因此更易于管理和测试数据。 其次,您可以考虑数据之间的依赖性。 在包括swift在内的许多语言中,都有现成的依赖项管理容器,通常依赖项形成一棵树。 我将不会列出其余的优缺点,您可以在我在博文开头发布的链接上了解它们。

这是使用容器的程序集的外观。

 import Foundation import Swinject public class Configurator { private static let container = Container() public static func register<T>(name: String, value: T) { container.register(type(of: value), name: name) { _ in value } } public static func resolve<T>(service: T.Type, name: String) -> T? { return container.resolve(service, name: name) } } extension AccountDetail { public static func make(account: String) -> AccountDetail? { if let p1 = Configurator.resolve(service: Bool.self, name: "permission1"), let p2 = Configurator.resolve(service: Bool.self, name: "permission2") { return AccountDetail(account: account, permission1: p1, permission2: p2) } else { return nil } } } // -   ,         //   ()  Configurator.register(name: "permission1", value: true) Configurator.register(name: "permission2", value: false) ... 

这是一个可能的实现示例。 该示例使用了不久前诞生的Swinject框架。 Swinject允许您创建用于自动依赖性管理的容器,还允许您为Storyboard创建容器。 可以在raywenderlich的示例中找到有关Swinject的更多信息。 我真的很喜欢这个网站,但是这个例子并不是最成功的,因为它只考虑在自动测试中使用容器,而容器应该放在应用程序的体系结构中。 您可以在代码中自己编写一个容器。

谢谢大家 希望您不要因为阅读本文而感到无聊。

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


All Articles