依赖注入(Dependency Injection)今天我们讨论的内容核心是面向接口编程,我决定还是要从依赖注入开始讲起,因为DI的思想可以说是面向接口编程思想的特殊表现,也可以说是与面向接口编程相辅相成。
先撇开让人头脑发晕的文字定义,我们还是用我们最忠实和伙伴——代码来了解依赖注入。我们先来一个粗略的例子,由浅入深:
我们有一个公交车类(Bus),每天早上6点钟需要发车(work),为其分配对应的司机(Driver),看代码
1 2 3 4 5 6 7 |
@implementation Bus - (void)work { Driver *driver = [[Driver alloc] initWithName:@"张三"]; //dosomething } @end |
在上面这段代码中,Bus对象的运作需要用到Driver对象,因而创建了一个Driver对象,我们称Bus对Driver有一个依赖。这样的强耦合关系会因为日后的变化而给我们带来很多麻烦,不久将来张三师傅辞职了,我们需要修改Bus-work()的代码,也就是说在开发过程中非常不便于单元测试(一是不能方便地更换各种Driver对象,二是如果Driver这个职位创建是耗时操作或者高成本操作,我们并不能使用准备好的Driver实现快速重复测试)。 我们继续:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@implementation Bus @property (strong, nonatomic) Driver *driver; - (instancetype)initWithDriver:(Driver *)driver { self = [super init]; if (self) { self.dirver = driver; } return self; } - (void)work { //dosomething } @end |
以上这段代码我们通过init方法,为Bus对象传入了一个Driver对象,像这种非自己主动初始化依赖,而从外部通过注入点注入依赖的方式,我们就称为依赖注入,而例子中的这种注入的方法称之为构造器注入。明显的,这个场景中Bus和Driver的耦合因此轻了一层。说到解耦,并不是说Bus和Driver之间的依赖关系就不存在了,在Bus的范围内看来,只是将依赖建立从编译期间推迟到了运行期间,毕竟Bus无论如何也是需要Driver提供服务的。对此,这篇文章有一个非常形象的比喻,“依赖就像是系统中的plugin,主系统并不强依赖于任何一个插件,但一旦插件被加载,主系统就应该可以准确调用适当插件的功能”。
类似这样的注入方式还有
- 属性注入
- 方法注入
- 环境上下文注入
- 子类重写方法注入等
不同的只是注入的手段,思想还是一样的。
轻轻地思考
例子说完了,那是不是说我们对所有的依赖都要这样一视同仁,破坏程序的封装性而减轻所有的依赖呢?不,这仅仅是让我们认识依赖注入的思想。但是对于测试驱动开发(TDD),一定量的依赖抽取又是必须的。如果说实在不希望把那么多的拉环暴露出来,又必须贯彻测试驱动开发,objc的这篇文章这么说到:
“This can be done by declaring them in a class category in a separate header file. For example, if we’re dealing with Example.h, then create an additional header ExampleInternal.h. This will be imported only by Example.m and by test code.”
我们可以通过强大的Category,将注入的针口放在Category中,而对应的Category放在一个专门用来测试的header中,思考下这个Category中做了什么?swizzle掉依赖所在的方法,并且执行依赖注入,当然这两者是分开的。
看到这里,是不是有点觉得DI完全就是为了单测服务?我以前也是这么认为的,其实不然,这仅仅是一个简单介绍DI思想的一个例子,层次不同,我们不能从中体验到DI带来的好处。
组件化
也是objc的那篇文章中提到一种叫做“pluggable排插思想”
,用原话来说,如果一个类的initializer需要提供一个id
的参数,说明我们需要为之提供一个遵守foo协议的对象才可以让这个类运作起来,有没有发现DI外衣下的面向接口思想
的肉体?所以说更深层的,DI的一个目标是为了实现组件化架构,DI让依赖更加明显,DI划定了组件的边界和组件的组装方式。
开闭原则(Open-Closed Principle)
这里要带入一个比较重要的思想——OCP,国内比较少笔墨对OCP思想的介绍和强调,他的原文解释是Software entities should be open for extension,but closed for modification,对扩展开放,对修改关闭。也就是说我们对模块的设计,应该满足将来在不可修改源代码的情况下对模块的职能扩展,或者改变模块的行为。单单这句话就能表现出OCP可怕的地位,他迫使我们主动考虑了将来,使应用保证了核心代码的稳定性和对新需求的灵活性。
依赖获取(Dependency Locate)
上面我们理解了依赖注入的基础思想,让依赖显式化,为依赖提供合适的注入点(针口),提升程序的灵活性。带来的结果就是当我们需要更换依赖的时候不需要对使用服务的类(姑且叫作客户类)作代码修改,将提供服务的类(服务类)由注入点注入到客户类中,耦合的确轻了一层,也符合OCP原则,ok现在我们往外跳一层,在实例化客户类的角色上下文中,需要实例化服务类进而完成对客户类注入,服务类的更变必然导致此处代码的修改,这时OCP又要站出来打差评。
此时有必要讲下依赖获取
。既然有注入,当然也应该有获取,但这两者并不是先后执行的两个过程,而是相同目的的同一种操作,换句话说,我们让客户类由被动注入转换成主动获取,继续贯彻的仍然是依赖注入思想。
DL就是在系统中配置一个获取点,客户类依赖于服务类的接口而不直接依赖服务类,客户类根据自身需要从获取点主动获取服务类为其提供服务。理解了DI,对DL的概念肯定是迎刃而解。
我们思考下,客户类只知道获取点,按照道上的规矩交货的对方的身份完全不需要去了解,有没有发现面向接口(POP)的内体又暴露了一点?
更高级的依赖注入
认识完DI的另一种方式依赖获取后,做依赖注入的办法就不仅仅局限于上文列举的几种最基本的依赖注入方式。目前比较主流的有配置文件依赖注入
,反射依赖注入
,例如java中强大的Spring
和移植到.NET平台的Spring.NET
,.NET中自己的Autofac
,他们是结合配置文件和反射工作的,而oc中的objection
我看了下是通过key-Value内存容器来做的DI,如果我自己做的话,还可以使用runtime target-action方式(类似于其他语言的反射),而重型项目中需不需要用到NSInvocation笔者缺乏这方面的经验不敢独断。
下面还是用一个简单的例子来增强对通过配置文件做依赖获取的认识:
最近有看qq浏览器庄延军老师关于内存管理的公开课,就用手Q浏览器更换主题打一个例子吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//定义一个主题接口,让所有主题都实现它 @protocol ItfThemeFactory - (void)drawing; @end //主题 @implementation SpringFactory - (void)drawing { //drawing theme... } @end @implementation SummerFactory - (void)drawing { //drawing theme... } @end //主题工厂Animator @interface ThemeFactoryAnimator : NSObject @property (strong, nonatomic) id themeFactory; @end @implementation ThemeFactoryAnimator - (id)themeFactory { NSString *path = [[NSBundle mainBundle] pathForResource:@"theme" ofType:@"plist"]; NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:path]; NSString *theme = [dict objectForKey:@"theme"]; if ([theme isEqualToString:@"spring"]) { _themeFactory = [[SpringFactory alloc] init]; } else if ([theme isEqualToString:@"summer"]) { _themeFactory = [[SummerFactory alloc] init]; } else { //assert } } 天我们讨论的内容核心是面向接口编程,我决定还是要从依赖注入开始讲起,因为DI的思想可以说是面向接口编程思想的特殊表现,也可以说是与面向接口编程相辅相成。
先撇开让人头脑发晕的文字定义,我们还是用我们最忠实和伙伴——代码来了解依赖注入。我们先来一个粗略的例子,由浅入深:
在上面这段代码中,Bus对象的运作需要用到Driver对象,因而创建了一个Driver对象,我们称Bus对Driver有一个依赖。这样的强耦合关系会因为日后的变化而给我们带来很多麻烦,不久将来张三师傅辞职了,我们需要修改Bus-work()的代码,也就是说在开发过程中非常不便于单元测试(一是不能方便地更换各种Driver对象,二是如果Driver这个职位创建是耗时操作或者高成本操作,我们并不能使用准备好的Driver实现快速重复测试)。 我们继续:
以上这段代码我们通过init方法,为Bus对象传入了一个Driver对象,像这种非自己主动初始化依赖,而从外部通过注入点注入依赖的方式,我们就称为依赖注入,而例子中的这种注入的方法称之为构造器注入。明显的,这个场景中Bus和Driver的耦合因此轻了一层。说到解耦,并不是说Bus和Driver之间的依赖关系就不存在了,在Bus的范围内看来,只是将依赖建立从编译期间推迟到了运行期间,毕竟Bus无论如何也是需要Driver提供服务的。对此,这篇文章有一个非常形象的比喻,“依赖就像是系统中的plugin,主系统并不强依赖于任何一个插件,但一旦插件被加载,主系统就应该可以准确调用适当插件的功能”。 类似这样的注入方式还有
不同的只是注入的手段,思想还是一样的。 轻轻地思考例子说完了,那是不是说我们对所有的依赖都要这样一视同仁,破坏程序的封装性而减轻所有的依赖呢?不,这仅仅是让我们认识依赖注入的思想。但是对于测试驱动开发(TDD),一定量的依赖抽取又是必须的。如果说实在不希望把那么多的拉环暴露出来,又必须贯彻测试驱动开发,objc的这篇文章这么说到:
我们可以通过强大的Category,将注入的针口放在Category中,而对应的Category放在一个专门用来测试的header中,思考下这个Category中做了什么?swizzle掉依赖所在的方法,并且执行依赖注入,当然这两者是分开的。 看到这里,是不是有点觉得DI完全就是为了单测服务?我以前也是这么认为的,其实不然,这仅仅是一个简单介绍DI思想的一个例子,层次不同,我们不能从中体验到DI带来的好处。 组件化也是objc的那篇文章中提到一种叫做 开闭原则(Open-Closed Principle)这里要带入一个比较重要的思想——OCP,国内比较少笔墨对OCP思想的介绍和强调,他的原文解释是Software entities should be open for extension,but closed for modification,对扩展开放,对修改关闭。也就是说我们对模块的设计,应该满足将来在不可修改源代码的情况下对模块的职能扩展,或者改变模块的行为。单单这句话就能表现出OCP可怕的地位,他迫使我们主动考虑了将来,使应用保证了核心代码的稳定性和对新需求的灵活性。 依赖获取(Dependency Locate)上面我们理解了依赖注入的基础思想,让依赖显式化,为依赖提供合适的注入点(针口),提升程序的灵活性。带来的结果就是当我们需要更换依赖的时候不需要对使用服务的类(姑且叫作客户类)作代码修改,将提供服务的类(服务类)由注入点注入到客户类中,耦合的确轻了一层,也符合OCP原则,ok现在我们往外跳一层,在实例化客户类的角色上下文中,需要实例化服务类进而完成对客户类注入,服务类的更变必然导致此处代码的修改,这时OCP又要站出来打差评。 此时有必要讲下 DL就是在系统中配置一个获取点,客户类依赖于服务类的接口而不直接依赖服务类,客户类根据自身需要从获取点主动获取服务类为其提供服务。理解了DI,对DL的概念肯定是迎刃而解。 我们思考下,客户类只知道获取点,按照道上的规矩交货的对方的身份完全不需要去了解,有没有发现面向接口(POP)的内体又暴露了一点? 更高级的依赖注入认识完DI的另一种方式依赖获取后,做依赖注入的办法就不仅仅局限于上文列举的几种最基本的依赖注入方式。目前比较主流的有 下面还是用一个简单的例子来增强对通过配置文件做依赖获取的认识: 最近有看qq浏览器庄延军老师关于内存管理的公开课,就用手Q浏览器更换主题打一个例子吧:
|