本文由CocoaChina譯者@祈祈祈祈祈祈翻譯自Square的技術博客
原文:Dependency Injection: Give Your iOS Code a Shot in the Arm
什麼是Dependency Injection(依賴注入)?
在許多程序設計語言裡,比如Java,C#,依賴注入(DI)都是一種較流行的設計模式,但是它在Objective-C中沒有得到廣泛應用。本文旨在用 Objective-C的例子對依賴注入進行簡要介紹,同時介紹 Objective-C 代碼中使用依賴注入的實用方法。盡管文章主要針對Objective-C,但是提到的所有概念對Swift同樣適用。
依賴注入的概念十分簡單:一個對象應該通過依賴傳遞獲得,而不是創建他們本身。推薦Martin Fowler的 excellent discussion on the subject 作為背景材料閱讀。
依賴可以通過initializer(初始化器)(或者constructor(構造器))或者屬性(set方法)傳遞給對象。它們通常被稱為"constructor injection" 和 "setter injection"。(構造器注入和 set方法注入)
Constructor Injection:
- (instancetype)initWithDependency1:(Dependency1 *)d1 dependency2:(Dependency2 *)d2;
Setter Injection:
@property (nonatomic, retain) Dependency1 *dependency1; @property (nonatomic, retain) Dependency2 *dependency2;
根據Fowler的描述,一般情況下,首選構造器注入,在構造函數注入不適合的情況下才選擇setter注入。雖然使用構造函數注入時,很可能還是要給這些依賴定義屬性,但你可以給這些屬性設置成read only從而簡化你的對象API。
為什麼要使用依賴注入?
使用依賴注入有很多優點:
1. 依賴申明清晰。 一個對象需要進行的操作變得一目了然,同時也容易消除危險的隱藏依賴,比如全局變量。
2.組件化。 依賴注入提倡composition over inheritance,以提高代碼的重用性。
3. 更易定制。 當創建對象的時,在特殊情況下更易對對象進行部分的定制。
4. 明確從屬關系。 特別是在使用構造器依賴注入時,對象所有權規則嚴格執行--可以建立一個直接非循環的對象圖。
5.易測試性。 依賴注入比其他方法更能提高對象的易測試性。因為通過構造器創建這些對象很簡單,也沒有必要管理隱藏的依賴。此外,模擬依賴變得簡單,從而可以把測試集中在被測試的對象上。
在代碼中使用依賴注入
你的代碼庫可能還沒有使用依賴注入設計模式,但是轉換一下很簡單。依賴注入很好的一點就是你不需要讓整個工程的代碼全都采取該模式。相反,你可以在代碼庫的特定區域運用然後從那邊擴展開來。
二級各種類的注入
首先,把類分為兩種:基本類型和復雜類型。基本類型是沒有依賴的,或者是只依靠其他基本類型。基本類型基本不用被繼承,因為他們功能清晰不變,也不需要鏈接外部資源。許多基本類型都是從Cocoa 自身獲得的,比如NSString, NSArray, NSDictionary, and NSNumber.
復雜類型就相反了。它們有復雜的依賴,包括應用級別的邏輯(需要修改的部分),或者訪問額外的資源,例如磁盤,網絡或者全局內存服務。應用中絕大多數類都是復雜的,包括幾乎所有的控制器對象和模型對象。很多cocoa類型也很復雜,例如NSURLConnection or UIViewController.。
根據以上分類情況,想要使用依賴注入模式最簡單的方法是先選擇應用中一個復雜的類,找到類中的初始化其他復雜對象的地方(找"alloc]init"或者"new"關鍵字)。將類中引進依賴注入,改變這一實例化對象作為初始化參數在類中傳遞而不是類初始化對象本身。
在初始化時分配依賴
讓我們來看一個例子,子對象(依賴)在母體的初始化函數中被初始化。原始的代碼如下:
@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
惰性初始化依賴
有一些對象可能一段時間後才用到,或者初始化之後才會用到,或者永遠也不會用到。沒有用依賴注入之前的例子:
@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
一般情況下賽車一般不會撞車,所以我們永遠不會使用我們的滅火器。因為需要這個對象的概率很低,我們不想在初始化方法中立即創建他們從而拖慢了每個賽車的創建。另外,如果我們的賽車需要從多個撞車中恢復過來,這就需要創建多個滅火器。對於這樣的情況,我們可以使用工廠設計模式。
工廠設計模式是標准的objectice-c blocks語法,它不需要參數並且返回一個對象的實體。一個對象可以在不需要知道如何創建他們的細節的時候就能使用他們的blocks創建依賴。
這邊是一個使用依賴注入也就是使用工廠設計模式來創建我們的滅火器的例子:
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(); // Keep the wheel factory for later in case we need a spare. _wheelFactory = [wheelFactory copy]; return self; } @end
避免笨重的配置
如果對象不應該在其他對象裡被alloc,那它應該在哪邊被alloc?是不是這樣的依賴都很難去配置?難道每次alloc他們都一樣困難?對於這些問題的解決要依靠類型的簡潔初始化器(例如+[NSDictionary dictionary]),我們將我們的對象圖配置從普通對象中取出,使他們純淨可測試,業務邏輯清晰。
在添加類型簡易初始化方法之前,確保它是有必要的。如果一個對象只有少量的參數在init方法,並且這些參數沒有合理的地默認值,那麼這個類型是不需要簡介初始化方法的,就直接調用標准的init方法就可以了。
我們將從4處地方手機我們的依賴去配置我們的對象:
值沒有合理的默認值。如每個實例都可能包含不同的布爾值或者數值。這些值應該作為參數傳給類型的簡潔初始化器。
現存的共享對象。這些對象應該作為參數傳給類型的簡潔初始器(例如 一段無線電波)。這些都是之前可能被評估成單例或者通過父類指針的對象。
新創建的對象。如果我們的對象不能將這些依賴共享給其他對象,那麼合作的對象應該在類型簡介初始化函數中新建一個實例。這些都是之前在對象的implementation裡面直接分配的對象。
系統單例。這些是cocoa提供的單例和可以直接使用的單例。這些單例的應用,如[NSFileManager defaultManager],在你的app中,預計只需要產生一個實例的類型使用可以使用單例。系統中有很多這樣的單例。
一個賽車類的簡潔初始化方法如下:
+ (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 對象使用的配置器應該放在RaceCar的@interface裡面。
系統單例
在Cocoa庫裡很多對象只有一個實例存在,例如[UIApplication sharedApplication], [NSFileManager defaultManager], [NSUserDefaults standardUserDefaults], [UIDevice currentDevice].如果一個對象依賴於以上這些對象,應該把它放進初始化器的參數。即使你的代碼中可能只有一個實例,你的測試想模擬這個實例或創建一個實例的測試避免測試的相互依賴。
建議大家在自己的代碼中避免創建全局引用的單例,也不要在一個對象第一次需要或者注入它所有依賴於它的對象時創建他的單個實例。
不可變的構造器
偶爾會有這種問題,就是一個類的初始化器/構造器不能被改變,或者直接調用。在這種情況下,應該使用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 允許你配置對象,但是這在對象設計上引入了額外的可變性,需要測試和解決。幸運的是,導致初始化不能訪問或者不能修改的兩種主要場景都可以避免。
類注冊
使用類注冊工廠模式也就是對象不能修改他們的初始化器。
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不允許你選擇調用哪個初始化方法。同樣地,當在sytoyboard中定義頁面跳轉的時候,目標View Controller不會給你自定初始化方法來產生實例。
解決方法就是避免使用storyboard。這聽起來是個極端的解決方案,但是我們將發現使用storyboard會產生大量其他問題。另外,不想失去storyboard給我們帶來的便利,可以使用XIB,而且XIB可以讓你自定義初始化器。
公有 Vs.私有
依賴注入鼓勵你在公共接口中暴露更多的對象。如前所述,這有很多優點。搭建框架時候,他能大大的充實你的公共API。而且運用依賴注入,公共對象A可以使用私有對象B(這樣輪流過來就可以使用私有對象C),但對象B和C從來沒有暴露在框架外面。對象A在初始化器中依賴注入對象B,然後對象B的構造器又創建了公共對象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
你也不希望框架的使用者擔心對象B和對象C的實現細節,我們可以通過協議解決這個問題。
@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
結束語
依賴注入很適合objective-c和之後的Swift。恰當的運用可以使你的代碼庫更加易讀,易測試,易維護。