依赖注入可以通过初始化方法(或构造函数)传递所需要的参数,或者通过属性(setter)传递。这里将对这两种方法进行讲解。
初始化方法注入:
1. - (instancetype)initWithDependency1:(Dependency1 *)d1
2. dependency2:(Dependency2 *)d2;
属性注入:
1. @property (nonatomic, retain) Dependency1 *dependency1;
2. @property (nonatomic, retain) Dependency2 *dependency2;
一般更趋向于初始化注入,如果在初始化(构造函数)的时候没办法进行注入,才通过属性进行注入。在通过初始化注入的情况下,这些依赖可能仍然需要作为属性存在,但是这些属性应该被设置为只读(readonly)的。
为什么使用依赖注入
依赖注入的几个重要之处:
1. 明确的定义。 使用依赖注入很明确的告诉了使用者要操作这个类对象需要做什么事情,初始化哪些变量,并且可以隐藏一些危险的依赖,如全局变量;
2. 使用构成。 即我们一直坚持的多构成少继承原则。它能提高代码的可重用性;
3. 更简单的自定义。在创建一个对象的时候通过传入指定的参数,更容易自定义。
4. 明确的所有者。
5. 可测试性。 因为我们只需根据初始化方法,传入需要的参数即可进行操作,不需要去管理被隐藏的依赖。
开始使用依赖注入
一、注入类的类型
首先,我们将类(Class)的分为两类(type):一类是简单的类,一类是复杂的类。其中简单的类是一个没有任何依赖或者只是依赖于其他简单的类,这个简单类是不可能被子类化的,以为它们的功能是很明确的并且不可变的,也没有引用其他额外的资源。在Cocoa框架中就有很多简单类,如:NSString, NSArray, NSNumber, NSDictionary 等。
复杂类则相反。它们有其他复杂的依赖,包括应用程序等级逻辑(根据应用程序的逻辑不同可能改变)。又或者它们需要访问其他外部的资源,如硬盘,网络或全局变量。这些类在你的应用程序中将变得很复杂,它们可能包含所有的控制器对象或所有的model对象。Cocoa框架中的复杂类有:NSURLConnection, UIViewController等。。。
分类后,我们就可以很容易的在应用程序中选出哪些是复杂类了,然后开始对他们进行优化。
二、在初始化时依赖分配
下面开始举例。原始代码:
1. @interface RCRaceCar ()
2.
3. @property (nonatomic, readonly) RCEngine *engine;
4.
5. @end
6.
7.
8. @implementation RCRaceCar
9.
10. - (instancetype)init
11. {
12. ...
13.
14. // 创建一个引擎。注:这里不能被自定义或者没有修改RCRaceCar的内部不能被模拟
15. alloc] init];
16.
17. return self;
18. }
19.
20. @end
使用依赖注入改版后:
1. @interface RCRaceCar ()
2.
3. @property (nonatomic, readonly) RCEngine *engine;
4.
5. @end
6.
7.
8. @implementation RCRaceCar
9.
10. // 引擎是在赛车被创建之前被创建的并且作为参数传进来,
11. // 调用者如果想,还可以自定义。
12. - (instancetype)initWithEngine:(RCEngine *)engine
13. {
14. ...
15.
16. _engine = engine;
17.
18. return self;
19. }
20.
21. @end
三、延迟初始化依赖
1. @interface RCRaceCar ()
2.
3. @property (nonatomic) RCEngine *engine;
4.
5. @end
6.
7.
8. @implementation RCRaceCar
9.
10. - (instancetype)initWithEngine:(RCEngine *)engine
11. {
12. ...
13.
14. _engine = engine;
15.
16. return self;
17. }
18.
19. - (void)recoverFromCrash
20. {
21. if (self.fire != nil) {
22. RCFireExtinguisher *fireExtinguisher = [[RCFireExtinguisher alloc] init];
23. extinguishFire:self.fire];
24. }
25. }
26.
27. @end
在这种情况,汽车当然是希望永远都没事,所以我们可能永远不需要灭火器。因为用到这个灭火器对象的几率很低,我们不想使得每一辆车创建得缓慢直接通过初始化方法创建它。或者,如果我们的汽车需要为多次车祸去恢复,这将需要创建多个灭火器。这种情况,我们可以使用一个工厂方法。
1. typedef RCFireExtinguisher *(^RCFireExtinguisherFactory)();
2.
3.
4. @interface RCRaceCar ()
5.
6. @property (nonatomic, readonly) RCEngine *engine;
7. @property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory;
8.
9. @end
10.
11.
12. @implementation RCRaceCar
13.
14. - (instancetype)initWithEngine:(RCEngine *)engine
15. fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory
16. {
17. ...
18.
19. _engine = engine;
20. copy];
21.
22. return self;
23. }
24.
25. - (void)recoverFromCrash
26. {
27. if (self.fire != nil) {
28. RCFireExtinguisher *fireExtinguisher = self.fireExtinguisherFactory();
29. extinguishFire:self.fire];
30. }
31. }
32.
33. @end
工厂在下面的情况下也很有用。当我们需要去创建一个不知道数量的依赖,甚至它是知道初始化之后才被创建的。如下:
1. @implementation RCRaceCar
2.
3. - (instancetype)initWithEngine:(RCEngine *)engine
4. transmission:(RCTransmission *)transmission
5. wheelFactory:(RCWheel *(^)())wheelFactory;
6. {
7. self = [super init];
8. if (self == nil) {
9. return nil;
10. }
11.
12. _engine = engine;
13. _transmission = transmission;
14.
15. _leftFrontWheel = wheelFactory();
16. _leftRearWheel = wheelFactory();
17. _rightFrontWheel = wheelFactory();
18. _rightRearWheel = wheelFactory();
19.
20. // 保留轮子工厂,之后还需要一个备胎。
21. copy];
22.
23. return self;
24. }
25.
26. @end
避免多余的配置
1. 没有一个切确的默认值。 包括 boolean值或 number值,他们可能根据在不同实例变量中的值各不相同。所以这些值应该作为参数传递到便利构造器中;
2. 存在共享对象。 这个也需要作为参数传递到便利构造器中(比如一个无线电频率)。这些对象之前可能已经作为单例或通过父类指针被赋值;
3. 被新创建的对象。 如果一个对象没有把这个依赖分享给其他对象,那其他对象(同一个类)应该在遍历构造器内创建一个新的依赖对象。
4. 系统单例。 Cocoa内提供的单例是可以直接被访问的,比如文件管理者单例[NSFileManager defaultManager], 这里很明确在你的应用程序中只有一个实例将会被使用。
下面是关于赛车的简单初便利构造器
1. + (instancetype)raceCarWithPitRadioFrequency:(RCRadioFrequency *)frequency;
2. {
3. RCEngine *engine = [[RCEngine alloc] init];
4. RCTransmission *transmission = [[RCTransmission alloc] init];
5.
6. RCWheel *(^wheelFactory)() = ^{
7. return [[RCWheel alloc] init];
8. };
9.
10. return [[self alloc] initWithEngine:engine
11. transmission:transmission
12. pitRadioFrequency:frequency
13. wheelFactory:wheelFactory];
14. }
遍历构造器应该放置在一个更加适合的地方与类分离。通常情况下都是放置在相同的 *.m 文件中,但是当指定通过如 Foo 对象配置的时候就应该将它放置在 @interface RaceCar (FooConfiguration) 这个 category 中,并且命名为类似 fooRaceCar 之类的。
系统单例
在Cocoa中有许多对象只有一个实例存在,如[UIApplication sharedApplication]
, [NSFileManager defaultManager]
, [NSUserDefaults standardUserDefaults]
, 和 [UIDevice currentDevice]
等。如果一个对象依赖于这些对象中的一个,那就应该被作为参数包含进来。即使在你的应用程序中只有这样一个实例。在你的测试中可能想要模拟实例或在测试前创建一个实例来避免测试依赖。
不可修改的构造函数
1. // 一个我们不能直接调用初始化方法的例子。
2. RCRaceTrack *raceTrack = [objectYouCantModify createRaceTrack];
3.
4. // 我们仍然可以使用属性来配置我们的赛车路径
5. raceTrack.width = 10;
6. raceTrack.numberOfHairpinTurns = 2;
类注册
“类注册” 工厂模式的使用意味着对象不能修改它们的初始化方法。见代码:
1. NSArray *raceCarClasses = @[
2. class],
3. class],
4. ];
5.
6. NSMutableArray *raceCars = [[NSMutableArray alloc] init];
7. for (Class raceCarClass in raceCarClasses) {
8. // 所有赛车必须有相同的初始化方法 (在这个例子中是 "init" 方法).
9. // 这里意味着我们不能自定义不同的子类
10. addObject:[[raceCarClass alloc] init]];
11. }
一个简单的替换方法是:使用工厂 block来代替
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. // 现在这样,我们就不用关心到底是那个初始化方法被调用了
11. addObject:raceCarFactory()];
12. }
Storyboards
Storyboards 提供了很方便的方法来构建我们的界面,但是在依赖注入中它也带来了问题。 特别是当在Storyboard中实例化一个初始化的视图控制器,它不允许你选择调用哪一个初始化方法。 类似的,当在 storyboard 中定义一个 segue 是,目标控制器在实例化时也不能让你指定调用那个初始化方法。
公有和私有
1. // In public ObjectA.h.
2. @interface ObjectA
3. // 因为初始化方法使用了对象 B 的引用,所以我们需要
4. // 在使用对象 B 之前引入它的头文件
5. - (instancetype)initWithObjectB:(ObjectB *)objectB;
6. @end
7.
8. @interface ObjectB
9. // 这里也一样:我们需要暴露 ObjectC.h
10. - (instancetype)initWithObjectC:(ObjectC *)objectC;
11. @end
12.
13. @interface ObjectC
14. - (instancetype)init;
15. @end
对象 B 和对象 C 都是具体的实现,而你不想让框架的使用者去关心它们。这时我们可以通过 协议(protocol)来解决。
1. @interface ObjectA
2. - (instancetype)initWithObjectB:(id <ObjectB>)objectB;
3. @end
4.
5. // 这个协议只暴露ObjectA需要原始的ObjectB。
6. // 我们并不是在具体的ObjectB(或 ObjectC)实现创建一个硬依赖
7. @protocol ObjectB
8. - (void)methodNeededByObjectA;
9. @end
结束
依赖注入在Objective-C中很自然的存在,在Swift中也一样。合理的使用它能让你的代码可读性更强,可测试性更好,可维护性更高。