第一篇文章對VIPER進行了簡單的介紹,這篇文章將從VIPER的源頭開始,比較現有的幾種VIPER實現,對VIPER進行進一步的職責剖析,並對各類細節實現問題進行挖掘和探討。最後給出兩個完整的VIPER實現,而且提供快速生成VIPER代碼的模板。html
Demo和輪子的github地址是:ZIKViper,路由工具:ZIKRouter。有用請點個star~ 注意,Demo須要先用pod install安裝一下依賴庫。ios
兩個實現展現瞭如下問題的解決方案:git
VIPER架構,最初是2013年在MutualMobile的技術博客上,由Jeff Gilbert 和 Conrad Stoll 提出的。他們的博客網站有過一次遷移,原文地址已經失效,這是遷移後的博文:MEET VIPER: MUTUAL MOBILE’S APPLICATION OF CLEAN ARCHITECTURE FOR IOS APPS。github
這是文章中提出的架構示意圖:數據庫
Wireframe能夠看做是Router的另外一種表達。能夠看到,VIPER之間的關係已經很明確了。以後,做者在2014年在objc.io上發表了另外一篇更詳細的介紹文章:Architecting iOS Apps with VIPER。swift
在做者的第一篇文章裏,闡述了VIPER是在接觸到了Uncle Bob的Clean Architecture後,對Clean Architecture的一次實踐。所以,VIPER真正的源頭應該是Clean Architecture。後端
由Uncle Bob在2011年提出的Clean Architecture
,是一個平臺無關的抽象架構。想要詳細學習的,能夠閱讀做者的原文:Clean Architecture,翻譯:乾淨的架構The Clean Architecture。bash
它經過梳理軟件中不一樣層之間的依賴關係,提出了一個自外向內,單向依賴的架構,以下圖所示:網絡
越靠近內層,越變得抽象,越接近設計的核心。越靠近外層,越和具體的平臺和實現技術相關。內層的部分徹底不知道外層的存在和實現方式,代碼只能從外層向內層引用,目的是爲了實現層與層之間的隔離。將不一樣抽象程度的層進行隔離,作到了把業務規則和具體實現分離開。你能夠把外層看做是內層的delegate,外層只能經過內層提供的delegate接口來使用內層。數據結構
表明了這個軟件項目的業務規則。由數據實體體現,是一些能夠在不一樣的程序應用之間共享的數據結構。
表明了本應用所使用的一些業務規則。封裝和實現了用到的業務功能,會將各類實體的數據結構轉爲在用例中傳遞的實體類,可是和具體的數據庫技術或者UI無關。
接口適配層。將用例的規則和具體的實現技術進行抽象地對接,將用例中用到的實體類轉爲供數據庫存儲的格式或者供View展現的格式。相似於MVVM中把Model的數據傳遞給ViewModel供View顯示。
右下角表示了接口適配層中不一樣模塊間的通訊方式。不一樣的模塊在業務用例中產生關聯和數據傳遞。Input、Output就是Use Case提供給外層的數據流動接口。
庫和驅動層,表明了選用的各類具體的實現技術,例如持久層使用SQLite仍是Core Data,網絡層使用NSURLSession、NSURLConnection仍是AFNetworking等。
能夠看到,Clean Architecture裏已經出現了Use Case、Interactor、Presenter等概念,它爲VIPER的工程實現提供了設計思想,VIPER將它的設計轉化成了具體的實現。VIPER裏的各部分正是存在着由外向內的依賴,從外向內表現爲:View -> Presenter -> Interactor -> Entity
,Wireframe
嚴格來講也是一類特殊的Use Case,用於不一樣模塊之間通訊,鏈接了不一樣的Presenter
。
必需要記住的是,VIPER架構是根據由外向內的依賴關係來設計的。這句話是指導咱們進行進一步設計和優化的關鍵。
MutualMobile的那兩篇文章雖然已經明確了VIPER各部分之間的職責,而且給出了簡單的Demo,可是對Wireframe部分的實現有些爭議,解耦作得不夠完全,而且對各層之間如何交互還處在最簡單的實現上。以後出現了挺多文章來將VIPER進一步細化,不過某些細節的實現上有些差異,在給出我本身的VIPER以前,我將先對這些實現進行一次綜合的比較分析,看看他們都使用了哪些技術,遇到了哪些爭議點。不一樣實現之間已經公認的地方我就再也不單獨列出了。
原文地址:Brigade’s Experience Using an MVC Alternative: VIPER architecture for iOS applications。
文章把VIPER的優勢總結了一下,提出了這樣的架構圖:
他們對VIPER的各部分都沒有異議,只是對Interactor的實現進行了進一步細化。用一個Data Manager提供給各個Use Case管理Entity,好比獲取、存儲功能。在Service中調用網絡層去獲取服務端的數據。
文章中還認爲應該由Wireframe負責初始化整個VIPER,生成各部分的類,並設置依賴關係,而且引用另外一個模塊的Wireframe,負責跳轉到另外一個界面。
和這個實現相似的還有:
針對VIPER須要編寫太多初始化代碼的麻煩,可使用Xcode自帶的Template解決。而不少做者都提到了一個代碼生成工具:Generamba。
文章並無對VIPER進行修改,只是進一步細化了。這應該是一個最簡單的實現。若是你要實施VIPER,參照這篇文章來沒有什麼大問題。可是它沒有探討的問題是:
一個對VIPER十分感興趣的俄國團隊,編寫了一本關於VIPER的書:The-Book-of-VIPER。而且給出了一個目前網絡上實現完成度最高的開源Demo:rambler-it-ios,以及他們用於實施VIPER的庫:ViperMcFlurry。
他們整理的VIPER架構圖以下:
和其餘實現不一樣的是,他們把VIPER的初始化和裝配工做單獨放到了一個Assembly裏,Router只作界面跳轉的工做。而且把VIPER內不一樣部分之間的通訊統一用Input和Output來表示。Input表示外部主動調用模塊提供的接口,Output表示模塊經過外部實現所要求的接口,將事件傳遞到外部。
之因此將模塊初始化單獨放到Assembly裏,是由於Router若是負責初始化本模塊,會違背單一職責原則。
這個實現的願景很好,只是在轉變爲具體實現的時候不夠完美,有不少問題尚待解決。具體能夠參見Demo。
我對Typhoon這個依賴注入工具不是特別感冒,它使用了十分複雜的run time技術,想要追蹤一個對象的注入過程時,會看得暈頭轉向。並且它沒法實現運行時由調用方動態注入,只能實現預約義好的靜態注入。也就是不能動態傳參。
在Demo中實現了在執行segue時用block來使用-prepareForSegue:sender:
,實現向目的界面傳參,實現了動態注入。可是這樣就把路由限定在了storyboard的segue技術上,那麼對於那些沒有使用storyboard的項目應該怎麼辦呢?Demo並無給出答案。並且-prepareForSegue:sender:
只能向View傳參,可是有一些參數是View不該該接觸到的,而是應該直接傳給Presenter或者Interactor的。
Output
並不能完整描述它的職責,還能夠再進一步劃分也就是說,他們的方案在設計上是不錯的,但在技術上還有不少改進空間。
Uber因爲業務愈來愈複雜,舊項目的架構已經沒法知足當前的需求,所以在2016年徹底重構了他們的 rider app。他們借鑑VIPER,而且設計出了一個VIPER的變種架構:Riblets。文章地址:ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP。
架構圖以下:
數據流向圖:
父模塊和子模塊之間通訊:
這裏只列出一些和VIPER有差別的地方:
最大的改變是將Router從Presenter移到了Interactor,改變了模塊的主從關係,整個模塊的生命週期如今由Interactor來管理。而以前的VIPER模塊是依賴於View的生命週期的。這樣一來,整個架構就從View驅動變成了業務驅動,或者數據驅動。
關於這個改變,Uber給出了兩個緣由:
Uber團隊的確頗有想法。在對他們的這個方案進行深刻實踐以前,我沒法評論這個方案是好是壞,我只在這裏提出一些實踐中可能會遇到的問題。
關於Uber給出的第一個緣由,這是Uber團隊基於協調兩個開發團隊的狀況而作出的選擇,若是咱們沒有他們這樣統一開發的需求,並無必要借鑑。iOS的UIKit是一個視圖驅動的框架,很難作到100%數據驅動,在實踐中將會遇到許多須要解決的問題,除非有足夠的開發時間,不然不要草率地投入其中。是否要使用數據驅動的設計,仍是應該由項目的業務設計來決定。當數據變化大部分是由後端的Service和網絡數據引發時,再去考慮數據驅動吧。例如Uber的地圖路線由定位模塊不斷計算,自動更新,就比較適合使用數據驅動。
關於第二個緣由,一個沒有View和Presenter的VIPER,就只剩下Router、Interactor、Model,這時這個模塊能夠看作是一個能夠經過Router調用的Service或者Manager,這個Service有本身的狀態和生命週期,Service也能夠在View銷燬後繼續完成剩餘的業務工做,只要業務須要,能夠進行自持有,自釋放。並且這個Service最終仍是會表如今某個View上。這麼看來,Router的層級已經升高了,成爲了整個app內的模塊間通訊工具,能夠鏈接任意模塊,不只僅是VIPER,所以Router由誰持有,就徹底由模塊內部自由管理了。
只是,在iOS中的VIPER裏,實際的路由API都是存在於UIViewController上的,Router會直接和View產生引用,把Router放到和View隔離的Interactor裏會破壞隔離。並且從Clean架構的分層來看,層級升高後的Router應該是處在Interface Adapter層和Framework & Driver層之間,而Interactor則是在Application Business Rules層,由Interactor來管理其餘角色,會破壞了Clean Architecture裏的依賴關係。
好比一個沒有View的、用於管理語音通話數據的Interactor,收到了通話異常中斷的事件,在處理事件時,它不該該經過Router將本身移除,或者結束整個語音通話業務,或者自動調用從新撥號的業務,這樣很容易會讓不一樣的Use Case之間產生耦合,這些都應該由更上層的Service去選擇執行,若是有頁面跳轉的設計,則應該把事件轉發給一個存在Presenter層的Parent VIPER模塊,由parent來決定是退出通話界面仍是彈窗提示。當一個Interactor沒有Presenter和View時,它必定是另外一個VIPER的子模塊。這麼看來,在沒有View時,或許讓Service來持有Router纔是正確的。
所以,若是真的有把VIPER變成數據驅動的需求,主要仍是源於Uber給出的第一個基於團隊統一的理由。
文章裏還給出了一些頗有參考價值的內容,好比:
Uber的這個方案講了不少其餘方案沒有提到的方面,好比依賴注入、如何引入子模塊等問題。不過這個方案並無開源。
首先總結出一個絕對標準的VIPER,各部分遵循隔離關係,同時考慮到依賴注入、子模塊通訊、模塊間解耦等問題,將VIPER的各部分的職責變得更加明確,也新增了幾個角色。示例圖以下,各角色的顏色和Clean Architecture圖中各層的顏色對應:
示例代碼將用一個筆記應用做爲演示。
View能夠是一個UIView + UIViewController,也能夠只是一個custom UIView,也能夠是一個自定義的用於管理UIView的Manager,只要它實現了View的接口就能夠。
View層的職責:
View層會引入各類自定義控件,這些控件有許多delegate,都在View層實現,統一包裝後,再交給Presenter層實現。由於Presenter層並不知道View的實現細節,所以也就不知道這些控件的接口,Presenter層只知道View層統一暴露出來的接口。並且這些控件的接口在定義時可能會將數據獲取、事件回調、控件渲染接口混雜起來,最具表明性的就是UITableViewDataSource
裏的-tableView:cellForRowAtIndexPath:
。這個接口同時涉及到了UITableViewCell
和渲染cell所須要的Model,是很是容易產生耦合的地方,所以須要作一次分解。應該在View的dataSource裏定義一個從外部獲取所須要的簡單類型數據的方法,在-tableView:cellForRowAtIndexPath:
裏用獲取到的數據渲染cell。示例代碼:
@protocol ZIKNoteListViewEventHandler <NSObject>
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath;
@end
複製代碼
@protocol ZIKNoteListViewDataSource <NSObject>
- (NSInteger)numberOfRowsInSection:(NSInteger)section;
- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end
複製代碼
@interface ZIKNoteListViewController () <UITableViewDelegate,UITableViewDataSource>
@property (nonatomic, strong) id<ZIKNoteListViewEventHandler> eventHandler;
@property (nonatomic, strong) id<ZIKNoteListViewDataSource> viewDataSource;
@property (weak, nonatomic) IBOutlet UITableView *noteListTableView;
@end
@implementation ZIKNoteListViewController
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
text:(NSString *)text
detailText:(NSString *)detailText {
UITableViewCell *cell = [self.noteListTableView dequeueReusableCellWithIdentifier:@"noteListCell" forIndexPath:indexPath];
cell.textLabel.text = text;
cell.detailTextLabel.text = detailText;
return cell;
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.viewDataSource numberOfRowsInSection:section];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *text = [self.viewDataSource textOfCellForRowAtIndexPath:indexPath];
NSString *detailText = [self.viewDataSource detailTextOfCellForRowAtIndexPath:indexPath];
UITableViewCell *cell = [self cellForRowAtIndexPath:indexPath
text:text
detailText:detailText];
return cell;
}
#pragma mark UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[self.eventHandler handleDidSelectRowAtIndexPath:indexPath];
}
@end
複製代碼
通常來講,viewDataSource和eventHandler都是由Presenter來擔任的,Presenter接收到dataSource請求時,從Interactor裏獲取並返回對應的數據。你也能夠選擇在View和Presenter之間用ViewModel來進行交互。
Presenter由View持有,它的職責有:
Presenter是View和業務之間的中轉站,它不包含業務實現代碼,而是負責調用現成的各類Use Case,將具體事件轉化爲具體業務。Presenter裏不該該導入UIKit,不然就有可能入侵View層的渲染工做。Presenter裏也不該該出現Model類,當數據從Interactor傳遞到Presenter裏時,應該轉變爲簡單的數據結構。
示例代碼:
@interface ZIKNoteListViewPresenter () <ZIKNoteListViewDataSource, ZIKNoteListViewEventHandler>
@property (nonatomic, strong) id<ZIKNoteListWireframeProtocol> wireframe;
@property (nonatomic, weak) id<ZIKViperView,ZIKNoteListViewProtocol> view;
@property (nonatomic, strong) id<ZIKNoteListInteractorInput> interactor;
@end
@implementation ZIKNoteListViewPresenter
#pragma mark ZIKNoteListViewDataSource
- (NSInteger)numberOfRowsInSection:(NSInteger)section {
return self.interactor.noteCount;
}
- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *title = [self.interactor titleForNoteAtIndex:indexPath.row];
return title;
}
- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *content = [self.interactor contentForNoteAtIndex:indexPath.row];
return content;
}
#pragma mark ZIKNoteListViewEventHandler
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *uuid = [self.interactor noteUUIDAtIndex:indexPath.row];
NSString *title = [self.interactor noteTitleAtIndex:indexPath.row];
NSString *content = [self.interactor noteContentAtIndex:indexPath.row];
[self.wireframe pushEditorViewForEditingNoteWithUUID:uuid title:title content:content delegate:self];
}
@end
複製代碼
Ineractor的職責:
Interactor是業務的實現者和維護者,它會調用各類Service來實現業務邏輯,封裝成明確的用例。而這些Service在使用時,也都是基於接口的,由於Interactor的實現不和具體的類綁定,而是由Application注入Interactor須要的Service。
示例代碼:
@protocol ZIKNoteListInteractorInput <NSObject>
- (void)loadAllNotes;
- (NSInteger)noteCount;
- (NSString *)titleForNoteAtIndex:(NSUInteger)idx;
- (NSString *)contentForNoteAtIndex:(NSUInteger)idx;
- (NSString *)noteUUIDAtIndex:(NSUInteger)idx;
- (NSString *)noteTitleAtIndex:(NSUInteger)idx;
- (NSString *)noteContentAtIndex:(NSUInteger)idx;
@end
複製代碼
@interface ZIKNoteListInteractor : NSObject <ZIKNoteListInteractorInput>
@property (nonatomic, weak) id dataSource;
@property (nonatomic, weak) id eventHandler;
@end
@implementation ZIKNoteListInteractor
- (void)loadAllNotes {
[[ZIKNoteDataManager sharedInsatnce] fetchAllNotesWithCompletion:^(NSArray *notes) {
[self.eventHandler didFinishLoadAllNotes];
}];
}
- (NSArray<ZIKNoteModel *> *)noteList {
return [ZIKNoteDataManager sharedInsatnce].noteList;
}
- (NSInteger)noteCount {
return self.noteList.count;
}
- (NSString *)titleForNoteAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] title];
}
- (NSString *)contentForNoteAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] content];
}
- (NSString *)noteUUIDAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] uuid];
}
- (NSString *)noteTitleAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] title];
}
- (NSString *)noteContentAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] content];
}
@end
複製代碼
向Interactor提供各類封裝好的服務,例如數據庫的訪問、存儲,調用定位功能等。Service由Application在執行路由時注入到Builder裏,再由Buidler注入到Interactor裏。也能夠只注入一個Service Router,在運行時再經過這個Service Router懶加載須要的Service,至關於注入了一個提供Router功能的Service。
Service能夠看做是沒有View的VIPER,也有本身的路由和Builder。
翻譯成中文叫線框,用於表達從一個Module到另外一個Module的過程。雖然也是扮演者執行路由的角色,可是其實它和Router是有區別的。
Wireframe和storyboard中鏈接好的一個個segue相似,負責提供一系列具體的路由用例,這個用例裏已經配置好了源界面和目的界面的一些依賴,包括轉場動畫、模塊間傳參等。Wireframe的接口是提供給模塊內部使用的,它經過調用Router來執行真正的路由操做。
示例代碼:
@interface ZIKTNoteListWireframe : NSObject <ZIKTViperWireframe>
- (void)presentLoginViewWithMessage:(NSString *)message delegate:(id<ZIKTLoginViewDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)dismissLoginView:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
- (void)presentEditorForCreatingNewNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)pushEditorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (UIViewController *)editorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (void)pushEditorViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
- (void)quitEditorViewWithAnimated:(BOOL)animated;
@end
複製代碼
Router則是由Application提供的具體路由技術,能夠簡單封裝UIKit裏的那些跳轉方法,也能夠用URL Router來執行路由。可是一個模塊是不須要知道app使用的是什麼具體技術的。Router纔是真正鏈接各個模塊的地方。它也負責尋找對應的目的模塊,而且經過Buidler進行依賴注入。
示例代碼:
@interface ZIKTRouter : NSObject <ZIKTViperRouter>
///封裝UIKit的跳轉方法
+ (void)pushViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
+ (void)popViewController:(UIViewController *)viewController animated:(BOOL)animated;
+ (void)presentViewController:(UIViewController *)viewControllerToPresent fromViewController:(UIViewController *)source animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
+ (void)dismissViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
@end
@implementation ZIKTRouter (ZIKTEditor)
+ (UIViewController *)viewForCreatingNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate {
return [ZIKTEditorBuilder viewForCreatingNoteWithDelegate:delegate];
}
+ (UIViewController *)viewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate {
return [ZIKTEditorBuilder viewForEditingNoteWithUUID:uuid title:title content:content delegate:delegate];
}
@end
複製代碼
由Application實現,負責在模塊通訊時進行一些接口的轉換,例如兩個模塊使用了相同業務功能的某個Service,使用的protocol實現同樣,可是protocol名字不同,就能夠在路由時,在Adapter裏進行一次轉換。甚至只要定義的邏輯同樣,依賴參數的名字和數據類型也能夠容許不一樣。這樣就能讓模塊不依賴於某個具體的protocol,而是依賴於protocol實際定義的依賴和接口。
注意這裏的Adapter和Clean Architecture裏的Interface Adapter
是不同的。這裏的Adapter就是字面意義上的接口轉換,而Clean Architecture裏的Interface Adapter
層更加抽象,是Use Case層與具體實現技術之間的轉換,囊括了更多的角色。
負責初始化整個模塊,配置VIPER之間的關係,並對外聲明模塊須要的依賴,讓外部執行注入。
一個VIPER模塊能夠看作是一個獨立的組件,能夠被單獨封裝成一個庫,被app引用。這時候,app就負責將各個模塊鏈接起來,也就是圖中灰色的Application Context
部分。一個模塊,確定是存在於一個上下文環境中才能運行起來的。
Wireframe
-> Router
-> Adapter
-> Builder
實現了一個完整的模塊間路由,而且實現了模塊間的解耦。
其中Wireframe和Builder是分別由引用者模塊和被引用模塊提供的,是兩個模塊的出口和入口,而Router和Adapter則是由模塊的使用者——Application實現的。
當兩個模塊之間存在引用關係時,說明存在業務邏輯上的耦合,這種耦合是業務的一部分,是不可能消除的。咱們能作的就是把耦合儘可能交給模塊調用者,由Application來提供具體的類,注入到各個模塊之中,而模塊內部只面向protocol便可。這樣的話,被引用模塊只要實現了相同的接口,就能夠隨時替換,甚至接口有一些差別時,只要被引用模塊提供了相同功能的接口,也能夠經過Adapter來作接口兼容轉換,讓引用者模塊無需作任何修改。
Wireframe至關於插頭,Builder至關於插座,而Router和Adapter至關於電路和轉接頭,將不一樣規格的插座和插頭鏈接起來。把這些鏈接和適配的工做交給Application層,就能讓兩個模塊實現各自獨立。
大部分方案都沒有討論子模塊存在的狀況。在VIPER裏如何引入另外一個VIPER模塊?多個模塊之間如何交互?子模塊由誰初始化、由誰管理?
其餘幾個實現中,只有Uber較爲詳細地討論了子模塊的問題。在Uber的Riblets架構裏,子模塊的Router被添加到父模塊的Router,模塊之間經過delegate和監聽的方式進行通訊。這樣作會讓模塊間產生必定的耦合。若是子模塊是因爲父View使用了一個子View控件而被引入的,那麼父Interactor就會在代碼裏多出一個子Interactor,這樣就致使了View的實現方式影響了Interactor的實現。
子模塊的來源有:
子View多是一個UIView,也多是一個Child UIViewController。所以子View有可能須要向外部請求數據,也可能獨立完成全部任務,不須要依賴父模塊。
若是子View能夠獨立,那在子模塊裏不會出現和父模塊交互的邏輯,只有把一些事件經過Output傳遞出去的接口。這時只須要把子View的接口封裝在父View的接口裏便可,父Presenter和父Interactor是不知道父View提供的這幾個接口是經過子View實現的。這樣父模塊就能接收到子模塊的事件了,並且可以保持Interactor和Presenter、View之間從低到高的依賴關係。
若是父模塊須要調用子模塊的某些功能,或者從子模塊獲取數據,能夠選擇封裝到父View的接口裏,不過若是涉及到數據模型,而且不想讓數據模型出如今View的接口中,能夠把子Interactor做爲父Interactor的一個Service,在引入子模塊時,經過父Builder注入到父Interactor裏,或者根據依賴關係解耦地再完全一點,注入到父Presenter裏,讓父Presenter再把接口轉發給父Interactor。這樣子模塊和父模塊就能經過Service的形式進行通訊了,而這時,父Interactor也不知道這個Service是來自子模塊裏的。
在這樣的設計下,子模塊和父模塊是不知道彼此的存在的,只是經過接口進行交互。好處是父View若是想要更換爲另外一個相同功能的子View控件,就只須要在父View裏修改,不會影響Presenter和Interactor。
這個VIPER的設計是經過接口將各個部分組合在一塊兒的,一個類須要設置不少依賴,例如Interactor須要依賴許多Service。這就涉及到了兩個問題:
在這個方案中,由Builder聲明整個模塊的依賴,而後在Builder內部爲不一樣的類設置依賴,外部在注入依賴時,就沒必要知道內部是怎麼使用這些依賴參數的。一個類若是有必需的依賴參數,能夠直接在init方法裏體現,對於那些非必需的依賴,能夠經過暴露接口來聲明。
若是須要動態注入,而不是在模塊初始化時就配置全部的依賴,Builder也能夠提供動態注入的接口。
若是你須要把一個模塊從MVC重構到VIPER,能夠先按照這個步驟:
下面就是第一步裏在Controller中能夠分隔出的職責:
@implementation ViewController
//------View-------
//View的生命週期
#pragma mark View life
//View的配置,包括佈局設置
#pragma mark View config
//更新View的接口
#pragma mark Update view
//View須要從model中獲取的數據
#pragma mark Request view data source
//監控、接收View的事件
#pragma mark Send view event
//------Presenter-------
//處理View的事件
#pragma mark Handle view event
//界面跳轉
#pragma mark Wireframe
//向View提供配置用的數據
#pragma mark Provide view data source
//提供生成model須要的數據
#pragma mark Provide model data source
//處理業務事件,調用業務用例
#pragma mark Handle business event
//------Interactor-------
//監控、接收業務事件
#pragma mark Send business event
//業務用例
#pragma mark Business use case
//獲取生成model須要的數據
#pragma mark Request data for model
//維護model
#pragma mark Manage model
@end
複製代碼
這裏缺乏了View狀態管理、業務狀態管理等職責,由於這些狀態通常都是@property,用pragma mark不能分隔它們,只能在@interface裏聲明的時候進行隔離。
上面的方案是以最完全的解耦爲目標設計的,在實踐中,若是真的徹底按照這個設計,代碼量的確不小。其實一些地方的耦合並不會引發多大問題,除非你的模塊須要封裝成通用組件供多個app使用,不然並不須要按照100%的解耦要求來編寫。所以接下來我再總結一個稍微簡化的方案,總結一下各部分能夠在哪些地方出現耦合,哪些耦合不能出現。
在這個方案裏,我使用了一箇中介者來減小一部分代碼,Router就是一個很適合成爲中介者的角色。
架構圖以下:
UITableViewDataSource
),不用再封裝一遍後交給Presenter改變得最多的就是路由部分。View、Presenter和Interactor均可以使用路由來獲取一些模塊。View能夠經過路由獲取子View,Presenter能夠經過路由獲取其餘View模塊,Interactor能夠經過路由獲取Service。
在實現時,能夠把Wireframe、Router、Builder整合到一塊兒,全都放到Router裏,Router由模塊實現並提供給外部使用。相似於Brigade團隊和Rambler&Co團隊的實現。可是他們的實現都是直接在Router裏引入其餘模塊的Router,這樣會致使依賴混亂,更好的方式是經過一箇中間人統一提供其餘模塊的接口。
我在這裏造了個輪子,經過protocol來尋找須要的模塊並執行路由,不用直接導入目的模塊中的類,而且提供了Adapter的支持,可讓多個protocol指向同一個模塊。這樣就能避免模塊間的直接依賴。
示例代碼:
///editor模塊的依賴聲明
@protocol NoteEditorProtocol <NSObject>
@property (nonatomic, weak) id<ZIKEditorDelegate> delegate;
- (void)constructForCreatingNewNote;
- (void)constructForEditingNote:(ZIKNoteModel *)note;
@end
@implementation ZIKNoteListViewPresenter
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSAssert([[self.view routeSource] isKindOfClass:[UIViewController class]], nil);
//跳轉到編輯器界面;經過protocol獲取對應的router類,再經過protocol注入依賴
//App能夠用Adapter把NoteEditorProtocol和真正的protocol進行匹配和轉接
[ZIKViewRouterToModule(NoteEditorProtocol)
performFromSource:[self.view routeSource] //跳轉的源界面
configuring:^(ZIKViewRouteConfiguration<NoteEditorProtocol> *config) {
//路由配置
//設置跳轉方式,支持全部界面跳轉類型
config.routeType = ZIKViewRouteTypePush;
//Router內部負責用獲取到的參數初始化editor模塊
config.delegate = self;
[config constructForEditingNote:[self.interactor noteAtIndex:indexPath.row]];
config.prepareForRoute = ^(id destination) {
//跳轉前配置目的界面
};
config.routeCompletion = ^(id destination) {
//跳轉結束處理
};
config.performerErrorHandler = ^(SEL routeAction, NSError * error) {
//跳轉失敗處理
};
}];
}
@end
複製代碼
這個方案依賴於一個統一的中間人,也就是路由工具,在個人實現裏就是ZIKRouter。View、Presenter、Interactor均可以使用對應功能的Router獲取子模塊。而因爲ZIKRouter仍然是經過protocol的方式來和子模塊進行交互,所以仍然可保持模塊間解耦。惟一的耦合就是各部分都引用了ZIKRouter這個工具。若是你想把模塊和ZIKRouter的耦合也去除,可讓Router也變成面向接口,由外部注入。
針對兩個方案,同時寫了兩個相同功能的Demo,能夠比較一下代碼上的區別。地址在:ZIKViper。注意,Demo須要先用pod install安裝一下依賴庫。
項目裏也提供了Xcode File Template用於快速生成VIPER代碼模板。把.xctemplate
後綴的文件夾拷貝到~/Library/Developer/Xcode/Templates/
目錄下,就能夠在Xcode的New->File->Template
裏選擇代碼模板快速生成代碼。