什么是依賴注入呢?
依賴注入(DI)是一種非常流行的設(shè)計模式在許多的語言之中秽澳,比如說Java和C#,但是它似乎并沒有在Objective-C廣泛的被采用鸵荠,這篇文章的目的就是簡要的介紹一下在Objective-C中使用依賴注入的例子员帮。雖然代碼是用Objective-C寫的,但是也可以應(yīng)用到Swift邓梅。
依賴注入的概念很簡單:一個對象應(yīng)該被依賴而不是被創(chuàng)建。強烈建議閱讀Martin Fowler的excellent discussion on the subject作為背景閱讀月培。
注入可以通過對象的初始化(或者可以說構(gòu)造器)或者通過特性(setter)谷暮,這些就指的是構(gòu)造器注入和setter方法注入蔑匣。
- (instancetype)initWithDependency1:(Dependency1 *)d1
dependency2:(Dependency2 *)d2;
@property (nonatomic, retain) Dependency1 *dependency1;
@property (nonatomic, retain) Dependency2 *dependency2;
根據(jù)福勒的描述劣欢,首選構(gòu)造器注入,一般來說只有在構(gòu)造器注入不可行的情況下才會選擇settter注入裁良。在構(gòu)造器注入中凿将,你可以還是會使用@property來定義這些依賴,但是在接口中你應(yīng)該讓他們?yōu)橹蛔x价脾,
依賴注入提供了一系列的好處牧抵,其中重要的是一下幾點:
依賴定義清晰可以很明顯的看出對象需要什么來進行操作,從而可以避免依賴類似(dependencies—like)侨把,全部變量消失(globals—disappear)犀变。
組合依賴注入提倡組合而不是繼承, 提高代碼的可重用性。
定制簡單當(dāng)創(chuàng)建一個對象的時候秋柄,可以輕松的定制對象的每一個部分來面對最糟糕的情況获枝。
所有權(quán)清晰特別是當(dāng)使用構(gòu)造函數(shù)注入,嚴格執(zhí)行對象的所有權(quán)規(guī)則骇笔,幫助建立一個有向無循環(huán)對象圖省店。
可測試性更重要的是,依賴注入提高了你的對象的可測性笨触。因為他們可以被非常簡單的創(chuàng)建通過初始化方法萨西,沒有隱藏的依賴關(guān)系需要管理。此外旭旭,可以簡單的模擬出依賴讓你關(guān)注于被測試的對象。
你的代碼可能還沒有使用依賴注入的設(shè)計模式來設(shè)計葱跋,但是很容易上手持寄。依賴注入的一個很好的方面是,你不需要采用“全或無”娱俺。相反稍味,你可以將它應(yīng)用到代碼的特定區(qū)域并從那里展開。
首先荠卷,讓我們把類分成兩種:基本類和復(fù)合類模庐。基本類是沒有依賴的油宜,或只依賴于其他基礎(chǔ)類掂碱×耍基本類被繼承的可能性極低,因為它們的功能是明確的疼燥,不變的沧卢,不引用外部資源。Cocoa本身有很多的基本類醉者,如NSStringNSArray但狭,NSDictionaryNSNumber……。
復(fù)雜類則相反撬即,他們有復(fù)雜的依賴關(guān)系立磁,包括應(yīng)用級別的邏輯(可能被改變),或者訪問外部資源剥槐,比如磁盤唱歧,網(wǎng)絡(luò)或者一些全局的服務(wù)。在你應(yīng)用內(nèi)的大多數(shù)類都是復(fù)雜的才沧,包括大多數(shù)的控制器對象和模型對象迈喉。需要Cocoa類也食非常復(fù)雜的,比如說NSURLConnection或者UIViewController
判斷這個的最簡單方法就是拿起一個復(fù)雜類然后看它初始化其他復(fù)雜類的地方(搜索"alloc] init" or "new]")温圆,介紹了類的依賴注入挨摸,改變這個實例化的對象是通過作為一個類的初始化參數(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
一些對象有時會在很后面才會被用到,有時甚至在初始化之后不會被用到锅移,舉個例子(在依賴注入之前)也許看起來是這樣子的:
@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
在這種情況下熔掺,賽車希望不會出事,而我們也不需要使用滅火器非剃。由于需要該對象的機會很低置逻,我們不想每一個賽車的創(chuàng)建的時候慢下來。另外备绽,如果我們的賽車需要從多個事故恢復(fù)券坞,就需要創(chuàng)建多個滅火器。針對這些情況肺素,我們可以使用工廠恨锚。
工廠標(biāo)準(zhǔn)的Objective-C塊不需要參數(shù)和返回一個對象的實例。一個對象可以控制當(dāng)其依賴的是使用這些塊而不需要詳細了解如何創(chuàng)建倍靡。
這里是一個例子使用了依賴注入和工廠模式來創(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ù)量的依賴的時候也是非常有用的,即使是在初始化期間,例子如下:
@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
如果對象不應(yīng)該在其他對象分配他挎,他們在哪里分配筝尾?是不是所有的這些依賴關(guān)系很難配置?大多不都是一樣的嗎雇盖?這些問題的解決在于類方便的初始化(想一想+[NSDictionary dictionary])忿等。我們會把我們的對象圖的配置移出我們正常的對象,把他們抽象出來崔挖,測試贸街,業(yè)務(wù)邏輯。
在我們添加簡單初始化方法之前狸相,先確認它是有必要的薛匪。如果一個對象只有少量的參數(shù)在初始化方法之中,這些參數(shù)沒有規(guī)律脓鹃,那么簡單初始化方法是沒有必要的逸尖。調(diào)用者應(yīng)該使用標(biāo)準(zhǔn)初始化方法。
我們需要從四個地方搜集依賴來配置我們的對象:
沒有明確默認值的變量這些包括布爾類型和數(shù)字類型的有可能在每個實例中是不同的瘸右。這些變量應(yīng)該在簡單初始化方法的參數(shù)中娇跟。
共享對象這些應(yīng)該在簡單初始化方法中作為參數(shù)(例如電臺頻率),這些對象先前可能被訪問作為一個單例或者通過父指針太颤。
新創(chuàng)建的對象如果我們的對象不與另一個對象分享這種依賴關(guān)系苞俘,合作者對象應(yīng)該是新實例化的類中簡單初始化。這些都是以前分配的對象直接在該對象的實現(xiàn)龄章。
系統(tǒng)單例Cocoa提供了很多單例可以直接被訪問吃谣,例如[NSFileManager defaultManager],那里有一個明確的目的只需要一個實例被使用在應(yīng)用中做裙,還有很多單例在系統(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)該是最合適的。一個經(jīng)常被用到的(可重用)的配置在一個.m文件中锚贱,而一個特定的配置文件應(yīng)該在在@interface RaceCar (FooConfiguration)的類別文件中比如fooRaceCar仔戈。
在Cocoa的許多對象中,只有一種實例將一直存在拧廊。 例如[UIApplication sharedApplication],[NSFileManager defaultManager],[NSUserDefaults standardUserDefaults], 和[UIDevice currentDevice]杂穷,如果一個對象對他們存在依賴,那么應(yīng)該在初始化的參數(shù)中包含他們卦绣,即使也許在你的代碼中只有一個實例,你的測試可能要模擬實例或創(chuàng)建一個實例每個測試避免測試相互依存飞蚓。
建議避免創(chuàng)建全局引用單例滤港,而是創(chuàng)建一個對象的單個實例當(dāng)它第一次被需要的時候并把它注入到所有依賴它的對象中去。
偶爾在某個類的構(gòu)造器或者初始化方法不能被改變或者調(diào)用的時候,我們可以使用setter注入溅漾。例如:
// 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注入允許你配置對象山叮,但是它也引入了易變性,你必須進行測試和處理添履,幸運的是屁倔,有兩個場景導(dǎo)致初始化方法不能改變和調(dià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]];
}
一個簡單的替代方法就是使用工廠塊而不是類的聲明列表。
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()];
}
故事提供了一個方便的方式來設(shè)計你的用戶界面往衷,但也帶來一些問題的時候钞翔。特別是依賴注入,實例化初始視圖控制器在故事板中不允許你選擇初始化方法席舍。同樣布轿,當(dāng)下面的事情就是在故事板中定義,目標(biāo)視圖控制器被實例化也不允許你指定的初始化方法来颤。
解決這個問題的辦法是避免使用故事板汰扭。這似乎是一個極端的解決方案,但是我們發(fā)現(xiàn)故事板有其他問題在大型團隊的開發(fā)中福铅。此外萝毛,沒有必要失去大部分故事版的好處。xibs提供所有相同的好處和故事板本讥,除了segues珊泳,但仍然讓你自定義初始化。
依賴注入鼓勵你暴露更多的對象在你的公共接口拷沸。如前所述色查,這有許多好處。但當(dāng)你構(gòu)建框架撞芍,它會明顯的膨脹公共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都是具體的實現(xiàn),但你并不需要框架的使用者來擔(dān)心他們哟玷,我們可以通過協(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
依賴注入在 Objective-C(同樣在Swift),合理的使用會使你的代碼更加易讀、易測試和易維護喉脖。