依赖注入——让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中也一样。合理的使用它能让你的代码可读性更强,可测试性更好,可维护性更高。

时间: 2024-10-06 18:40:18

依赖注入——让iOS代码更简洁的相关文章

lombok ------让代码更简洁方便

估计在平常写代码中,都会创建entity类的实体来,都是那种创建变量,生成set get 方法,方便外部调用,你以为你很流利的操作快捷键就很方便的了? 其实不然,有一个lombok 工具可以帮我们自动生成,但不显示在我们的代码中,该怎么用呢? 1.安装lombok的插件,很简单:下载lombok.jar,百度lombok,进入官网下载最新版本,版本太低的话,eclipse时不支持的.我使用的是1.16.18,eclipse最新版本支持. 双击lombok.jar运行,它会自动查找你的eclips

【Butter Knife】依赖注入方式简化代码提高开发效率

Butter Knife是一款非常不错的开源框架,其目的是简化代码,提高项目的开发效率. 以往的开发我们经常需要用findViewById(R.xx.xxx);几乎没个页面都会涉及到,无论Activity还是Fragment甚至listView.GridView中的 Adapter.这些重复性的代码会让人觉得很枯燥,因为闭上眼都不会敲错的代码,每天重复几百遍是有点耗时,尽管有代码提示.而且可能会因为不同field的忘记书写而导致NullPoint空指针. 那么还是直接进入主题: 步骤: 第三方框

SpringBoot集成lombook让代码更简洁

1)添加lombok依赖 <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> 2) @Slf4j//等同于 @Getter @Setter public class User { private String name; private Integer age; @Override public

利用margin代替小图标的绝对定位;使代码更简洁

1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Document</title> 6 <style> 7 *{margin:0;padding:0} 8 #div1{background:black;display: inline-block} 9 .img{width:1

使用字面量语法让iOS代码更漂亮

字面量语法 第一.字面数值 复杂方法: NSNumber *someNumber=[NSNumber numberWithDouble:3.4]; NSLog(@"the value is %@",someNumber); 替代方法: NSNumber *[email protected]; NSNumber *[email protected]; NSLog(@"the value is %@",a); NSLog(@"the value is %@&q

iOS项目依赖注入简介

依赖注入(Dependency Injection) 依赖注入 最大的特点就是:帮助我们开发出松散耦合(loose coupled).可维护.可测试的代码和程序.这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程. 众所周知该编程思想在各大语言中都有体现如jave. C++. PHP 以及 .net中.当然设计模式的广泛程度远远大于这些,iOS 当然也不例外. 本文主要介绍本人在学习Dependency Injection的时候的学习过程以及对一些学习资料的总结,主要介绍iOS中的两大框架

iOS依赖注入框架系列(三):模块化Typhoon

在以前的文章 系列中,我们简要地讨论了组织和运作框架台风的基本原则 -为iOS依赖注入容器. 但是,如何将工具布置一点理解-最重要的是要正确地使用它. 在第一部分中,我们看的创建依赖关系,现在处理更高级别的配置设置的各种例子-模块分为TyphoonAssembly自己和测试. 周期"在iOS中右依赖驱动的应用程序" 熟悉台风 该器件台风 模块化台风 台风技巧与诀窍 台风替代品 (可选)Rambler.iOS#3. 依赖注入的iOS. 幻灯片 (可选)Rambler.iOS#3. 依赖注

iOS依赖注入框架系列(二):设置Typhoon

在循环的最后一部分,我们遇到了依赖注入框架为iOS - 台风,并审查其项目Rambler.Pochta使用的基本示例. 这一次,我们进入它的内部结构的研究. 周期"在iOS中右依赖驱动的应用程序" 熟悉台风 该器件台风 模块化台风 台风技巧与诀窍 台风替代品 (可选)Rambler.iOS#3. 依赖注入的iOS. 幻灯片 (可选)Rambler.iOS#3. 依赖注入的iOS. 视频 介绍 要开始分析一个小字典将被广泛使用在这篇文章中的术语: 大会(读作[essembli]). 最近

ASP.NET Core依赖注入——依赖注入最佳实践

在这篇文章中,我们将深入研究.NET Core和ASP.NET Core MVC中的依赖注入,将介绍几乎所有可能的选项,依赖注入是ASP.Net Core的核心,我将分享在ASP.Net Core应用中使用依赖注入的一些经验和建议,并且将会讨论这些原则背后的动机是什么: (1)有效地设计服务及其依赖关系. (2)防止多线程问题. (3)防止内存泄漏. (4)防止潜在的错误. 在讨论该话题之前,了解什么是服务是生命周期至关重要,当组件通过依赖注入请求另一个组件时,它接收的实例是否对该组件实例是唯一