代碼的一針強心劑——依賴注入
什么是Dependency Injection(依賴注入)?
在許多程序設(shè)計語言里,比如Java,C#,依賴注入(DI)都是一種較流行的設(shè)計模式,但是它在Objective-C中沒有得到廣泛應(yīng)用。本文旨在用 Objective-C的例子對依賴注入進行簡要介紹,同時介紹 Objective-C 代碼中使用依賴注入的實用方法。盡管文章主要針對Objective-C,但是提到的所有概念對Swift同樣適用。
依賴注入的概念十分簡單:一個對象應(yīng)該通過依賴傳遞獲得,而不是創(chuàng)建他們本身。推薦Martin Fowler的 excellent discussion on the subject 作為背景材料閱讀。
依賴可以通過initializer(初始化器)(或者constructor(構(gòu)造器))或者屬性(set方法)傳遞給對象。它們通常被稱為"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è)置成read only從而簡化你的對象API。
為什么要使用依賴注入?
使用依賴注入有很多優(yōu)點:
- 1. 依賴申明清晰。 一個對象需要進行的操作變得一目了然,同時也容易消除危險的隱藏依賴,比如全局變量。
- 2.組件化。 依賴注入提倡composition over inheritance,以提高代碼的重用性。
- 3. 更易定制。 當(dāng)創(chuàng)建對象的時,在特殊情況下更易對對象進行部分的定制。
- 4. 明確從屬關(guān)系。 特別是在使用構(gòu)造器依賴注入時,對象所有權(quán)規(guī)則嚴(yán)格執(zhí)行--可以建立一個直接非循環(huán)的對象圖。
- 5.易測試性。 依賴注入比其他方法更能提高對象的易測試性。因為通過構(gòu)造器創(chuàng)建這些對象很簡單,也沒有必要管理隱藏的依賴。此外,模擬依賴變得簡單,從而可以把測試集中在被測試的對象上。
在代碼中使用依賴注入
你的代碼庫可能還沒有使用依賴注入設(shè)計模式,但是轉(zhuǎn)換一下很簡單。依賴注入很好的一點就是你不需要讓整個工程的代碼全都采取該模式。相反,你可以在代碼庫的特定區(qū)域運用然后從那邊擴展開來。
二級各種類的注入
首先,把類分為兩種:基本類型和復(fù)雜類型?;绢愋褪菦]有依賴的,或者是只依靠其他基本類型。基本類型基本不用被繼承,因為他們功能清晰不變,也不需要鏈接外部資源。許多基本類型都是從Cocoa 自身獲得的,比如NSString, NSArray, NSDictionary, and NSNumber.
復(fù)雜類型就相反了。它們有復(fù)雜的依賴,包括應(yīng)用級別的邏輯(需要修改的部分),或者訪問額外的資源,例如磁盤,網(wǎng)絡(luò)或者全局內(nèi)存服務(wù)。應(yīng)用中絕大多數(shù)類都是復(fù)雜的,包括幾乎所有的控制器對象和模型對象。很多cocoa類型也很復(fù)雜,例如NSURLConnection or UIViewController.。
根據(jù)以上分類情況,想要使用依賴注入模式最簡單的方法是先選擇應(yīng)用中一個復(fù)雜的類,找到類中的初始化其他復(fù)雜對象的地方(找"alloc]init"或者"new"關(guān)鍵字)。將類中引進依賴注入,改變這一實例化對象作為初始化參數(shù)在類中傳遞而不是類初始化對象本身。
在初始化時分配依賴
讓我們來看一個例子,子對象(依賴)在母體的初始化函數(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
惰性初始化依賴
有一些對象可能一段時間后才用到,或者初始化之后才會用到,或者永遠(yuǎn)也不會用到。沒有用依賴注入之前的例子:
- @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
一般情況下賽車一般不會撞車,所以我們永遠(yuǎn)不會使用我們的滅火器。因為需要這個對象的概率很低,我們不想在初始化方法中立即創(chuàng)建他們從而拖慢了每個賽車的創(chuàng)建。另外,如果我們的賽車需要從多個撞車中恢復(fù)過來,這就需要創(chuàng)建多個滅火器。對于這樣的情況,我們可以使用工廠設(shè)計模式。
工廠設(shè)計模式是標(biāo)準(zhǔn)的objectice-c blocks語法,它不需要參數(shù)并且返回一個對象的實體。一個對象可以在不需要知道如何創(chuàng)建他們的細(xì)節(jié)的時候就能使用他們的blocks創(chuàng)建依賴。
這邊是一個使用依賴注入也就是使用工廠設(shè)計模式來創(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
工廠模式在我們需要創(chuàng)建未知個數(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#
避免笨重的配置
如果對象不應(yīng)該在其他對象里被alloc,那它應(yīng)該在哪邊被alloc?是不是這樣的依賴都很難去配置?難道每次alloc他們都一樣困難?對于這些問題的解決要依靠類型的簡潔初始化器(例如+[NSDictionary dictionary]),我們將我們的對象圖配置從普通對象中取出,使他們純凈可測試,業(yè)務(wù)邏輯清晰。
在添加類型簡易初始化方法之前,確保它是有必要的。如果一個對象只有少量的參數(shù)在init方法,并且這些參數(shù)沒有合理的地默認(rèn)值,那么這個類型是不需要簡介初始化方法的,就直接調(diào)用標(biāo)準(zhǔn)的init方法就可以了。
我們將從4處地方手機我們的依賴去配置我們的對象:
值沒有合理的默認(rèn)值。如每個實例都可能包含不同的布爾值或者數(shù)值。這些值應(yīng)該作為參數(shù)傳給類型的簡潔初始化器。
現(xiàn)存的共享對象。這些對象應(yīng)該作為參數(shù)傳給類型的簡潔初始器(例如 一段無線電波)。這些都是之前可能被評估成單例或者通過父類指針的對象。
新創(chuàng)建的對象。如果我們的對象不能將這些依賴共享給其他對象,那么合作的對象應(yīng)該在類型簡介初始化函數(shù)中新建一個實例。這些都是之前在對象的implementation里面直接分配的對象。
系統(tǒng)單例。這些是cocoa提供的單例和可以直接使用的單例。這些單例的應(yīng)用,如[NSFileManager defaultManager],在你的app中,預(yù)計只需要產(chǎn)生一個實例的類型使用可以使用單例。系統(tǒng)中有很多這樣的單例。
一個賽車類的簡潔初始化方法如下:
- + (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ù)用的配置文件將作為對象放在.m文件里面,而由一個特殊的Foo 對象使用的配置器應(yīng)該放在RaceCar的@interface里面。
系統(tǒng)單例
在Cocoa庫里很多對象只有一個實例存在,例如[UIApplication sharedApplication], [NSFileManager defaultManager], [NSUserDefaults standardUserDefaults], [UIDevice currentDevice].如果一個對象依賴于以上這些對象,應(yīng)該把它放進初始化器的參數(shù)。即使你的代碼中可能只有一個實例,你的測試想模擬這個實例或創(chuàng)建一個實例的測試避免測試的相互依賴。
建議大家在自己的代碼中避免創(chuàng)建全局引用的單例,也不要在一個對象第一次需要或者注入它所有依賴于它的對象時創(chuàng)建他的單個實例。
不可變的構(gòu)造器
偶爾會有這種問題,就是一個類的初始化器/構(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 允許你配置對象,但是這在對象設(shè)計上引入了額外的可變性,需要測試和解決。幸運的是,導(dǎo)致初始化不能訪問或者不能修改的兩種主要場景都可以避免。
類注冊
使用類注冊工廠模式也就是對象不能修改他們的初始化器。
- 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]];
- }
對于這樣的問題可以用工廠模式 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)用哪個初始化方法。同樣地,當(dāng)在sytoyboard中定義頁面跳轉(zhuǎn)的時候,目標(biāo)View Controller不會給你自定初始化方法來產(chǎn)生實例。
解決方法就是避免使用storyboard。這聽起來是個極端的解決方案,但是我們將發(fā)現(xiàn)使用storyboard會產(chǎn)生大量其他問題。另外,不想失去storyboard給我們帶來的便利,可以使用XIB,而且XIB可以讓你自定義初始化器。
公有 Vs.私有
依賴注入鼓勵你在公共接口中暴露更多的對象。如前所述,這有很多優(yōu)點。搭建框架時候,他能大大的充實你的公共API。而且運用依賴注入,公共對象A可以使用私有對象B(這樣輪流過來就可以使用私有對象C),但對象B和C從來沒有暴露在框架外面。對象A在初始化器中依賴注入對象B,然后對象B的構(gòu)造器又創(chuàng)建了公共對象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)心對象B和對象C的實現(xiàn)細(xì)節(jié),我們可以通過協(xié)議解決這個問題。
- @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ù)倪\用可以使你的代碼庫更加易讀,易測試,易維護。