依赖注入-什么,为什么和如何?
我们将探索依赖注入,重点是快速的iOS开发,但是这个概念适用于大多数面向对象的语言。 我们还将看到在iOS环境中应用DI的一些实际注意事项。 本文是我深入实施DI并学习各种实践,理论方面的结果。
什么是依赖注入?
“依赖注入”是5美分概念的25美元术语”。
是关于DI的经常重复的格言。
从本质上讲,DI意味着尽可能地通过从代码外部提供对象来代替在代码内部创建对象 。 因此,这个词。
构造函数,属性,方法通常是我们创建对象的地方,可以从外部替换。 因此,我们最终得到3种类型的注射。
- 构造函数注入
- 资产注入
- 方法注入
我们将在“如何..?”部分中探讨这三种类型。
为什么要进行依赖注入?
让我们在一个场景的帮助下进行讨论。
场景-Koala Koder!
假设您有一个具有用户登录名的应用程序。 用户模型struct / class封装了登录的用户数据。
假设您是Koala Koder(一个像树袋熊一样懒惰的程序员)。 您也许提出了一个既快速又肮脏的解决方案。 将用户模型存储在NSUserDefaults中,然后通过属性获取它。 众所周知,苹果是如何喜欢单例类的,所以我们遵循它们使UserModel成为单例。
问题,到处都是问题…
1.单元测试员Vader罢工!
一位高级开发人员突然转为阴暗面,开始抱怨单元测试 。 他/她将不允许未经单元测试的应用通过代码审查。
2.出现疯狂的新用例!
如果这还不够,则应该支持一个新的用例 。 我们的应用程序现在应该支持多个用户 。
3.“让我们移动到”
现在,我们还达到了用户默认设置无法扩展的地步,希望迁移到新的数据序列化方法。 现在,甚至UserProfile中的我们的获取器和设置器也不安全。
为什么会有问题?
因此,我们发现自己陷入了严重的麻烦。 让我们分析为什么。
1.单身人士很难测试
现在,如果要对使用此单例的viewcontroller进行单元测试。
在单元测试中,您将创建一个UserProfile对象,手动调用viewDidLoad或其他方法。 现在,您必须验证是否调用了UserModel.sharedInstance.greet()
。
由于UserModel.sharedInstance是不可变的,因此我们不能用扩展UserModel的模拟类替换它,该类将覆盖greetUser并设置一个可以检查的标志。 因此,我们的测试范围下降了。
2.代码约束内部的实例化会产生强烈的耦合,从而产生有害的约束
在我们的方案中,通过使用单例,我们将代码库绑定到单个UserModel,但是现在我们的应用程序需要多个用户模型。 因此,一般而言,使用单例将很难适应新的用例。 您以为单身汉的想法突然不再那么单身了 。
但是请考虑我们没有使用单例,而是通过在需要的地方调用UserModel()
实例化UserModel。
如果用户配置文件类不了解多用户模型,或者没有其他允许UserModel()返回正确用户模型的全局变量,我们将无法支持多个用户的新用例,这些解决方案都是丑陋的,它们通过提供过多的知识而增加了复杂性类或使用全局变量并放弃面向对象的封装。
3.混凝土类型的使用产生牢固的耦合
考虑到UserProfile,它使用NSUserDefaults,现在假设我们移到coredata来保存数据,由于内部使用了singleton,我们再次遇到麻烦。 我们的UserProfile只需要一种序列化某些数据的方式,不需要知道我们用于序列化的内容 。 这是考虑依赖项注入时要记住的关键见解。
DI救人
DI有助于解决我们在上面遇到的与单元可测试性,单例和强耦合有关的各种问题。
依赖注入怎么办?
因此,我们将通过构造函数将序列化器注入UserModel中。
将依赖项移至构造函数,并尽可能使它成为接口/协议类型,而不是具体的类/结构
因此,我们删除了对userdefaults的单例访问。 而是传递一个序列化程序协议,该协议具有我们需要序列化到构造函数的方法。
由于我们是Koala Koder,因此我们只需在NSUserdefaults中将序列化协议方法设置为相同的方法,因此可以使NSUserDefaults易于遵循
。
现在使用用户默认值作为序列化器,我们执行以下操作。
对于我们喜欢的多用户用例,我们也可以这样做。 (注意:这是急着做的,可能会有一些边缘情况,仅作为示例提供)
因此,现在userModel提取数据,并将数据设置为当前的userID甚至不知道它。 我们还可以使用NSUserDefaults.standard
其他东西来对顶层数据进行序列化。
关键是通过从UserModel
删除替换具体的依赖项NSUserDefaults.standard
并将其交换为Serializing
协议,我们现在可以轻松满足多用户建模的新用例。 这是松散耦合背后的核心思想。
我们现在也可以通过传递Serializing
的Mock实现来对用户模型进行单元测试
依赖项注入仅适用于具体类型。
有时您没有时间,或者确定不必更改所使用的具体类型。 您仍然可以直接输入具体类型,而不必为协议创建而烦恼。
例如:
我们将UserModel的创建移出控制器,而无需为UserModel属性创建任何协议。
我们仍然可以使用普通的模拟对象获得单元可测试性的优势,而且我们可以自由使用我们的多用户序列化UserModel,因此使UserProfile支持多用户模型而无需更改用户配置文件中的任何逻辑。
(但是,如果您必须做一些事来通知数据更改,那是单独的主题)
我的构造函数现在是怪物了
您将遇到的一个常见问题是,您的UIViewController(如果不使用情节提要)或任何您进行DI的类都会变得很大。
这是一件好事,明确指出您的课堂违反了“ 单一职责”规则 。 单一责任规则规定,班级应承担单一责任。
我们可以通过将一些当前依赖项移到新类中,然后将新类作为依赖项传递给UserProfile来解决。
猜猜如果我们能做到这一点,我们将重新发明设计模式MVVM(Nodel View ViewModel)或MVP。 ew! DI刚刚解决了巨大的ViewController问题!
DI可以轻松地帮助您逐步重构代码,以进行更好的设计。
资产注入
我们似乎已经解决了所有上述问题,为什么还要打更多类型的注射呢?
如果您不控制对象的创建,那么最好的选择就是属性注入。 但是,如果不是这种情况,则更喜欢使用构造函数注入。
有时您不创建类的对象,而是通过一些混乱的框架来实现。 例如,如果使用情节提要,则无法执行let userProfileViewController = UserProfile(multiUserModel)
。 您将不得不重构为类似
通过使用隐式解包的Optional属性,并从外部进行设置,我们可以达到构造函数注入的相同效果。 但是并不是完美的,因为userModel
属性可以从外部进行突变。 但是,这是可以得到的。
方法注入
方法注入只是用其参数之一代替实例化方法内部。
运行时注入—工厂模式
有时您想在运行时创建特定类的对象。 但是您只想使用协议类型(或超级类型),而不要使用实际的实现(或子类)。 假设您需要基于用户操作设置的网络服务,但是由于您将在运行时需要它,因此您可能会认为您无法注入它。
但是您可以做的是注入一个工厂网络对象或闭包,并在运行时构造它。 该工厂可以填充具体的Networking类的依赖项,因此您的类可能不知道这一点。
依赖注入—容器和框架
有一些依赖注入框架使依赖注入的工作更加容易。 您可能会说:“哇塞! 等等,我们真的需要依赖注入框架作为依赖吗? 不能做到“。
当我们检查我们对DI所做的工作时,我们正在建立一个以具体类型为节点的图,它们的依赖关系将它们链接在一起。 如果我们完全这样做,则所有依赖项都将源自根对象。
没有框架,我们将做很多复制粘贴编码。 如果我们的应用程序在多个区域中使用网络协议类型,则我们必须在任何地方键入相同的具体实现,并在任何地方填写网络类的每个依赖项。
例如:
为了简化此过程,我们可以构建一些存储依赖项类型列表及其具体实现的列表。 我们将有一个映射表。
我们可以将协议依赖类型注册为具体的实现,例如将Networking,X,Y分别注册为NetworkService,XService,YService。 或将具体类型映射到其具体实现,该实现可以是完全相同的类型(例如A),也可以是子类,例如将Z映射到ZSubClass。 现在,容器要做的就是必须创建A时,它会自动从表中解析A的每个依赖关系。
您可以实现一个容器,也可以使用可以为您执行此操作的DI框架。
容器还允许您自动填充属性注入,处理依赖项的生命周期,例如将其标记为单例,以便您的整个容器仅创建一个该类型的对象,并使用其他精美功能来简化您的生活。
Dip框架-Swift
在现在迅速退出的DI框架中,我建议Dip。 Dip具有一些漂亮的功能,可以使您的DI疼痛减轻。
浸洗功能
- 范围 。 Dip支持5个不同的范围(或生命周期策略): Unique , Shared , Singleton , EagerSingleton , WeakSingleton ;
- 自动接线和自动注入 。 Dip可以推断出注入到构造函数中的组件的依赖项,并自动解析它们以及注入属性的依赖项。
- 解决可选问题 。 Dip能够解析定义为可选的构造函数或属性依赖项。
- 类型转发 。 您可以注册同一个工厂来解析由单个类实现的不同类型。
- 循环依赖 。 如果您遵循一些简单的规则,Dip将能够解决循环依赖性。
- 情节提要集成 。 您可以轻松将Dip与情节提要和Xib一起使用,而无需在视图控制器的代码中引用容器。
- 命名定义 。 您可以使用标签注册不同的工厂以使用相同的协议或类型。
- 运行时参数 。 您可以注册接受最多6个运行时参数的工厂(并在需要时扩展它);
- 易于配置和代码生成 。 没有复杂的容器层次结构,没有不需要的功能。 厌倦了用手写所有注册表? 有一个很酷的代码生成器将为您创建它们。 您唯一需要做的就是用一些注释来注释代码。
- 弱类型的组件 。 当在编译时未知“弱”类型时,Dip可以解决它们。
- 线程安全 。 注册和解析组件是线程安全的;
- 有用的错误消息和配置验证 。 您可以验证容器配置。 如果某些问题在运行时无法解决,则Dip会引发一个错误,完全描述问题;
浸入式注解
Java具有良好的框架,例如Dagger和Guice,它们使用注释使工作变得比swift更简单。 Dip允许您在注释中保留依赖项的批注,还可以从中为DI生成代码。 多么酷啊?
斯威夫特的穷人DI
尽管我建议使用框架方法,但假设您不想一开始就使用框架,而仍然希望进行DI,而不是害怕。 您可以使用Swift的默认参数进行构造函数/方法注入,而只需使用变量属性进行属性注入。 您可以维护一个单独的DI单例并使用该单例来填充依赖项。
https://gist.github.com/ceb8ffbf229762420e7d02c69388eeff
但是,如果使用上述方法,请注意循环依赖关系。
结论
因此,总结来说,DI很棒。 它使您可以逐步使您的代码更好地除去单例,使您的代码模块化,可测试,并允许您发展良好的设计模式。 在您现有的代码库中进行尝试,这将是重构旧有OOP代码的最简单方法之一,而无需首先修改内部逻辑。如果您有任何疑问,建议,建设性的批评,请在下面评论。