依赖注入——让iOS代码更简洁

原文链接:https://corner.squareup.com/2015/06/dependency-injection-in-objc.html

       本文是自己通过阅读 Dependency Injection:Give Your iOS Code a Shot in the Arm 进行总结+翻译的,有错误之处请不吝啬的指出。下面是正文:

       依赖注入可以通过初始化方法(或构造函数)传递所需要的参数,或者通过属性(setter)传递。这里将对这两种方法进行讲解。

初始化方法注入:

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

属性注入:

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

        一般更趋向于初始化注入,如果在初始化(构造函数)的时候没办法进行注入,才通过属性进行注入。在通过初始化注入的情况下,这些依赖可能仍然需要作为属性存在,但是这些属性应该被设置为只读(readonly)的。


为什么使用依赖注入

       依赖注入的几个重要之处:
       1. 明确的定义。 使用依赖注入很明确的告诉了使用者要操作这个类对象需要做什么事情,初始化哪些变量,并且可以隐藏一些危险的依赖,如全局变量;
       2. 使用构成。 即我们一直坚持的多构成少继承原则。它能提高代码的可重用性;
       3. 更简单的自定义。在创建一个对象的时候通过传入指定的参数,更容易自定义。
       4. 明确的所有者。
       5. 可测试性。 因为我们只需根据初始化方法,传入需要的参数即可进行操作,不需要去管理被隐藏的依赖。

开始使用依赖注入
一、注入类的类型
        首先,我们将类(Class)的分为两类(type):一类是简单的类,一类是复杂的类。其中简单的类是一个没有任何依赖或者只是依赖于其他简单的类,这个简单类是不可能被子类化的,以为它们的功能是很明确的并且不可变的,也没有引用其他额外的资源。在Cocoa框架中就有很多简单类,如:NSString, NSArray, NSNumber, NSDictionary 等。
        复杂类则相反。它们有其他复杂的依赖,包括应用程序等级逻辑(根据应用程序的逻辑不同可能改变)。又或者它们需要访问其他外部的资源,如硬盘,网络或全局变量。这些类在你的应用程序中将变得很复杂,它们可能包含所有的控制器对象或所有的model对象。Cocoa框架中的复杂类有:NSURLConnection, UIViewController等。。。
        分类后,我们就可以很容易的在应用程序中选出哪些是复杂类了,然后开始对他们进行优化。

二、在初始化时依赖分配
下面开始举例。原始代码:
@interface RCRaceCar ()

@property (nonatomic, readonly) RCEngine *engine;

@end


@implementation RCRaceCar

- (instancetype)init
{
   ...

   // 创建一个引擎。注:这里不能被自定义或者没有修改RCRaceCar的内部不能被模拟
   _engine = [[RCEngine alloc] init];

   return self;
}

@end

使用依赖注入改版后:
@interface RCRaceCar ()

@property (nonatomic, readonly) RCEngine *engine;

@end


@implementation RCRaceCar

// 引擎是在赛车被创建之前被创建的并且作为参数传进来,
// 调用者如果想,还可以自定义。
- (instancetype)initWithEngine:(RCEngine *)engine
{
   ...

   _engine = engine;

   return self;
}

@end

三、延迟初始化依赖
       通常,有一些对象是初始化之后才需要用到的,甚至有时可能几乎用不到,如在用户的一个收藏列表中,当一个收藏都没有的时候显示一个和谐的页面,但这种情况有时很少遇到,以为只要用户收藏了一个资源,这个页面就不需要了。如下面以灭火器为例子:
@interface RCRaceCar ()

@property (nonatomic) RCEngine *engine;

@end


@implementation RCRaceCar

- (instancetype)initWithEngine:(RCEngine *)engine
{
   ...

   _engine = engine;

   return self;
}

- (void)recoverFromCrash
{
   if (self.fire != nil) {
      RCFireExtinguisher *fireExtinguisher = [[RCFireExtinguisher alloc] init];
      [fireExtinguisher extinguishFire:self.fire];
   }
}

@end

       在这种情况,汽车当然是希望永远都没事,所以我们可能永远不需要灭火器。因为用到这个灭火器对象的几率很低,我们不想使得每一辆车创建得缓慢直接通过初始化方法创建它。或者,如果我们的汽车需要为多次车祸去恢复,这将需要创建多个灭火器。这种情况,我们可以使用一个工厂方法。

       工厂方法是一个标准的Objective-C的block, 它要求没有参数并且返回一个具体的实例对象。当一个对象依赖使用这个block创建时它不需要知道它具体是怎样被创建的。

       下面,通过一个工厂,使用依赖注入创建一个灭火器。

typedef RCFireExtinguisher *(^RCFireExtinguisherFactory)();


@interface RCRaceCar ()

@property (nonatomic, readonly) RCEngine *engine;
@property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory;

@end


@implementation RCRaceCar

- (instancetype)initWithEngine:(RCEngine *)engine
       fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory
{
   ...

   _engine = engine;
   _fireExtinguisherFactory = [extFactory copy];

   return self;
}

- (void)recoverFromCrash
{
   if (self.fire != nil) {
      RCFireExtinguisher *fireExtinguisher = self.fireExtinguisherFactory();
      [fireExtinguisher extinguishFire:self.fire];
   }
}

@end

       工厂在下面的情况下也很有用。当我们需要去创建一个不知道数量的依赖,甚至它是知道初始化之后才被创建的。如下:

@implementation RCRaceCar

- (instancetype)initWithEngine:(RCEngine *)engine 
                  transmission:(RCTransmission *)transmission
                  wheelFactory:(RCWheel *(^)())wheelFactory;
{
   self = [super init];
   if (self == nil) {
      return nil;
   }

   _engine = engine;
   _transmission = transmission;

   _leftFrontWheel = wheelFactory();
   _leftRearWheel = wheelFactory();
   _rightFrontWheel = wheelFactory();
   _rightRearWheel = wheelFactory();

   // 保留轮子工厂,之后还需要一个备胎。
   _wheelFactory = [wheelFactory copy];

   return self;
}

@end

避免多余的配置

       如果一个对象不应该在其他对象内部进行配置,那就是用便利构造器(如+ [NSDictionary dictionary])。我们将把配置从我们的对象图中移出到我们普通的对象,分开他们使得代码更整洁,可测试,业务逻辑更清晰。

       在添加一个便利构造器时,应该先确保是否是必须的。如果一个对象在init方法中只有几个参数,并且这些参数没有切确的默认值,那添加一个便利构造器是没必要的并且调用者应该直接使用标准的init方法。

       为了配置我们的对象,我们将从4个点来收集我们的依赖:

       1. 没有一个切确的默认值。 包括 boolean值或 number值,他们可能根据在不同实例变量中的值各不相同。所以这些值应该作为参数传递到便利构造器中;

       2. 存在共享对象。 这个也需要作为参数传递到便利构造器中(比如一个无线电频率)。这些对象之前可能已经作为单例或通过父类指针被赋值;

       3. 被新创建的对象。 如果一个对象没有把这个依赖分享给其他对象,那其他对象(同一个类)应该在遍历构造器内创建一个新的依赖对象。

       4. 系统单例。 Cocoa内提供的单例是可以直接被访问的,比如文件管理者单例[NSFileManager defaultManager], 这里很明确在你的应用程序中只有一个实例将会被使用。

       下面是关于赛车的简单初便利构造器

+ (instancetype)raceCarWithPitRadioFrequency:(RCRadioFrequency *)frequency;
{
   RCEngine *engine = [[RCEngine alloc] init];
   RCTransmission *transmission = [[RCTransmission alloc] init];

   RCWheel *(^wheelFactory)() = ^{
      return [[RCWheel alloc] init];
   };

   return [[self alloc] initWithEngine:engine 
                          transmission:transmission 
                     pitRadioFrequency:frequency 
                          wheelFactory:wheelFactory];
}
       遍历构造器应该放置在一个更加适合的地方与类分离。通常情况下都是放置在相同的 *.m 文件中,但是当指定通过如 Foo 对象配置的时候就应该将它放置在 @interface RaceCar (FooConfiguration) 这个 category 中,并且命名为类似 fooRaceCar 之类的。
    

系统单例

        在Cocoa中有许多对象只有一个实例存在,如[UIApplication sharedApplication][NSFileManager defaultManager][NSUserDefaults standardUserDefaults], 和 [UIDevice currentDevice] 等。如果一个对象依赖于这些对象中的一个,那就应该被作为参数包含进来。即使在你的应用程序中只有这样一个实例。在你的测试中可能想要模拟实例或在测试前创建一个实例来避免测试依赖。

       这里建议避免在你的代码中创建全局的单例,而是在一个对象中创建一个单一的实例,当它第一次被使用时,将它注入到其他依赖它的对象中去。


不可修改的构造函数

       有些时候,一个类的 初始化方法/构造方法 不能被修改或不能被直接调用。在这种情况下,你需要使用 setter 注入。如下代码:

// 一个我们不能直接调用初始化方法的例子。
RCRaceTrack *raceTrack = [objectYouCantModify createRaceTrack];

// 我们仍然可以使用属性来配置我们的赛车路径
raceTrack.width = 10;
raceTrack.numberOfHairpinTurns = 2;
       setter 注入允许你配置这个对象,但是它引入了额外的可变性使得在这个类的设计中必须做额外的测试和处理。幸运的是,这里有两种主要的场景导致无法访问和修改初始化方法,并且这两种情况都是可以避免的。

类注册

       “类注册” 工厂模式的使用意味着对象不能修改它们的初始化方法。见代码:

NSArray *raceCarClasses = @[
   [RCFastRaceCar class],
   [RCSlowRaceCar class],
];

NSMutableArray *raceCars = [[NSMutableArray alloc] init];
for (Class raceCarClass in raceCarClasses) {
   // 所有赛车必须有相同的初始化方法 (在这个例子中是 "init" 方法).
   // 这里意味着我们不能自定义不同的子类
   [raceCars addObject:[[raceCarClass alloc] init]];
}
       一个简单的替换方法是:使用工厂 block来代替
typedef RCRaceCar *(^RCRaceCarFactory)();

NSArray *raceCarFactories = @[
   ^{ return [[RCFastRaceCar alloc] initWithTopSpeed:200]; },
   ^{ return [[RCSlowRaceCar alloc] initWithLeatherPlushiness:11]; }
];

NSMutableArray *raceCars = [[NSMutableArray alloc] init];
for (RCRaceCarFactory raceCarFactory in raceCarFactories) {
   // 现在这样,我们就不用关心到底是那个初始化方法被调用了
   [raceCars addObject:raceCarFactory()];
}

Storyboards

       Storyboards 提供了很方便的方法来构建我们的界面,但是在依赖注入中它也带来了问题。 特别是当在Storyboard中实例化一个初始化的视图控制器,它不允许你选择调用哪一个初始化方法。 类似的,当在 storyboard 中定义一个 segue 是,目标控制器在实例化时也不能让你指定调用那个初始化方法。

       解决方法是避免使用 storyboard。这看起来是一种极端的解决方案,但是我们发现在大型团队开发中, storyboard 带来了其他问题。另外,不适用storyboard并没有丢掉它的所有好处,除了storyboard提供的segues外,XIB也提供了和 storyboard 相同的好处,而且Xib 可以让你自定义初始化方法。


公有和私有

       依赖注入鼓励你在你的共有接口上暴露出更多是对象。正如上面提到的,这有很多好处。但是当在构建框架时,它使你的共有API变得臃肿。使用依赖注入之前,公有对象 A 已经使用私有对象 B(对象 B 反过来使用私有对象C),但是对象B 和对象C 从来没有从框架中暴露。通过依赖注入,对象 A 在它的公有初始化方法中有对象 B ,而对象 B 反过来使得对象 C 在它的初始化方法中公开。

// In public ObjectA.h.
@interface ObjectA
// 因为初始化方法使用了对象 B 的引用,所以我们需要
// 在使用对象 B 之前引入它的头文件
- (instancetype)initWithObjectB:(ObjectB *)objectB;
@end

@interface ObjectB
// 这里也一样:我们需要暴露 ObjectC.h
- (instancetype)initWithObjectC:(ObjectC *)objectC;
@end

@interface ObjectC
- (instancetype)init;
@end
       对象 B 和对象 C 都是具体的实现,而你不想让框架的使用者去关心它们。这时我们可以通过 协议(protocol)来解决。

@interface ObjectA
- (instancetype)initWithObjectB:(id <ObjectB>)objectB;
@end

// 这个协议只暴露ObjectA需要原始的ObjectB。
// 我们并不是在具体的ObjectB(或 ObjectC)实现创建一个硬依赖 
@protocol ObjectB
- (void)methodNeededByObjectA;
@end

结束

       依赖注入在Objective-C中很自然的存在,在Swift中也一样。合理的使用它能让你的代码可读性更强,可测试性更好,可维护性更高。









  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值