依賴(lài)注入(Dependency Injection)
依賴(lài)注入最大的特點(diǎn)就是:幫助我們開(kāi)發(fā)出松散耦合(loose coupled)、可維護(hù)、可測(cè)試的代碼和程序。這條原則的做法是大家熟知的面向接口,或者說(shuō)是面向抽象編程。 眾所周知該編程思想在各大語(yǔ)言中都有體現(xiàn)如jave、C++、PHP以及.net中。當(dāng)然設(shè)計(jì)模式的廣泛程度遠(yuǎn)遠(yuǎn)大于這些,iOS當(dāng)然也不例外。 本文主要介紹本人在學(xué)習(xí)Dependency Injection的時(shí)候的學(xué)習(xí)過(guò)程以及對(duì)一些學(xué)習(xí)資料的總結(jié),主要介紹ios中的兩大框架Objection和Typhoon。 在Android上比較流行的有RoboGuice和Dagger等.
什么是依賴(lài)注入(Dependency Injection)?
依賴(lài)注入(Dependency Injection)是一個(gè)將行為從依賴(lài)中分離的技術(shù),簡(jiǎn)單地說(shuō),它允許開(kāi)發(fā)者定義一個(gè)方法函數(shù)依賴(lài)于外部其他各種交互,而不需要編碼如何獲得這些外部交互的實(shí)例。 這樣就在各種組件之間解耦,從而獲得干凈的代碼,相比依賴(lài)的硬編碼, 一個(gè)組件只有在運(yùn)行時(shí)才調(diào)用其所需要的其他組件,因此在代碼運(yùn)行時(shí),通過(guò)特定的框架或容器,將其所需要的其他依賴(lài)組件進(jìn)行注入,主動(dòng)推入。
依賴(lài)注入是最早spring和Piconcontainer等提出,如今已經(jīng)是一個(gè)缺省主流模式,并擴(kuò)展到前端如Angular.js等等。
如果在Class A中,有Class B的實(shí)例,則稱(chēng)Class A對(duì)Class B有一個(gè)依賴(lài)。例如下面類(lèi)ViewControllerA中用到一個(gè)ViewControllerB對(duì)象,我們就說(shuō)類(lèi)ViewControllerA對(duì)類(lèi)ViewControllerB有一個(gè)依賴(lài)。
#import"ViewControllerB.h"@implementationViewControllerA- (void)buttonTapped{? ? ViewControllerB *vc = [[ViewControllerB alloc] init];? ? [self.navigationControllerpushViewController:vc animated:YES];}
仔細(xì)看這段代碼我們會(huì)發(fā)現(xiàn)存在一些問(wèn)題:
(1). 如果現(xiàn)在要改變ViewControllerB生成方式,如需要用initWithOrderid:(NSString * orderid)初始化vc,需要修改ViewControllerA代碼;
(2). 如果想測(cè)試不同ViewControllerB對(duì)象對(duì)ViewControllerA的影響很困難,因?yàn)閂iewControllerB 的初始化被寫(xiě)死在了ViewControllerA` 的構(gòu)造函數(shù)中;
(3). 如果[[ViewControllerB alloc] init]過(guò)程非常緩慢,單測(cè)時(shí)我們希望用已經(jīng)初始化好的ViewControllerB對(duì)象Mock掉這個(gè)過(guò)程也很困難。
上面將依賴(lài)在構(gòu)造函數(shù)中直接初始化是一種Hard init方式,弊端在于兩個(gè)類(lèi)不夠獨(dú)立,不方便測(cè)試。我們還有另外一種Init方式,如下:
@interfaceViewControllerA()@property(nonatomic,readonly) ViewControllerB *vcB;@end@implementationViewControllerA// vcB是在ViewControllerA被創(chuàng)建之前被創(chuàng)建的并且作為參數(shù)傳進(jìn)來(lái),// 調(diào)用者如果想,還可以自定義。- (instancetype)initWithEngine:(ViewControllerB *)vcB? {? ? ...? ? _vcB = vcB;returnself;? }@end
上面代碼中,我們將vcB對(duì)象作為構(gòu)造函數(shù)的一個(gè)參數(shù)傳入。在調(diào)用ViewControllerA的構(gòu)造方法之前外部就已經(jīng)初始化好了vcB對(duì)象。像這種非自己主動(dòng)初始化依賴(lài),而通過(guò)外部來(lái)傳入依賴(lài)的方式,我們就稱(chēng)為依賴(lài)注入。
現(xiàn)在我們發(fā)現(xiàn)上面1中存在的兩個(gè)問(wèn)題都很好解決了,簡(jiǎn)單的說(shuō)依賴(lài)注入主要有兩個(gè)好處:
解耦,將依賴(lài)之間解耦。
因?yàn)橐呀?jīng)解耦,所以方便做單元測(cè)試,尤其是Mock測(cè)試。
一:Objection 和 Typhoon這兩個(gè)框架有什么區(qū)別呢 其實(shí)這兩個(gè)框架各有優(yōu)勢(shì):
Objection框架,使用起來(lái)比較靈活,用法比較簡(jiǎn)單。示例代碼如下:
屬性注冊(cè):
@classEngine,Brakes;@interfaceCar:NSObject{? ? Engine *engine;? ? Brakes *brakes;BOOLawake;? }// Will be filled in by objection@property(nonatomic,strong) Engine *engine;// Will be filled in by objection@property(nonatomic,strong) Brakes *brakes;@property(nonatomic)BOOLawake;@implementationCarobjection_requires(@"engine", @"brakes")//屬性的依賴(lài)注入@synthesizeengine, brakes, awake;@end
方法注入:
@implementation Truckobjection_requires(@"engine", @"brakes")objection_initializer(truckWithMake:model:)//方法的依賴(lài)注入+ (instancetype)truckWithMake:(NSString *) make model: (NSString *)model {...}@end
2.對(duì)比來(lái)說(shuō)Typhoon的使用起來(lái)就比較規(guī)范,首先需要?jiǎng)?chuàng)建一個(gè)TyphoonAssembly的子類(lèi)。其需要注入的方法和屬性都需要寫(xiě)在這個(gè)統(tǒng)一個(gè)子類(lèi)中,當(dāng)然可以實(shí)現(xiàn)不同的子類(lèi)來(lái)完成不同的功能:
@interface MiddleAgesAssembly : TyphoonAssembly-(Knight*)basicKnight;-(Knight*)cavalryMan;-(id)defaultQuest;@end
屬性注入:
- (Knight*)cavalryMan{return[TyphoonDefinitionwithClass:[CavalryManclass]configuration:^(TyphoonDefinition*definition) {? ? [definitioninjectProperty:@selector(quest)with:[selfdefaultQuest]];? ? [definitioninjectProperty:@selector(damselsRescued)with:@(12)];}];}
方法注入:
- (Knight*)knightWithMethodInjection{return[TyphoonDefinitionwithClass:[Knightclass]configuration:^(TyphoonDefinition*definition) {? ? [definitioninjectMethod:@selector(setQuest:andDamselsRescued:)parameters:^(TyphoonMethod*method) {? ? ? ? [methodinjectParameterWith:[selfdefaultQuest]];? ? ? ? [methodinjectParameterWith:@321];? ? }];}];}
3.當(dāng)然還有一些硬性的區(qū)別就是Typhoon現(xiàn)在已經(jīng)支持Swift。
4.兩者維護(hù)時(shí)間都超過(guò)2年以上。
Tythoon官方介紹的優(yōu)勢(shì):
1)Non-invasive. No macrosorXML required. Uses powerful ObjC runtime instrumentation.2)No magic strings – supports IDE refactoring, code-completionandcompile-timechecking.3)Provides full-modularizationandencapsulationofconfiguration details. Let your architecturetella story.4)Dependencies declaredinany order. (The orderthatmakes sensetohumans).5)Makesiteasytohave multiple configurationsofthesame base-classorprotocol.6)Supports injectionofview controllersandstoryboard integration. Supports both initializerandpropertyinjection, plus life-cycle management.7)Powerful memory management features. Provides pre-configured objects,withoutthememory overheadofsingletons.8)Excellent supportforcircular dependencies.9)Lean. Has a very low footprint, soisappropriateforCPUandmemory constrained devices.10)While being feature-packed, Typhoon weighs-inatjust3000linesofcodeintotal.11)Battle-tested — usedinall kindsofAppstore-featured apps.
大體翻譯過(guò)來(lái):
1)非侵入性。不需要宏或XML。使用強(qiáng)大的ObjC運(yùn)行時(shí)儀器。2)沒(méi)有魔法字符串——支持IDE重構(gòu),完成和編譯時(shí)檢查。3)提供full-modularization和封裝的配置細(xì)節(jié)。讓你的架構(gòu)告訴一個(gè)故事。4)依賴(lài)關(guān)系中聲明的任何順序。(對(duì)人類(lèi)有意義的順序)。5)很容易有多個(gè)配置相同的基類(lèi)或協(xié)議。6)支持注射的視圖控制器和故事板集成。同時(shí)支持初始化器和屬性注入,以及生命周期管理。7)強(qiáng)大的內(nèi)存管理功能。提供預(yù)配置對(duì)象,沒(méi)有單件的內(nèi)存開(kāi)銷(xiāo)。8)優(yōu)秀的支持循環(huán)依賴(lài)。9)精益。占用很低,所以適合CPU和內(nèi)存受限的設(shè)備。10),功能強(qiáng)大,臺(tái)風(fēng)重總共只有3000行代碼。11)久經(jīng)沙場(chǎng),用于各種Appstore-featured應(yīng)用。
針對(duì)這兩個(gè)框架網(wǎng)上教程并不多,收集了一些比較有用的資料。最主要的用法還得看官方文檔分別在:Objection和Typhoon
objc.io官網(wǎng)的博文 Dependency Injection 和 Typhoon原創(chuàng)大神(Graham Lee)的文章Dependency Injection, iOS and You不看后悔一輩子^_^
Objection是一個(gè)輕量級(jí)的依賴(lài)注入框架,受Guice的啟發(fā),Google Wallet 也是使用的該項(xiàng)目?!敢蕾?lài)注入」是面向?qū)ο缶幊痰囊环N設(shè)計(jì)模式,用來(lái)減少代碼之間的耦合度。通常基于接口來(lái)實(shí)現(xiàn),也就是說(shuō)不需要new一個(gè)對(duì)象,而是通過(guò)相關(guān)的控制器來(lái)獲取對(duì)象。2013年最火的php框架laravel就是其中的典型。
假設(shè)有以下場(chǎng)景:ViewControllerA.view里有一個(gè)button,點(diǎn)擊之后push一個(gè)ViewControllerB,最簡(jiǎn)單的寫(xiě)法類(lèi)似這樣:
-? (void)viewDidLoad{? ? [superviewDidLoad];UIButton*button = [UIButtonbuttonWithType:UIButtonTypeSystem];? ? button.frame= CGRectMake(100,100,100,30);? ? [button setTitle:@"Button"forState:UIControlStateNormal];? ? [button addTarget:selfaction:@selector(buttonTapped) forControlEvents:UIControlEventTouchUpInside];? ? [self.viewaddSubview:button];}- (void)buttonTapped{? ? ViewControllerB *vc = [[ViewControllerB alloc] init];? ? [self.navigationControllerpushViewController:vc animated:YES];}
這樣寫(xiě)的一個(gè)問(wèn)題是,ViewControllerA需要import ViewControllerB,也就是對(duì)ViewControllerB產(chǎn)生了依賴(lài)。依賴(lài)的東西越多,維護(hù)起來(lái)就越麻煩,也容易出現(xiàn)循環(huán)依賴(lài)的問(wèn)題,而objection正好可以處理這些問(wèn)題。
實(shí)現(xiàn)方法是:先定義一個(gè)協(xié)議(protocol),然后通過(guò)objection來(lái)注冊(cè)這個(gè)協(xié)議對(duì)應(yīng)的class,需要的時(shí)候,可以獲取該協(xié)議對(duì)應(yīng)的object。對(duì)于使用方無(wú)需關(guān)心到底使用的是哪個(gè)Class,反正該有的方法、屬性都有了(在協(xié)議中指定)。這樣就去除了對(duì)某個(gè)特定Class的依賴(lài)。也就是通常所說(shuō)的「面向接口編程」。
JSObjectionInjector *injector = [JSObjection defaultInjector];// [1]UIViewController *vc = [injector getObject:@protocol(ViewControllerAProtocol)]; // [2]vc.backgroundColor= [UIColorlightGrayColor];// [3]UINavigationController*nc = [[UINavigationControlleralloc] initWithRootViewController:vc];self.window.rootViewController= nc;
[1] 獲取默認(rèn)的injector,這個(gè)injector已經(jīng)注冊(cè)過(guò)ViewControllerAProtocol了。
[2] 獲取ViewControllerAProtocol對(duì)應(yīng)的Object。
[3] 拿到VC后,設(shè)置它的某些屬性,比如這里的backgroundColor,因?yàn)樵赩iewControllerAProtocol里有定義這個(gè)屬性,所以不會(huì)有warning。
可以看到這里沒(méi)有引用ViewControllerA。再來(lái)看看這個(gè)ViewControllerAProtocol是如何注冊(cè)到injector中的,這里涉及到了Module,對(duì)Protocol的注冊(cè)都是在Module中完成的。Module只要繼承JSObjectionModule這個(gè)Class即可。
@interface ViewControllerAModule : JSObjectionModule@end@implementation ViewControllerAModule+ (void)load{? ? JSObjectionInjector *injector = [JSObjection defaultInjector];? ? injector = injector ? : [JSObjection createInjector];? ? injector = [injector withModule:[[ViewControllerAModule alloc] init]];? ? [JSObjection setDefaultInjector:injector]; }- (void)configure{? ? [self bindClass:[ViewControllerA class] toProtocol:@protocol(ViewControllerAProtocol)];}@end
綁定操作是在configure方法里進(jìn)行的,這個(gè)方法在被添加到injector里時(shí)會(huì)被自動(dòng)觸發(fā)。
JSObjectionInjector *injector = [JSObjection defaultInjector]; // [1]injector = injector ? : [JSObjection createInjector]; // [2]injector = [injector withModule:[[ViewControllerAModule alloc] init]]; // [3][JSObjection setDefaultInjector:injector]; // [4]
[1] 獲取默認(rèn)的injector
[2] 如果默認(rèn)的injector不存在,就新建一個(gè)
[3] 往這個(gè)injector里注冊(cè)我們的Module
[4] 設(shè)置該injector為默認(rèn)的injector
這段代碼可以直接放到+ (void)load里執(zhí)行,這樣就可以避免在AppDelegate里import各種Module。
因?yàn)槲覀儫o(wú)法直接獲得對(duì)應(yīng)的Class,所以必須要在協(xié)議里定義好對(duì)外暴露的方法和屬性,然后該Class也要實(shí)現(xiàn)該協(xié)議。
@protocolViewControllerAProtocol@property(nonatomic) NSUInteger currentIndex;@property(nonatomic)UIColor*backgroundColor;@end@interfaceViewControllerA:UIViewController@end
通過(guò)Objection實(shí)現(xiàn)依賴(lài)注入后,就能更好地實(shí)現(xiàn)SRP(Single Responsibility Principle),代碼更簡(jiǎn)潔,心情更舒暢,生活更美好。
總體來(lái)說(shuō),這個(gè)lib還是挺靠譜的,已經(jīng)維護(hù)了兩年多,也有一些項(xiàng)目在用,對(duì)于提高開(kāi)發(fā)成員的效率也會(huì)有不少的幫助,可以考慮嘗試下。