依赖注入
从一个例子开始,比如说写了这样一个方法: - (NSNumber *)nextReminderId
{
NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:@"currentReminderId"];
if (currentReminderId) {
// 增加前一个 reminderId
currentReminderId = @([currentReminderId intValue] + 1);
} else {
// 如果还没有,设为 0
currentReminderId = @0;
}
// 将 currentReminderId 更新到 model 中
[[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:return currentReminderId;
}
如何针对这个方法编写单元测试呢?这里需要注意一点,该方法中操作了一个不属于其控制的对象 容我赘述,就这个例子展开说,虽然这里我使用了? 目前此类单元测试的最大障碍是,如何在你想要测试的代码之外的地方处理这种依赖关系。依赖注入 (dependency injection,简称 DI) 这一范畴内就有一系列方法专门用于解决此类问题。 依赖注入的几种形式其实一提到 DI,很多人会直接想到依赖注入框架或者是控制反转 (Inversion of Control 简称 IoC) 容器。请把这些概念都暂且搁置,我会在后面的 FAQ (常见问题) 中做说明。 现行有很多技术可以处理在依赖中注入某些东西这件事情。比如说 Objective-C runtime 中的 swizzling 就是其一,swizzling 可以在运行时动态地将方法进行替换。当然也有人提出质疑,他们觉得?swizzling 的存在让 DI 变得无关紧要,甚至应尽量避免使用 DI。但是我更倾向于那些使依赖关系能够清晰化的代码,因为这样更便于观察它们 (并且促使我们去处理那些由于依赖过于复杂而导致的变坏或者错误的代码)。 接下来我们快速了解一下 DI 的形式。其中除一个以外,其他的例子都来自于 Mark Seemann 的?Dependency Injection in .Net 构造器注入注意:尽管 Objective-C 本身没有所谓的构造器而是使用初始化方法,但因为构造器注入是 DI 的标准概念,放到各种语言中也是普遍适用的,所以我还是准备用构造器注入这个词来代指初始化注入。 构造器注入,即将某个依赖对象传入到构造器中 (在 Objective- C中指 designated 初始化方法) 并存储起来,以便在后续过程中使用: @interface Example ()
@property (nonatomic,strong,145)">readonly) NSUserDefaults *userDefaults;
@end
@implementation Example
- (instancetype)initWithUserDefaults:(NSUserDefaults *userDefaults)
{
self = [super init];
if (self) {
_userDefaults = userDefaults;
}
return self;
}
@end
可以用实例变量或者是属性来存储依赖对象。上面的例子中用一个只读的属性来存储,防止依赖对象被篡改。 对? 至此,这个类中每一处要使用单例? ###属性注入
这里不会给出具体的 swizzling 的例子;相关的资源有很多,感兴趣的读者可以自行查找。这边要说明的就是 swizzling?确实可以用于 DI。在以上的对 DI 形式的简单介绍后,我们会对它们各自的优缺点做进一步的对比分析,请大家继续阅读。 抽取和重写调用 最后要说的这个技术点不在 Seemann 书中所涉及的 DI 形式讨论的范畴。关于抽取和重写调用来自于 Michael Feathers 的?Working Effectively With Legacy Code。下面介绍一下如何将这个概念应用到我们的 步骤 1:随便找一处对? 步骤 2:将其他所有对? 修改后的代码如下: 妥当完成后,进入最后一步:
由于人们经常会对特定实例存在着固有认识,所以还应尽量避免潜意识中对使用属性注入的倾向性。另外,请确定默认值不会引用到其他库的代码。否则,当前的类的使用者还必须得去引用对应的库,这样的设计就违背了松耦合原则 (用 Seemann 的概念来解释就是,这属于内部默认和外部默认的区别)。 假如所依赖的对象针对每次调用都会有所不同的话,使用方法注入会比较好。一个例子是对调用点来说,可能会涉及到特定应用上下文条件的时候,比如基于一个随机数,或者是当前时间等。 好比一个方法依赖于当前时间。不建议直接调用? (虽然对 Objective-C 来说,不需要使用 procotols 也能很好的利用测试置换来做重复性测试,但我还是推荐大家阅读一下 J.B. Rainsberger 的"Beyond Mock Objects"。这篇文章从一个有趣的应用场景出发,由一个日期注入问题引发了一系列关于设计和重用的很详实的讨论。) 如果依赖对象在底层有多处应用,这就极有可能产生横切问题。若继续将依赖对象向上层传递,尤其是还无法预知这个对象将会在什么时候使用的话,便会促生干扰代码。举几个可能产生这样问题的例子:
[NSUserDefaults standardUserDefaults] [NSDate date] 这类场景下推荐适用环境上下文方式。由于是影响全局的上下文,使用完毕后,别忘了要将其还原。比如你用 swizzle 替换了一个方法,需要在? 尽量不要自己去 swizzling,推荐使用那些现成的、专注于解决与你要处理的问题类似的环境上下文的库。比如:
鉴于抽取和重写调用的方法使用简单,效果强大,你可能会采取能用则用的态度。但是由于这种方式需要配备特定的测试子类,这样就会相应的增加了测试的脆弱性。因为可以避免对依赖对象的调用点进行修改,通常来说,这种方式对有点年头的代码非常有效。 FAQ“该用哪种 DI 框架?”我对那些刚开始使用 mock 对象的朋友们的建议是应尽量避免使用 mock 框架,这样你会对各个步骤和细节有更好的理解。同样地,我建议那些刚开始使用 DI 的朋友也不要使用任何 DI 框架。在不依靠 DI 框架的情况下,对 DI 的理解会更纯粹,会更明白自己该做什么,怎么做。 事实上,很可能不知不觉中你已经在使用 DI 框架了!它就是Interface Builder。IB其实不仅仅是UI设计器,任何属性都可以通过将其声明为 IBOutlets 来赋值。当通过 IB 创建的 View 初始化的时候,可以一并创建通过 IB 声明的 Object (因为利用 IB 不仅可以创建 UI 对象还可以创建 “Object” (NSObject 类型,代表 icon 是个纯黄色立方体),那么当 IB 文件初始化的时候,IB 上定义的对象也会随之初始化)。2009年,Eric Smith 在其文章“Dependency Inversion Principle and iPhone”?中将 Interface Builder 称为是自己 “一直以来最偏爱的 DI 框架”,文中同时给出了如何用 Interface Builder 做依赖注入的例子。 如果你觉得 Interface Builder 还不足以应付 DI 工作,还需要使用其他 DI 框架,怎么选择才合适呢?我的建议是:慎选那些需要改变你自己的代码才能使用的框架。比如说继承某个东西,实现某个接口,或者添加什么注解等等,这些都会将你的代码和某种特定实现捆绑在一起 (这与 DI 的根本设计哲学相悖)。反过来,应尽量去用那些不需要浸染你的类即可以从外部连接的框架,至于说是 DSL 方式还是代码方式都无所谓。 “不想公开全部的 Hooks”对于以上几种公开注入点的注入方式,比如说初始化注入,属性注入,以及方法参数注入等都会让人有一种破坏程序封装性的感觉。我们总有一种想要掩盖依赖注入衔接点的想法,这是可以理解的,因为我们知道这些衔接点是专门为单元测试准备的,它们并不属于 API 业务范畴。所以可以将它们声明在 category 中,并且放到一个单独的头文件里。以上面的 Example.h 为例,再添加一个单独的头文件 ExampleInternal.h。这个头文件只会被 Example.m 和相应的测试代码引用。 在采纳这个方法去实践之前,我还要讨论一下关于 DI 会导致破坏程序封装原则的这个问题。其实 DI 的目标就是让依赖更加明显。我们界定了组件的边界和它们之间的组装方式。举例来说,如果一个类的某个初始化方法中含有一个类型为? 如果觉得公开依赖会很繁冗,先看看是否符合以下的场景:
DI 不仅仅是测试我最初决定钻研DI是因为在执行测试驱动开发 (TDD),而在 TDD 的过程中有一个很纠结的问题会时常跳出来:“对于这个实现,如何编写单元测试?”。后来我发现其实 DI 本身是在彰显一个更高层面的概念:代码组成了模块,模块拼接构建成了应用本身。 使用这种方法有很多益处。Graham Lee 在文章?"Dependency Injection,iOS and You" 中这样描述:“适应新需求,解决bug,增加新功能,单独测试组件。” 所以当我们在编写单元测试的时候用到 DI,应该回想一下上文所提到的更高层面的概念。请将可插拔模块牢记吧。它会影响你的很多设计决策,并且引导你去理解更多的 DI 模式和原则。 原文?Dependency Injection (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |