代码的一针强心剂——依赖注入

移动开发
在许多程序设计语言里,比如Java,C#,依赖注入(DI)都是一种较流行的设计模式,但是它在Objective-C中没有得到广泛应用。本文旨在用 Objective-C的例子对依赖注入进行简要介绍,同时介绍 Objective-C 代码中使用依赖注入的实用方法。尽管文章主要针对Objective-C,但是提到的所有概念对Swift同样适用。

[[151314]]

什么是Dependency Injection(依赖注入)?

在许多程序设计语言里,比如Java,C#,依赖注入(DI)都是一种较流行的设计模式,但是它在Objective-C中没有得到广泛应用。本文旨在用 Objective-C的例子对依赖注入进行简要介绍,同时介绍 Objective-C 代码中使用依赖注入的实用方法。尽管文章主要针对Objective-C,但是提到的所有概念对Swift同样适用。

依赖注入的概念十分简单:一个对象应该通过依赖传递获得,而不是创建他们本身。推荐Martin Fowler的 excellent discussion on the subject 作为背景材料阅读。

依赖可以通过initializer(初始化器)(或者constructor(构造器))或者属性(set方法)传递给对象。它们通常被称为"constructor injection" 和 "setter injection"。(构造器注入和 set方法注入)

Constructor Injection:

  1. - (instancetype)initWithDependency1:(Dependency1 *)d1 
  2. dependency2:(Dependency2 *)d2; 

Setter Injection:

  1. @property (nonatomic, retain) Dependency1 *dependency1; 
  2. @property (nonatomic, retain) Dependency2 *dependency2; 

根据Fowler的描述,一般情况下,首选构造器注入,在构造函数注入不适合的情况下才选择setter注入。虽然使用构造函数注入时,很可能还是要给这些依赖定义属性,但你可以给这些属性设置成read only从而简化你的对象API。

为什么要使用依赖注入?

使用依赖注入有很多优点:

  • 1. 依赖申明清晰。 一个对象需要进行的操作变得一目了然,同时也容易消除危险的隐藏依赖,比如全局变量。
  • 2.组件化。 依赖注入提倡composition over inheritance,以提高代码的重用性。
  • 3. 更易定制。 当创建对象的时,在特殊情况下更易对对象进行部分的定制。
  • 4. 明确从属关系。 特别是在使用构造器依赖注入时,对象所有权规则严格执行--可以建立一个直接非循环的对象图。
  • 5.易测试性。 依赖注入比其他方法更能提高对象的易测试性。因为通过构造器创建这些对象很简单,也没有必要管理隐藏的依赖。此外,模拟依赖变得简单,从而可以把测试集中在被测试的对象上。

在代码中使用依赖注入

你的代码库可能还没有使用依赖注入设计模式,但是转换一下很简单。依赖注入很好的一点就是你不需要让整个工程的代码全都采取该模式。相反,你可以在代码库的特定区域运用然后从那边扩展开来。

二级各种类的注入

首先,把类分为两种:基本类型和复杂类型。基本类型是没有依赖的,或者是只依靠其他基本类型。基本类型基本不用被继承,因为他们功能清晰不变,也不需要链接外部资源。许多基本类型都是从Cocoa 自身获得的,比如NSString, NSArray, NSDictionary, and NSNumber.

复杂类型就相反了。它们有复杂的依赖,包括应用级别的逻辑(需要修改的部分),或者访问额外的资源,例如磁盘,网络或者全局内存服务。应用中绝大多数类都是复杂的,包括几乎所有的控制器对象和模型对象。很多cocoa类型也很复杂,例如NSURLConnection or UIViewController.。

根据以上分类情况,想要使用依赖注入模式最简单的方法是先选择应用中一个复杂的类,找到类中的初始化其他复杂对象的地方(找"alloc]init"或者"new"关键字)。将类中引进依赖注入,改变这一实例化对象作为初始化参数在类中传递而不是类初始化对象本身。

在初始化时分配依赖

让我们来看一个例子,子对象(依赖)在母体的初始化函数中被初始化。原始的代码如下:

  1. @interface RCRaceCar () 
  2.  
  3. @property (nonatomic, readonly) RCEngine *engine; 
  4.  
  5. @end 
  6.  
  7. @implementation RCRaceCar 
  8.  
  9. - (instancetype)init 
  10. ... 
  11. // Create the engine. Note that it cannot be customized or 
  12. // mocked out without modifying the internals of RCRaceCar. 
  13. _engine = [[RCEngine alloc] init]; 
  14.  
  15. return self; 
  16.  
  17. @end 

依赖注入做了小的修改:

  1. @interface RCRaceCar () 
  2.  
  3. @property (nonatomic, readonly) RCEngine *engine; 
  4.  
  5. @end 
  6.  
  7. @implementation RCRaceCar 
  8.  
  9. // The engine is created before the race car and passed in 
  10. // as a parameter, and the caller can customize it if desired. 
  11. - (instancetype)initWithEngine:(RCEngine *)engine 
  12. ... 
  13.  
  14. _engine = engine; 
  15.  
  16. return self; 
  17.  
  18. @end 

惰性初始化依赖

有一些对象可能一段时间后才用到,或者初始化之后才会用到,或者永远也不会用到。没有用依赖注入之前的例子:

  1. @interface RCRaceCar () 
  2.  
  3. @property (nonatomic) RCEngine *engine; 
  4.  
  5. @end 
  6.  
  7. @implementation RCRaceCar 
  8.  
  9. - (instancetype)initWithEngine:(RCEngine *)engine 
  10. ... 
  11.  
  12. _engine = engine; 
  13. return self; 
  14.  
  15. - (void)recoverFromCrash 
  16. if (self.fire != nil) { 
  17. RCFireExtinguisher *fireExtinguisher = [[RCFireExtinguisher alloc] init]; 
  18. [fireExtinguisher extinguishFire:self.fire]; 
  19.  
  20. @end 

一般情况下赛车一般不会撞车,所以我们永远不会使用我们的灭火器。因为需要这个对象的概率很低,我们不想在初始化方法中立即创建他们从而拖慢了每个赛车的创建。另外,如果我们的赛车需要从多个撞车中恢复过来,这就需要创建多个灭火器。对于这样的情况,我们可以使用工厂设计模式。

工厂设计模式是标准的objectice-c blocks语法,它不需要参数并且返回一个对象的实体。一个对象可以在不需要知道如何创建他们的细节的时候就能使用他们的blocks创建依赖。

这边是一个使用依赖注入也就是使用工厂设计模式来创建我们的灭火器的例子:

  1. typedef RCFireExtinguisher *(^RCFireExtinguisherFactory)(); 
  2.  
  3. @interface RCRaceCar () 
  4.  
  5. @property (nonatomic, readonly) RCEngine *engine; 
  6. @property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory; 
  7.  
  8. @end 
  9.  
  10. @implementation RCRaceCar 
  11. - (instancetype)initWithEngine:(RCEngine *)engine 
  12. fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory 
  13. ... 
  14.  
  15. _engine = engine; 
  16. _fireExtinguisherFactory = [extFactory copy]; 
  17. return self; 
  18.  
  19. - (void)recoverFromCrash 
  20. if (self.fire != nil) { 
  21. RCFireExtinguisher *fireExtinguisher = self.fireExtinguisherFactory(); 
  22. [fireExtinguisher extinguishFire:self.fire]; 
  23.  
  24. @end 

工厂模式在我们需要创建未知个数的依赖时也很有用,甚至在初始化器中创建,比如:

  1. @implementation RCRaceCar 
  2.  
  3. - (instancetype)initWithEngine:(RCEngine *)engine 
  4. transmission:(RCTransmission *)transmission 
  5. wheelFactory:(RCWheel *(^)())wheelFactory; 
  6. self = [super init]; 
  7. if (self == nil) { 
  8. return nil; 
  9.  
  10. _engine = engine; 
  11. _transmission = transmission; 
  12.  
  13. _leftFrontWheel = wheelFactory(); 
  14. _leftRearWheel = wheelFactory(); 
  15. _rightFrontWheel = wheelFactory(); 
  16. _rightRearWheel = wheelFactory(); 
  17.  
  18. // Keep the wheel factory for later in case we need a spare. 
  19. _wheelFactory = [wheelFactory copy]; 
  20.  
  21. return self; 
  22.  
  23. @end 

#p#

避免笨重的配置

如果对象不应该在其他对象里被alloc,那它应该在哪边被alloc?是不是这样的依赖都很难去配置?难道每次alloc他们都一样困难?对于这些问题的解决要依靠类型的简洁初始化器(例如+[NSDictionary dictionary]),我们将我们的对象图配置从普通对象中取出,使他们纯净可测试,业务逻辑清晰。

在添加类型简易初始化方法之前,确保它是有必要的。如果一个对象只有少量的参数在init方法,并且这些参数没有合理的地默认值,那么这个类型是不需要简介初始化方法的,就直接调用标准的init方法就可以了。

我们将从4处地方手机我们的依赖去配置我们的对象:

值没有合理的默认值。如每个实例都可能包含不同的布尔值或者数值。这些值应该作为参数传给类型的简洁初始化器。

现存的共享对象。这些对象应该作为参数传给类型的简洁初始器(例如 一段无线电波)。这些都是之前可能被评估成单例或者通过父类指针的对象。

新创建的对象。如果我们的对象不能将这些依赖共享给其他对象,那么合作的对象应该在类型简介初始化函数中新建一个实例。这些都是之前在对象的implementation里面直接分配的对象。

系统单例。这些是cocoa提供的单例和可以直接使用的单例。这些单例的应用,如[NSFileManager defaultManager],在你的app中,预计只需要产生一个实例的类型使用可以使用单例。系统中有很多这样的单例。

一个赛车类的简洁初始化方法如下:

  1. + (instancetype)raceCarWithPitRadioFrequency:(RCRadioFrequency *)frequency; 
  2. RCEngine *engine = [[RCEngine alloc] init]; 
  3. RCTransmission *transmission = [[RCTransmission alloc] init]; 
  4.  
  5. RCWheel *(^wheelFactory)() = ^{ 
  6. return [[RCWheel alloc] init]; 
  7. }; 
  8.  
  9. return [[self alloc] initWithEngine:engine 
  10. transmission:transmission 
  11. pitRadioFrequency:frequency 
  12. wheelFactory:wheelFactory]; 

你的类型便利初始化方法应该放在适合的地方。常用的或者可复用的配置文件将作为对象放在.m文件里面,而由一个特殊的Foo 对象使用的配置器应该放在RaceCar的@interface里面。
系统单例

在Cocoa库里很多对象只有一个实例存在,例如[UIApplication sharedApplication], [NSFileManager defaultManager], [NSUserDefaults standardUserDefaults], [UIDevice currentDevice].如果一个对象依赖于以上这些对象,应该把它放进初始化器的参数。即使你的代码中可能只有一个实例,你的测试想模拟这个实例或创建一个实例的测试避免测试的相互依赖。

建议大家在自己的代码中避免创建全局引用的单例,也不要在一个对象第一次需要或者注入它所有依赖于它的对象时创建他的单个实例。

不可变的构造器

偶尔会有这种问题,就是一个类的初始化器/构造器不能被改变,或者直接调用。在这种情况下,应该使用setter injection,例:

  1. / An example where we can't directly call the the initializer. 
  2. RCRaceTrack *raceTrack = [objectYouCantModify createRaceTrack]; 
  3.  
  4. // We can still use properties to configure our race track. 
  5. raceTrack.width = 10; 
  6. raceTrack.numberOfHairpinTurns = 2; 

setter injuection 允许你配置对象,但是这在对象设计上引入了额外的可变性,需要测试和解决。幸运的是,导致初始化不能访问或者不能修改的两种主要场景都可以避免。

类注册

使用类注册工厂模式也就是对象不能修改他们的初始化器。

  1. NSArray *raceCarClasses = @[ 
  2. [RCFastRaceCar class], 
  3. [RCSlowRaceCar class], 
  4. ]; 
  5.  
  6. NSMutableArray *raceCars = [[NSMutableArray alloc] init]; 
  7. for (Class raceCarClass in raceCarClasses) { 
  8. // All race cars must have the same initializer ("init" in this case). 
  9. // This means we can't customize different subclasses in different ways. 
  10. [raceCars addObject:[[raceCarClass alloc] init]]; 

对于这样的问题可以用工厂模式 blocks简单代替类型申明的列表。

  1. typedef RCRaceCar *(^RCRaceCarFactory)(); 
  2.  
  3. NSArray *raceCarFactories = @[ 
  4. ^{ return [[RCFastRaceCar alloc] initWithTopSpeed:200]; }, 
  5. ^{ return [[RCSlowRaceCar alloc] initWithLeatherPlushiness:11]; } 
  6. ]; 
  7.  
  8. NSMutableArray *raceCars = [[NSMutableArray alloc] init]; 
  9. for (RCRaceCarFactory raceCarFactory in raceCarFactories) { 
  10. // We now no longer care which initializer is being called. 
  11. [raceCars addObject:raceCarFactory()]; 

Storyboards
storyboards提供便捷的方法来布置我们的用户界面,但是给依赖注入带来了问题。尤其是在storyboard中初始化View Controller不允许你选择调用哪个初始化方法。同样地,当在sytoyboard中定义页面跳转的时候,目标View Controller不会给你自定初始化方法来产生实例。

解决方法就是避免使用storyboard。这听起来是个极端的解决方案,但是我们将发现使用storyboard会产生大量其他问题。另外,不想失去storyboard给我们带来的便利,可以使用XIB,而且XIB可以让你自定义初始化器。

公有 Vs.私有

依赖注入鼓励你在公共接口中暴露更多的对象。如前所述,这有很多优点。搭建框架时候,他能大大的充实你的公共API。而且运用依赖注入,公共对象A可以使用私有对象B(这样轮流过来就可以使用私有对象C),但对象B和C从来没有暴露在框架外面。对象A在初始化器中依赖注入对象B,然后对象B的构造器又创建了公共对象C.

  1. // In public ObjectA.h. 
  2. @interface ObjectA 
  3. // Because the initializer uses a reference to ObjectB we need to 
  4. // make the Object B header public where we wouldn't have before. 
  5. - (instancetype)initWithObjectB:(ObjectB *)objectB; 
  6. @end 
  7.  
  8. @interface ObjectB 
  9. // Same here: we need to expose ObjectC.h. 
  10. - (instancetype)initWithObjectC:(ObjectC *)objectC; 
  11. @end 
  12.  
  13. @interface ObjectC 
  14. - (instancetype)init; 
  15. @end 

你也不希望框架的使用者担心对象B和对象C的实现细节,我们可以通过协议解决这个问题。

  1. @interface ObjectA 
  2. - (instancetype)initWithObjectB:(id )objectB; 
  3. @end 
  4.  
  5. // This protocol exposes only the parts of the original ObjectB that 
  6. // are needed by ObjectA. We're not creating a hard dependency on 
  7. // our concrete ObjectB (or ObjectC) implementation. 
  8. @protocol ObjectB 
  9. - (void)methodNeededByObjectA; 
  10. @end 

结束语

依赖注入很适合objective-c和之后的Swift。恰当的运用可以使你的代码库更加易读,易测试,易维护。

责任编辑:chenqingxiang 来源: Square的技术博客
相关推荐

2012-12-06 10:32:06

KVM

2010-08-18 10:18:03

业务流程电信普元

2012-04-17 08:55:48

个人开发者开发心得

2023-03-08 15:25:42

2023-09-01 18:41:53

人工智能数字化转型

2015-08-24 17:33:11

华为

2009-04-07 18:07:01

NehalemIntel服务器

2011-07-09 17:14:39

复合一体机对比评测

2012-02-22 16:11:50

2012-04-17 14:56:54

笔记本评测

2015-07-07 12:03:01

2020-04-16 13:40:01

服务器

2011-06-20 17:40:34

至强E7关键业务

2009-06-03 08:44:46

2010-12-03 12:57:23

2016-01-21 11:18:36

云计算混合云Azure站点恢复

2009-09-24 11:28:15

2010-10-26 15:46:32

Windows 8

2009-03-18 08:37:16

智能手机安全应用程序

2016-12-28 09:30:37

Andriod安卓平台依赖注入
点赞
收藏

51CTO技术栈公众号