代碼的一針強(qiáng)心劑——依賴注入
什么是Dependency Injection(依賴注入)?
在許多程序設(shè)計(jì)語言里,比如Java,C#,依賴注入(DI)都是一種較流行的設(shè)計(jì)模式,但是它在Objective-C中沒有得到廣泛應(yīng)用。本文旨在用 Objective-C的例子對(duì)依賴注入進(jìn)行簡要介紹,同時(shí)介紹 Objective-C 代碼中使用依賴注入的實(shí)用方法。盡管文章主要針對(duì)Objective-C,但是提到的所有概念對(duì)Swift同樣適用。
依賴注入的概念十分簡單:一個(gè)對(duì)象應(yīng)該通過依賴傳遞獲得,而不是創(chuàng)建他們本身。推薦Martin Fowler的 excellent discussion on the subject 作為背景材料閱讀。
依賴可以通過initializer(初始化器)(或者constructor(構(gòu)造器))或者屬性(set方法)傳遞給對(duì)象。它們通常被稱為"constructor injection" 和 "setter injection"。(構(gòu)造器注入和 set方法注入)
Constructor Injection:
- - (instancetype)initWithDependency1:(Dependency1 *)d1
- dependency2:(Dependency2 *)d2;
Setter Injection:
- @property (nonatomic, retain) Dependency1 *dependency1;
- @property (nonatomic, retain) Dependency2 *dependency2;
根據(jù)Fowler的描述,一般情況下,首選構(gòu)造器注入,在構(gòu)造函數(shù)注入不適合的情況下才選擇setter注入。雖然使用構(gòu)造函數(shù)注入時(shí),很可能還是要給這些依賴定義屬性,但你可以給這些屬性設(shè)置成read only從而簡化你的對(duì)象API。
為什么要使用依賴注入?
使用依賴注入有很多優(yōu)點(diǎn):
- 1. 依賴申明清晰。 一個(gè)對(duì)象需要進(jìn)行的操作變得一目了然,同時(shí)也容易消除危險(xiǎn)的隱藏依賴,比如全局變量。
- 2.組件化。 依賴注入提倡composition over inheritance,以提高代碼的重用性。
- 3. 更易定制。 當(dāng)創(chuàng)建對(duì)象的時(shí),在特殊情況下更易對(duì)對(duì)象進(jìn)行部分的定制。
- 4. 明確從屬關(guān)系。 特別是在使用構(gòu)造器依賴注入時(shí),對(duì)象所有權(quán)規(guī)則嚴(yán)格執(zhí)行--可以建立一個(gè)直接非循環(huán)的對(duì)象圖。
- 5.易測試性。 依賴注入比其他方法更能提高對(duì)象的易測試性。因?yàn)橥ㄟ^構(gòu)造器創(chuàng)建這些對(duì)象很簡單,也沒有必要管理隱藏的依賴。此外,模擬依賴變得簡單,從而可以把測試集中在被測試的對(duì)象上。
在代碼中使用依賴注入
你的代碼庫可能還沒有使用依賴注入設(shè)計(jì)模式,但是轉(zhuǎn)換一下很簡單。依賴注入很好的一點(diǎn)就是你不需要讓整個(gè)工程的代碼全都采取該模式。相反,你可以在代碼庫的特定區(qū)域運(yùn)用然后從那邊擴(kuò)展開來。
二級(jí)各種類的注入
首先,把類分為兩種:基本類型和復(fù)雜類型。基本類型是沒有依賴的,或者是只依靠其他基本類型。基本類型基本不用被繼承,因?yàn)樗麄児δ芮逦蛔儯膊恍枰溄油獠抠Y源。許多基本類型都是從Cocoa 自身獲得的,比如NSString, NSArray, NSDictionary, and NSNumber.
復(fù)雜類型就相反了。它們有復(fù)雜的依賴,包括應(yīng)用級(jí)別的邏輯(需要修改的部分),或者訪問額外的資源,例如磁盤,網(wǎng)絡(luò)或者全局內(nèi)存服務(wù)。應(yīng)用中絕大多數(shù)類都是復(fù)雜的,包括幾乎所有的控制器對(duì)象和模型對(duì)象。很多cocoa類型也很復(fù)雜,例如NSURLConnection or UIViewController.。
根據(jù)以上分類情況,想要使用依賴注入模式最簡單的方法是先選擇應(yīng)用中一個(gè)復(fù)雜的類,找到類中的初始化其他復(fù)雜對(duì)象的地方(找"alloc]init"或者"new"關(guān)鍵字)。將類中引進(jìn)依賴注入,改變這一實(shí)例化對(duì)象作為初始化參數(shù)在類中傳遞而不是類初始化對(duì)象本身。
在初始化時(shí)分配依賴
讓我們來看一個(gè)例子,子對(duì)象(依賴)在母體的初始化函數(shù)中被初始化。原始的代碼如下:
- @interface RCRaceCar ()
- @property (nonatomic, readonly) RCEngine *engine;
- @end
- @implementation RCRaceCar
- - (instancetype)init
- {
- ...
- // Create the engine. Note that it cannot be customized or
- // mocked out without modifying the internals of RCRaceCar.
- _engine = [[RCEngine alloc] init];
- return self;
- }
- @end
依賴注入做了小的修改:
- @interface RCRaceCar ()
- @property (nonatomic, readonly) RCEngine *engine;
- @end
- @implementation RCRaceCar
- // The engine is created before the race car and passed in
- // as a parameter, and the caller can customize it if desired.
- - (instancetype)initWithEngine:(RCEngine *)engine
- {
- ...
- _engine = engine;
- return self;
- }
- @end
惰性初始化依賴
有一些對(duì)象可能一段時(shí)間后才用到,或者初始化之后才會(huì)用到,或者永遠(yuǎn)也不會(huì)用到。沒有用依賴注入之前的例子:
- @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
一般情況下賽車一般不會(huì)撞車,所以我們永遠(yuǎn)不會(huì)使用我們的滅火器。因?yàn)樾枰@個(gè)對(duì)象的概率很低,我們不想在初始化方法中立即創(chuàng)建他們從而拖慢了每個(gè)賽車的創(chuàng)建。另外,如果我們的賽車需要從多個(gè)撞車中恢復(fù)過來,這就需要?jiǎng)?chuàng)建多個(gè)滅火器。對(duì)于這樣的情況,我們可以使用工廠設(shè)計(jì)模式。
工廠設(shè)計(jì)模式是標(biāo)準(zhǔn)的objectice-c blocks語法,它不需要參數(shù)并且返回一個(gè)對(duì)象的實(shí)體。一個(gè)對(duì)象可以在不需要知道如何創(chuàng)建他們的細(xì)節(jié)的時(shí)候就能使用他們的blocks創(chuàng)建依賴。
這邊是一個(gè)使用依賴注入也就是使用工廠設(shè)計(jì)模式來創(chuàng)建我們的滅火器的例子:
- 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
工廠模式在我們需要?jiǎng)?chuàng)建未知個(gè)數(shù)的依賴時(shí)也很有用,甚至在初始化器中創(chuàng)建,比如:
- @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();
- // Keep the wheel factory for later in case we need a spare.
- _wheelFactory = [wheelFactory copy];
- return self;
- }
- @end
#p#
避免笨重的配置
如果對(duì)象不應(yīng)該在其他對(duì)象里被alloc,那它應(yīng)該在哪邊被alloc?是不是這樣的依賴都很難去配置?難道每次alloc他們都一樣困難?對(duì)于這些問題的解決要依靠類型的簡潔初始化器(例如+[NSDictionary dictionary]),我們將我們的對(duì)象圖配置從普通對(duì)象中取出,使他們純凈可測試,業(yè)務(wù)邏輯清晰。
在添加類型簡易初始化方法之前,確保它是有必要的。如果一個(gè)對(duì)象只有少量的參數(shù)在init方法,并且這些參數(shù)沒有合理的地默認(rèn)值,那么這個(gè)類型是不需要簡介初始化方法的,就直接調(diào)用標(biāo)準(zhǔn)的init方法就可以了。
我們將從4處地方手機(jī)我們的依賴去配置我們的對(duì)象:
值沒有合理的默認(rèn)值。如每個(gè)實(shí)例都可能包含不同的布爾值或者數(shù)值。這些值應(yīng)該作為參數(shù)傳給類型的簡潔初始化器。
現(xiàn)存的共享對(duì)象。這些對(duì)象應(yīng)該作為參數(shù)傳給類型的簡潔初始器(例如 一段無線電波)。這些都是之前可能被評(píng)估成單例或者通過父類指針的對(duì)象。
新創(chuàng)建的對(duì)象。如果我們的對(duì)象不能將這些依賴共享給其他對(duì)象,那么合作的對(duì)象應(yīng)該在類型簡介初始化函數(shù)中新建一個(gè)實(shí)例。這些都是之前在對(duì)象的implementation里面直接分配的對(duì)象。
系統(tǒng)單例。這些是cocoa提供的單例和可以直接使用的單例。這些單例的應(yīng)用,如[NSFileManager defaultManager],在你的app中,預(yù)計(jì)只需要產(chǎn)生一個(gè)實(shí)例的類型使用可以使用單例。系統(tǒng)中有很多這樣的單例。
一個(gè)賽車類的簡潔初始化方法如下:
- + (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];
- }
你的類型便利初始化方法應(yīng)該放在適合的地方。常用的或者可復(fù)用的配置文件將作為對(duì)象放在.m文件里面,而由一個(gè)特殊的Foo 對(duì)象使用的配置器應(yīng)該放在RaceCar的@interface里面。
系統(tǒng)單例
在Cocoa庫里很多對(duì)象只有一個(gè)實(shí)例存在,例如[UIApplication sharedApplication], [NSFileManager defaultManager], [NSUserDefaults standardUserDefaults], [UIDevice currentDevice].如果一個(gè)對(duì)象依賴于以上這些對(duì)象,應(yīng)該把它放進(jìn)初始化器的參數(shù)。即使你的代碼中可能只有一個(gè)實(shí)例,你的測試想模擬這個(gè)實(shí)例或創(chuàng)建一個(gè)實(shí)例的測試避免測試的相互依賴。
建議大家在自己的代碼中避免創(chuàng)建全局引用的單例,也不要在一個(gè)對(duì)象第一次需要或者注入它所有依賴于它的對(duì)象時(shí)創(chuàng)建他的單個(gè)實(shí)例。
不可變的構(gòu)造器
偶爾會(huì)有這種問題,就是一個(gè)類的初始化器/構(gòu)造器不能被改變,或者直接調(diào)用。在這種情況下,應(yīng)該使用setter injection,例:
- / An example where we can't directly call the the initializer.
- RCRaceTrack *raceTrack = [objectYouCantModify createRaceTrack];
- // We can still use properties to configure our race track.
- raceTrack.width = 10;
- raceTrack.numberOfHairpinTurns = 2;
setter injuection 允許你配置對(duì)象,但是這在對(duì)象設(shè)計(jì)上引入了額外的可變性,需要測試和解決。幸運(yùn)的是,導(dǎo)致初始化不能訪問或者不能修改的兩種主要場景都可以避免。
類注冊(cè)
使用類注冊(cè)工廠模式也就是對(duì)象不能修改他們的初始化器。
- NSArray *raceCarClasses = @[
- [RCFastRaceCar class],
- [RCSlowRaceCar class],
- ];
- NSMutableArray *raceCars = [[NSMutableArray alloc] init];
- for (Class raceCarClass in raceCarClasses) {
- // All race cars must have the same initializer ("init" in this case).
- // This means we can't customize different subclasses in different ways.
- [raceCars addObject:[[raceCarClass alloc] init]];
- }
對(duì)于這樣的問題可以用工廠模式 blocks簡單代替類型申明的列表。
- 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) {
- // We now no longer care which initializer is being called.
- [raceCars addObject:raceCarFactory()];
- }
Storyboards
storyboards提供便捷的方法來布置我們的用戶界面,但是給依賴注入帶來了問題。尤其是在storyboard中初始化View Controller不允許你選擇調(diào)用哪個(gè)初始化方法。同樣地,當(dāng)在sytoyboard中定義頁面跳轉(zhuǎn)的時(shí)候,目標(biāo)View Controller不會(huì)給你自定初始化方法來產(chǎn)生實(shí)例。
解決方法就是避免使用storyboard。這聽起來是個(gè)極端的解決方案,但是我們將發(fā)現(xiàn)使用storyboard會(huì)產(chǎn)生大量其他問題。另外,不想失去storyboard給我們帶來的便利,可以使用XIB,而且XIB可以讓你自定義初始化器。
公有 Vs.私有
依賴注入鼓勵(lì)你在公共接口中暴露更多的對(duì)象。如前所述,這有很多優(yōu)點(diǎn)。搭建框架時(shí)候,他能大大的充實(shí)你的公共API。而且運(yùn)用依賴注入,公共對(duì)象A可以使用私有對(duì)象B(這樣輪流過來就可以使用私有對(duì)象C),但對(duì)象B和C從來沒有暴露在框架外面。對(duì)象A在初始化器中依賴注入對(duì)象B,然后對(duì)象B的構(gòu)造器又創(chuàng)建了公共對(duì)象C.
- // In public ObjectA.h.
- @interface ObjectA
- // Because the initializer uses a reference to ObjectB we need to
- // make the Object B header public where we wouldn't have before.
- - (instancetype)initWithObjectB:(ObjectB *)objectB;
- @end
- @interface ObjectB
- // Same here: we need to expose ObjectC.h.
- - (instancetype)initWithObjectC:(ObjectC *)objectC;
- @end
- @interface ObjectC
- - (instancetype)init;
- @end
你也不希望框架的使用者擔(dān)心對(duì)象B和對(duì)象C的實(shí)現(xiàn)細(xì)節(jié),我們可以通過協(xié)議解決這個(gè)問題。
- @interface ObjectA
- - (instancetype)initWithObjectB:(id )objectB;
- @end
- // This protocol exposes only the parts of the original ObjectB that
- // are needed by ObjectA. We're not creating a hard dependency on
- // our concrete ObjectB (or ObjectC) implementation.
- @protocol ObjectB
- - (void)methodNeededByObjectA;
- @end
結(jié)束語
依賴注入很適合objective-c和之后的Swift。恰當(dāng)?shù)倪\(yùn)用可以使你的代碼庫更加易讀,易測試,易維護(hù)。





















