什么是依賴注入呢?
依賴注入(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)。
組合依賴注入提倡組合而不是繼承, 提高代碼的可重用性。
定制簡單當創(chuàng)建一個對象的時候,可以輕松的定制對象的每一個部分來面對最糟糕的情況。
所有權(quán)清晰特別是當使用構(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)建多個滅火器。針對這些情況,我們可以使用工廠。
工廠標準的Objective-C塊不需要參數(shù)和返回一個對象的實例。一個對象可以控制當其依賴的是使用這些塊而不需要詳細了解如何創(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)該使用標準初始化方法。
我們需要從四個地方搜集依賴來配置我們的對象:
沒有明確默認值的變量這些包括布爾類型和數(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)建一個對象的單個實例當它第一次被需要的時候并把它注入到所有依賴它的對象中去。
偶爾在某個類的構(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è)計你的用戶界面,但也帶來一些問題的時候。特別是依賴注入,實例化初始視圖控制器在故事板中不允許你選擇初始化方法。同樣,當下面的事情就是在故事板中定義,目標視圖控制器被實例化也不允許你指定的初始化方法。
解決這個問題的辦法是避免使用故事板。這似乎是一個極端的解決方案,但是我們發(fā)現(xiàn)故事板有其他問題在大型團隊的開發(fā)中。此外,沒有必要失去大部分故事版的好處。xibs提供所有相同的好處和故事板,除了segues,但仍然讓你自定義初始化。
依賴注入鼓勵你暴露更多的對象在你的公共接口。如前所述,這有許多好處。但當你構(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),但你并不需要框架的使用者來擔心他們,我們可以通過協(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),合理的使用會使你的代碼更加易讀、易測試和易維護。