一、依賴注入(Dependency Injection)
今天我們討論的內容核心是面向接口編程,我決定還是要從依賴注入開始講起,因為DI的思想可以說是面向接口編程思想的特殊表現,也可以說是與面向接口編程相輔相成。
舉個例子~
我們有一個公交車類(Bus),每天早上6點鐘需要發車(work),為其分配對應的司機(Driver),看代碼
<pre>
@implementation Bus
- (void)work {
Driver *driver = [[Driver alloc] initWithName:@"張三"];
//dosomething
}
@end
</pre>
上面這段代碼中,Bus對象的運作需要用到Driver對象,因而創建了一個Driver對象,我們稱Bus對Driver有一個依賴。這樣的強耦合關系會因為日后的變化而給我們帶來很多麻煩,不久將來張三師傅辭職了,我們需要修改Bus-work()的代碼,也就是說在開發過程中非常不便于單元測試(一是不能方便地更換各種Driver對象,二是如果Driver這個職位創建是耗時操作或者高成本操作,我們并不能使用準備好的Driver實現快速重復測試)。
<pre>
@implementation Bus
@property (strong, nonatomic) Driver *driver;
- (instancetype)initWithDriver:(Driver *)driver {
self = [super init];
if (self) {
self.dirver = driver;
}
return self;
} - (void)work {
//dosomething
}
@end
</pre>
以上這段代碼我們通過init方法,為Bus對象傳入了一個Driver對象,像這種非自己主動初始化依賴,而從外部通過注入點注入依賴的方式,我們就稱為依賴注入,而例子中的這種注入的方法稱之為構造器注入。明顯的,這個場景中Bus和Driver的耦合因此輕了一層。說到解耦,并不是說Bus和Driver之間的依賴關系就不存在了,在Bus的范圍內看來,只是將依賴建立從編譯期間推遲到了運行期間,畢竟Bus無論如何也是需要Driver提供服務的。
“依賴就像是系統中的plugin,主系統并不強依賴于任何一個插件,但一旦插件被加載,主系統就應該可以準確調用適當插件的功能”。
類似這樣的注入方式還有
- 屬性注入
- 方法注入
- 環境上下文注入
- 子類重寫方法注入等
不同的知識注入的手段,思想還是一樣的。
二、開閉原則(Open-Closed Principle)
他的原文解釋是Software entities should be open for extension,but closed for modification,對擴展開放,對修改關閉。也就是說我們對模塊的設計,應該滿足將來在不可修改源代碼的情況下對模塊的職能擴展,或者改變模塊的行為。單單這句話就能表現出OCP可怕的地位,他迫使我們主動考慮了將來,使應用保證了核心代碼的穩定性和對新需求的靈活性。
三、依賴獲取(Dependency Locate)
上面我們理解了依賴注入的基礎思想,讓依賴顯式化,為依賴提供合適的注入點(針口),提升程序的靈活性。帶來的結果就是當我們需要更換依賴的時候不需要對使用服務的類(姑且叫作客戶類)作代碼修改,將提供服務的類(服務類)由注入點注入到客戶類中,耦合的確輕了一層,也符合OCP原則,ok現在我們往外跳一層,在實例化客戶類的角色上下文中,需要實例化服務類進而完成對客戶類注入,服務類的更變必然導致此處代碼的修改,這時OCP又要站出來打差評。
此時有必要講下依賴獲取。既然有注入,當然也應該有獲取,但這兩者并不是先后執行的兩個過程,而是相同目的的同一種操作,換句話說,我們讓客戶類由被動注入轉換成主動獲取,繼續貫徹的仍然是依賴注入思想。
DL就是在系統中配置一個獲取點,客戶類依賴于服務類的接口而不直接依賴服務類,客戶類根據自身需要從獲取點主動獲取服務類為其提供服務。理解了DI,對DL的概念肯定是迎刃而解。
我們思考下,客戶類只知道獲取點,按照道上的規矩交貨的對方的身份完全不需要去了解,有沒有發現面向接口(POP)的內體又暴露了一點?
四、更高級的依賴注入
認識完DI的另一種方式依賴獲取后,做依賴注入的辦法就不僅僅局限于上文列舉的幾種最基本的依賴注入方式。目前比較主流的有配置文件依賴注入,反射依賴注入,例如java中強大的Spring和移植到.NET平臺的Spring.NET,.NET中自己的Autofac,他們是結合配置文件和反射工作的,而oc中的objection我看了下是通過key-Value內存容器來做的DI,如果我自己做的話,還可以使用runtime target-action方式(類似于其他語言的反射)。
下面還是用一個簡單的例子來增強對通過配置文件做依賴獲取的認識:
<pre>
//定義一個主題接口,讓所有主題都實現它
@protocol ItfThemeFactory
- (void)drawing;
@end
//主題
@implementation SpringFactory
- (void)drawing {
//drawing theme...
}
@end
@implementation SummerFactory
- (void)drawing {
//drawing theme...
}
@end
//主題工廠Animator
@interface ThemeFactoryAnimator : NSObject
@property (strong, nonatomic) id themeFactory;
@end
@implementation ThemeFactoryAnimator
- (id)themeFactory {
NSString *path = [[NSBundle mainBundle] pathForResource:@"theme" ofType:@"plist"];
NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:path];
NSString *theme = [dict objectForKey:@"theme"];
if ([theme isEqualToString:@"spring"]) {
_themeFactory = [[SpringFactory alloc] init];
} else if ([theme isEqualToString:@"summer"]) {
_themeFactory = [[SummerFactory alloc] init];
} else {
//assert
}
}
@end
//在執行方法里我們要做什么?
- (void)work {
ThemeFactoryAnimator *tfAnimator = [ThemeFactoryAnimator alloc] init];
id themeFactory = tfAnimator.themeFactory;
[themeFactory drawing];
}
</pre>
以上,我們只需要在執行方法(-work())中拿到themeFactory,對界面進行渲染即可,而原本有可能出現依賴的地方——ThemeFactoryAnimator已經不依賴于外部注入,而僅僅依賴于我的theme.plist配置文件,也可以說我們將多態封裝到了這個“獲取點”內,因此主題的改變映射到了配置文件中對應內容的改變,但是這個更換主題系統目前就利用DI變得符合OCP原則了嗎?不是的,雖然依賴的改變已經映射到了客戶類封裝的外部——配置文件中,可是我們還是無法避免if-else結構的存在,我們可以不修改代碼自由更換主題,可是如果又開發出了一套新的主題呢?這個系統對于未來還是無能為力,這一part的重點是依賴獲取,至于怎么消除這種缺陷?看完這篇文章也許你就自然明白了。
五、面向接口編程(Protocol-oriented programming)
接口泛指實體把自己提供給外界的一種抽象化物,用以由內部操作分離出外部溝通方法,使其能被修改內部而不影響外界其他實體與其交互的方式
舉個例子說明下:
<pre>
//首先我們定義一個交通工具接口
@protocol Transportation
- (void)drinking;
- (void)freight;
@end
//還有一個發光體接口
@protocol Irradiative - (void)shine;
@end
//當然drinking就代表補需,汽車飛機的內部實現就是加油,馬牛的內部實現就是吃草喝水什么的。freight就是裝載
//當上帝創造馬的時候,讓馬遵守并實現這個接口:
@implementation Horse
(void)drinking {
//吃草,喝果汁
}(void)freight {
//停住腳步,或者半蹲,讓友好的人類騎上去
}
@end
//當人類創造飛機的時候,慘了,不知道去哪里找上帝溝通,又怕疏漏了什么影響了這個世界的運行規律?沒事上帝給我們留下了Transportation接口,而且飛機同時還要遵守發光體接口Irradiative,于是:
@implementation Aircraft(void)drinking {
//加油, 92的
}(void)freight {
//降落,熄火,開艙門
}(void)shine {
//燃燒汽油,生物質能轉化成電能,照你
}
@end
</pre>
六、面向接口編程(架構)
無論是哪種架構方式,層次關系肯定是撇不開的,并且層次關系也代表著一種架構的主心骨,無論業務分層,功能分層,還是角色分層,存在于各個位置的依賴關系都需要我們去正視,而POP的目的正是為了化解這些強依賴,打破上層實例化下層去為其提供服務的強耦合,在大型項目中,一層的變化可能會聯動1+N層,這樣的變化是致命的,正如上文我們提到過的,讓一個實體由依賴另一個實體,轉變成依賴一個接口,將被依賴實體的變化隔絕于接口之外。
七、iOS面向協議的編程思想
其實講了這么多,大家或許已經發現,在我們日常OC編碼中似乎隨處可見接口編程的痕跡?——侵蝕了我們項目各個模塊的代理模式,代理模式的工作原理就是,一方使用protocol(接口)劃定一個或一組規則,成為其代理的角色必須遵守這一系列規則,最后根據規則去辦事,好處依然是那么明顯,主體并不需要與代理溝通,代理也不需要做多余的培訓,直接上崗,從這里又強化了一遍接口即一種由內部操作分離出外部溝通方法,而核心就是一系列規則,通過接口工作,比直接訪問屬性或者方法穩健得多。
舉個例子說明~
<pre>
pragma mark - 面向對象傳統的方式:
//服務實現者 甲方 ,編寫一個服務類
@interface MusicLoadingProtocolObj()
@end
@implementation MusicLoadingProtocolObj
- (void)requestWithUrl:(NSURL *)url Param:(NSDictionary *)param {
//do something
}
@end
//服務使用者 乙方 ,通過接口獲取服務類
import "MusicLoadingProtocolObj.h"
@interface Client()
@end
@implementation Client
- (void)work {
MusicLoadingProtocolObj *musicLoadingProtocolObj = [MusicLoadingProtocolObj alloc] init];
[musicLoadingProtocolObj requestWithUrl:url Param:param];
}
@end
//當然,在這里我們已經應用了構造器注入的DI思想。或者我們如果使用屬性注入?那么當然就沒那么直觀,沒有貫徹接口編程的思想。
pragma mark - 接下來就是面向接口(POP)的做法:
//首先,定義一個ServiceProtocol
@protocol MusicLoadingProtocol
- (void)requestWithUrl:(NSURL *)url Param:(NSDictionary *)param;
@end
//甲方
@interface MusicLoadingProtocolObj()
@end
@implementation MusicLoadingProtocolObj
- (void)requestWithUrl:(NSURL *)url Param:(NSDictionary *)param {
//do something
}
@end
//乙方
@interface Client()
@end
@implementation Client
- (void)work {
id service = [[JSObjection defaultInjector] getObject:@protocol(MusicLoadingProtocol)];
[service requestWithUrl:url Param:param];
}
@end
</pre>
上例中筆者借助了OC的一個輕量級的DI框架objection,服務實現者甲方獨立編寫服務實現,而后將服務通過objection綁定到protocol之上,去看看服務使用者,乙方利用objection通過protocol拿到服務類實例,根據protocol中定義的規則,馬上就實現了服務。不需要import,不需要實例化,高度解耦,并且符合OCP原則。objection的原理就是上文提到的key-value內存映射表,對于大型項目,多小組分項目開發再合并的生產線,POP是必不可少的。
如果說我們在輕型開發中不想使用框架,我們也可以談談自己實現POP+DI,利用起OC的利器——runtime。其實在上例已經埋下伏筆,這次我們的乙方可以這樣做:
<pre>
//乙方
@interface Client()
@end
@implementation Client
- (void)work {
NSString *clazzName = @"MusicLoadingProtocol";
[clazzName stringByAppendingString:@"Obj"];
Class serviceClazz = NSClassFromString(clazzName);
id service = [serviceClazz alloc] init];
[service requestWithUrl:url Param:param];
}
@end
</pre>
就是這樣,甲乙雙發約定了以接口名+Obj字符串的規則去定義服務類,乙方做DL時只需要配合runtime,也是輕而易舉。
那么如果服務類實例化需要參數呢?
配置文件能解決這個問題,上文有提到Spring框架做DI的原理就是反射+xml,一般來說大部分支持反射機制語言的DI框架原理都是相似的,這里說下筆者了解的兩種主流注入原理,構造器注入和屬性注入,記得上文也提到過著兩種注入方式,筆者強調過那只是一種思想,不是定性的一種方法,ok來看下那些DI框架是怎樣做的。
構造器注入
在進行依賴獲取的時候,DI框架通過反射機制得到待創建類的構造方法,然后根據構造器所需參數的類型或者順序,在DI容器節點中尋找,然后提供參數,創建實例。屬性注入
同樣的,在進行DL時,通過反射得到待創建類型的所有屬性,然后根據屬性在DI容器節點中進行匹配,有則創建提供,無則跳過。
八、最后
- 依賴注入+接口編程
- 調用者無須關心對象任何實現,只需按照接口規則調用服務
- 在系統分析和架構中,分清層次和依賴關系,每個層次不是直接向其上層提供服務(即不是直接實例化在上層中),而是通過定義一組接口,僅向上層暴露其接口功能,上層對于下層僅僅是接口依賴,而不依賴具體類。
- 服務使用端由對對象的依賴轉變成對接口的依賴,這樣甚至可以在服務提供對象還未存在之前編碼(分子項目開發)
依賴注入只是一種思想,其實也就是一個過程,依賴注入用到了面向接口的編程思想,面向接口的架構實現用到了依賴注入的執行方式。而面向接口編程和面向對象編程并不是平級的,它并不是比面向對象編程更先進的一種獨立的編程思想,而是附屬于面向對象思想體系,屬于其中一部分。或者說,它是面向對象編程體系中的思想精髓之一。