依賴注入(Dependency Injection)
依賴注入最大的特點就是:幫助我們開發出松散耦合(loose coupled)、可維護、可測試的代碼和程序。這條原則的做法是大家熟知的面向接口,或者說是面向抽象編程。 眾所周知該編程思想在各大語言中都有體現如jave、C++、PHP以及.net中。當然設計模式的廣泛程度遠遠大于這些,iOS當然也不例外。 本文主要介紹本人在學習Dependency Injection的時候的學習過程以及對一些學習資料的總結,主要介紹ios中的兩大框架Objection和Typhoon。 在Android上比較流行的有RoboGuice和Dagger等.
什么是依賴注入(Dependency Injection)?
依賴注入(Dependency Injection)是一個將行為從依賴中分離的技術,簡單地說,它允許開發者定義一個方法函數依賴于外部其他各種交互,而不需要編碼如何獲得這些外部交互的實例。 這樣就在各種組件之間解耦,從而獲得干凈的代碼,相比依賴的硬編碼, 一個組件只有在運行時才調用其所需要的其他組件,因此在代碼運行時,通過特定的框架或容器,將其所需要的其他依賴組件進行注入,主動推入。
依賴注入是最早spring和Piconcontainer等提出,如今已經是一個缺省主流模式,并擴展到前端如Angular.js等等。
如果在Class A中,有Class B的實例,則稱Class A對Class B有一個依賴。例如下面類ViewControllerA中用到一個ViewControllerB對象,我們就說類ViewControllerA對類ViewControllerB有一個依賴。
#import"ViewControllerB.h"@implementationViewControllerA- (void)buttonTapped{? ? ViewControllerB *vc = [[ViewControllerB alloc] init];? ? [self.navigationControllerpushViewController:vc animated:YES];}
仔細看這段代碼我們會發現存在一些問題:
(1). 如果現在要改變ViewControllerB生成方式,如需要用initWithOrderid:(NSString * orderid)初始化vc,需要修改ViewControllerA代碼;
(2). 如果想測試不同ViewControllerB對象對ViewControllerA的影響很困難,因為ViewControllerB 的初始化被寫死在了ViewControllerA` 的構造函數中;
(3). 如果[[ViewControllerB alloc] init]過程非常緩慢,單測時我們希望用已經初始化好的ViewControllerB對象Mock掉這個過程也很困難。
上面將依賴在構造函數中直接初始化是一種Hard init方式,弊端在于兩個類不夠獨立,不方便測試。我們還有另外一種Init方式,如下:
@interfaceViewControllerA()@property(nonatomic,readonly) ViewControllerB *vcB;@end@implementationViewControllerA// vcB是在ViewControllerA被創建之前被創建的并且作為參數傳進來,// 調用者如果想,還可以自定義。- (instancetype)initWithEngine:(ViewControllerB *)vcB? {? ? ...? ? _vcB = vcB;returnself;? }@end
上面代碼中,我們將vcB對象作為構造函數的一個參數傳入。在調用ViewControllerA的構造方法之前外部就已經初始化好了vcB對象。像這種非自己主動初始化依賴,而通過外部來傳入依賴的方式,我們就稱為依賴注入。
現在我們發現上面1中存在的兩個問題都很好解決了,簡單的說依賴注入主要有兩個好處:
解耦,將依賴之間解耦。
因為已經解耦,所以方便做單元測試,尤其是Mock測試。
那么問題來了,如何學習Dependency Injection呢 ?iOS有關DI依賴注入的框架比較好用的有兩個:Objection 和 Typhoon.下面就從幾個方便來介紹下這兩個框架
一:Objection 和 Typhoon這兩個框架有什么區別呢 其實這兩個框架各有優勢:
Objection框架,使用起來比較靈活,用法比較簡單。示例代碼如下:
屬性注冊:
@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")//屬性的依賴注入@synthesizeengine, brakes, awake;@end
方法注入:
@implementation Truckobjection_requires(@"engine", @"brakes")objection_initializer(truckWithMake:model:)//方法的依賴注入+ (instancetype)truckWithMake:(NSString *) make model: (NSString *)model {...}@end
2.對比來說Typhoon的使用起來就比較規范,首先需要創建一個TyphoonAssembly的子類。其需要注入的方法和屬性都需要寫在這個統一個子類中,當然可以實現不同的子類來完成不同的功能:
@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.當然還有一些硬性的區別就是Typhoon現在已經支持Swift。
4.兩者維護時間都超過2年以上。
Tythoon官方介紹的優勢:
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.
大體翻譯過來:
1)非侵入性。不需要宏或XML。使用強大的ObjC運行時儀器。2)沒有魔法字符串——支持IDE重構,完成和編譯時檢查。3)提供full-modularization和封裝的配置細節。讓你的架構告訴一個故事。4)依賴關系中聲明的任何順序。(對人類有意義的順序)。5)很容易有多個配置相同的基類或協議。6)支持注射的視圖控制器和故事板集成。同時支持初始化器和屬性注入,以及生命周期管理。7)強大的內存管理功能。提供預配置對象,沒有單件的內存開銷。8)優秀的支持循環依賴。9)精益。占用很低,所以適合CPU和內存受限的設備。10),功能強大,臺風重總共只有3000行代碼。11)久經沙場,用于各種Appstore-featured應用。
針對這兩個框架網上教程并不多,收集了一些比較有用的資料。最主要的用法還得看官方文檔分別在:Objection和Typhoon
objc.io官網的博文 Dependency Injection 和 Typhoon原創大神(Graham Lee)的文章Dependency Injection, iOS and You不看后悔一輩子^_^
Objection是一個輕量級的依賴注入框架,受Guice的啟發,Google Wallet 也是使用的該項目。「依賴注入」是面向對象編程的一種設計模式,用來減少代碼之間的耦合度。通常基于接口來實現,也就是說不需要new一個對象,而是通過相關的控制器來獲取對象。2013年最火的php框架laravel就是其中的典型。
假設有以下場景:ViewControllerA.view里有一個button,點擊之后push一個ViewControllerB,最簡單的寫法類似這樣:
-? (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];}
這樣寫的一個問題是,ViewControllerA需要import ViewControllerB,也就是對ViewControllerB產生了依賴。依賴的東西越多,維護起來就越麻煩,也容易出現循環依賴的問題,而objection正好可以處理這些問題。
實現方法是:先定義一個協議(protocol),然后通過objection來注冊這個協議對應的class,需要的時候,可以獲取該協議對應的object。對于使用方無需關心到底使用的是哪個Class,反正該有的方法、屬性都有了(在協議中指定)。這樣就去除了對某個特定Class的依賴。也就是通常所說的「面向接口編程」。
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] 獲取默認的injector,這個injector已經注冊過ViewControllerAProtocol了。
[2] 獲取ViewControllerAProtocol對應的Object。
[3] 拿到VC后,設置它的某些屬性,比如這里的backgroundColor,因為在ViewControllerAProtocol里有定義這個屬性,所以不會有warning。
可以看到這里沒有引用ViewControllerA。再來看看這個ViewControllerAProtocol是如何注冊到injector中的,這里涉及到了Module,對Protocol的注冊都是在Module中完成的。Module只要繼承JSObjectionModule這個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方法里進行的,這個方法在被添加到injector里時會被自動觸發。
JSObjectionInjector *injector = [JSObjection defaultInjector]; // [1]injector = injector ? : [JSObjection createInjector]; // [2]injector = [injector withModule:[[ViewControllerAModule alloc] init]]; // [3][JSObjection setDefaultInjector:injector]; // [4]
[1] 獲取默認的injector
[2] 如果默認的injector不存在,就新建一個
[3] 往這個injector里注冊我們的Module
[4] 設置該injector為默認的injector
這段代碼可以直接放到+ (void)load里執行,這樣就可以避免在AppDelegate里import各種Module。
因為我們無法直接獲得對應的Class,所以必須要在協議里定義好對外暴露的方法和屬性,然后該Class也要實現該協議。
@protocolViewControllerAProtocol@property(nonatomic) NSUInteger currentIndex;@property(nonatomic)UIColor*backgroundColor;@end@interfaceViewControllerA:UIViewController@end
通過Objection實現依賴注入后,就能更好地實現SRP(Single Responsibility Principle),代碼更簡潔,心情更舒暢,生活更美好。
總體來說,這個lib還是挺靠譜的,已經維護了兩年多,也有一些項目在用,對于提高開發成員的效率也會有不少的幫助,可以考慮嘗試下。