iOS VIPER架構實踐(二):VIPER詳解與實現

第一篇文章對VIPER進行了簡單的介紹,這篇文章將從VIPER的源頭開始,比較現有的幾種VIPER實現,對VIPER進行進一步的職責剖析,並對各類細節實現問題進行挖掘和探討。最後給出兩個完整的VIPER實現,而且提供快速生成VIPER代碼的模板。html

Demo和輪子的github地址是:ZIKViper,路由工具:ZIKRouter。有用請點個star~ 注意,Demo須要先用pod install安裝一下依賴庫。ios

兩個實現展現瞭如下問題的解決方案:git

  • 如何完全地解決不一樣模塊之間的耦合
  • 如何在一個模塊裏引入子模塊
  • 子模塊和父模塊之間如何通訊
  • 如何對模塊進行依賴注入
  • 面向接口的路由工具

目錄

  • 起源
  • Clean Architecture
    • Enterprise Business Rules
    • Application Business Rules
    • Interface Adapters
    • Frameworks & Drivers
    • 總結
  • 現有的各類VIPER實現
    • Brigade團隊的實現
      • 爭議
    • Rambler&Co團隊的實現
      • 爭議
    • Uber團隊的實現
      • 各部分職責
      • 數據驅動
      • 爭議
      • 其餘設計
  • 方案一:最完整的VIPER
    • View
    • Presenter
    • Interactor
    • Service
    • Wireframe
    • Router
    • Adapter
    • Builder
  • 模塊間解耦
  • 子模塊
    • 子模塊的來源
    • 通訊方式
  • 依賴注入
  • 映射到MVC
  • 方案二:容許適當耦合
    • View
    • Presenter
    • Interactor
    • 路由和依賴注入
    • 總結
  • Demo和代碼模板
  • 參考

起源

VIPER架構,最初是2013年在MutualMobile的技術博客上,由Jeff Gilbert 和 Conrad Stoll 提出的。他們的博客網站有過一次遷移,原文地址已經失效,這是遷移後的博文:MEET VIPER: MUTUAL MOBILE’S APPLICATION OF CLEAN ARCHITECTURE FOR IOS APPSgithub

這是文章中提出的架構示意圖:數據庫

viper-mutualmobile

Wireframe能夠看做是Router的另外一種表達。能夠看到,VIPER之間的關係已經很明確了。以後,做者在2014年在objc.io上發表了另外一篇更詳細的介紹文章:Architecting iOS Apps with VIPERswift

在做者的第一篇文章裏,闡述了VIPER是在接觸到了Uncle Bob的Clean Architecture後,對Clean Architecture的一次實踐。所以,VIPER真正的源頭應該是Clean Architecture。後端

Clean Architecture

由Uncle Bob在2011年提出的Clean Architecture,是一個平臺無關的抽象架構。想要詳細學習的,能夠閱讀做者的原文:Clean Architecture,翻譯:乾淨的架構The Clean Architecturebash

它經過梳理軟件中不一樣層之間的依賴關係,提出了一個自外向內,單向依賴的架構,以下圖所示:網絡

Clean Architecture

越靠近內層,越變得抽象,越接近設計的核心。越靠近外層,越和具體的平臺和實現技術相關。內層的部分徹底不知道外層的存在和實現方式,代碼只能從外層向內層引用,目的是爲了實現層與層之間的隔離。將不一樣抽象程度的層進行隔離,作到了把業務規則和具體實現分離開。你能夠把外層看做是內層的delegate,外層只能經過內層提供的delegate接口來使用內層。數據結構

Enterprise Business Rules

表明了這個軟件項目的業務規則。由數據實體體現,是一些能夠在不一樣的程序應用之間共享的數據結構。

Application Business Rules

表明了本應用所使用的一些業務規則。封裝和實現了用到的業務功能,會將各類實體的數據結構轉爲在用例中傳遞的實體類,可是和具體的數據庫技術或者UI無關。

Interface Adapters

接口適配層。將用例的規則和具體的實現技術進行抽象地對接,將用例中用到的實體類轉爲供數據庫存儲的格式或者供View展現的格式。相似於MVVM中把Model的數據傳遞給ViewModel供View顯示。

右下角表示了接口適配層中不一樣模塊間的通訊方式。不一樣的模塊在業務用例中產生關聯和數據傳遞。Input、Output就是Use Case提供給外層的數據流動接口。

Frameworks & Drivers

庫和驅動層,表明了選用的各類具體的實現技術,例如持久層使用SQLite仍是Core Data,網絡層使用NSURLSession、NSURLConnection仍是AFNetworking等。

總結

能夠看到,Clean Architecture裏已經出現了Use Case、Interactor、Presenter等概念,它爲VIPER的工程實現提供了設計思想,VIPER將它的設計轉化成了具體的實現。VIPER裏的各部分正是存在着由外向內的依賴,從外向內表現爲:View -> Presenter -> Interactor -> EntityWireframe嚴格來講也是一類特殊的Use Case,用於不一樣模塊之間通訊,鏈接了不一樣的Presenter

必需要記住的是,VIPER架構是根據由外向內的依賴關係來設計的。這句話是指導咱們進行進一步設計和優化的關鍵。

現有的各類VIPER實現

MutualMobile的那兩篇文章雖然已經明確了VIPER各部分之間的職責,而且給出了簡單的Demo,可是對Wireframe部分的實現有些爭議,解耦作得不夠完全,而且對各層之間如何交互還處在最簡單的實現上。以後出現了挺多文章來將VIPER進一步細化,不過某些細節的實現上有些差異,在給出我本身的VIPER以前,我將先對這些實現進行一次綜合的比較分析,看看他們都使用了哪些技術,遇到了哪些爭議點。不一樣實現之間已經公認的地方我就再也不單獨列出了。

Brigade團隊的實現

原文地址:Brigade’s Experience Using an MVC Alternative: VIPER architecture for iOS applications

文章把VIPER的優勢總結了一下,提出了這樣的架構圖:

Brigade’s VIPER

他們對VIPER的各部分都沒有異議,只是對Interactor的實現進行了進一步細化。用一個Data Manager提供給各個Use Case管理Entity,好比獲取、存儲功能。在Service中調用網絡層去獲取服務端的數據。

文章中還認爲應該由Wireframe負責初始化整個VIPER,生成各部分的類,並設置依賴關係,而且引用另外一個模塊的Wireframe,負責跳轉到另外一個界面。

和這個實現相似的還有:

針對VIPER須要編寫太多初始化代碼的麻煩,可使用Xcode自帶的Template解決。而不少做者都提到了一個代碼生成工具:Generamba

爭議

文章並無對VIPER進行修改,只是進一步細化了。這應該是一個最簡單的實現。若是你要實施VIPER,參照這篇文章來沒有什麼大問題。可是它沒有探討的問題是:

  • 如何解決不一樣Wrieframe之間的耦合?
  • Wrieframe如何知道其餘模塊須要的初始化參數?
  • 在模塊間通訊時,Interactor的數據如何傳遞給另外一個模塊?
  • 父模塊和子模塊之間是怎樣的關係?

Rambler&Co團隊的實現

一個對VIPER十分感興趣的俄國團隊,編寫了一本關於VIPER的書:The-Book-of-VIPER。而且給出了一個目前網絡上實現完成度最高的開源Demo:rambler-it-ios,以及他們用於實施VIPER的庫:ViperMcFlurry

他們整理的VIPER架構圖以下:

Rambler&Co's VIPER

和其餘實現不一樣的是,他們把VIPER的初始化和裝配工做單獨放到了一個Assembly裏,Router只作界面跳轉的工做。而且把VIPER內不一樣部分之間的通訊統一用Input和Output來表示。Input表示外部主動調用模塊提供的接口,Output表示模塊經過外部實現所要求的接口,將事件傳遞到外部。

之因此將模塊初始化單獨放到Assembly裏,是由於Router若是負責初始化本模塊,會違背單一職責原則。

爭議

這個實現的願景很好,只是在轉變爲具體實現的時候不夠完美,有不少問題尚待解決。具體能夠參見Demo。

  • Assembly使用了Typhoon這個依賴注入工具,經過Method Swizzling自動初始化VIPER的各個部分

我對Typhoon這個依賴注入工具不是特別感冒,它使用了十分複雜的run time技術,想要追蹤一個對象的注入過程時,會看得暈頭轉向。並且它沒法實現運行時由調用方動態注入,只能實現預約義好的靜態注入。也就是不能動態傳參。

  • 使用storyboard進行路由

在Demo中實現了在執行segue時用block來使用-prepareForSegue:sender:,實現向目的界面傳參,實現了動態注入。可是這樣就把路由限定在了storyboard的segue技術上,那麼對於那些沒有使用storyboard的項目應該怎麼辦呢?Demo並無給出答案。並且-prepareForSegue:sender:只能向View傳參,可是有一些參數是View不該該接觸到的,而是應該直接傳給Presenter或者Interactor的。

  • 有時候模塊須要從Output中獲取數據,例如Presenter主動獲取View中的文字,傳遞給Interactor,此時Output並不能完整描述它的職責,還能夠再進一步劃分

也就是說,他們的方案在設計上是不錯的,但在技術上還有不少改進空間。

Uber團隊的實現

Uber因爲業務愈來愈複雜,舊項目的架構已經沒法知足當前的需求,所以在2016年徹底重構了他們的 rider app。他們借鑑VIPER,而且設計出了一個VIPER的變種架構:Riblets。文章地址:ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP

架構圖以下:

riblets

數據流向圖:

數據流向

父模塊和子模塊之間通訊:

父子模塊間通訊

各部分職責

這裏只列出一些和VIPER有差別的地方:

  • Builder負責初始化Riblets模塊內的各個部分,定義了模塊的依賴參數
  • Component負責獲取和初始化那些不是Riblets模塊內的部分,例如services,並注入到Interactor中
  • Router負責管理子模塊,持有子模塊的Router,並把子模塊的View添加到視圖樹上
  • Interactor經過調用Service管理Model,而不是在Interactor中直接管理
  • Interactor和子模塊的Interactor經過監聽者模式和delegate互相通訊

數據驅動

最大的改變是將Router從Presenter移到了Interactor,改變了模塊的主從關係,整個模塊的生命週期如今由Interactor來管理。而以前的VIPER模塊是依賴於View的生命週期的。這樣一來,整個架構就從View驅動變成了業務驅動,或者數據驅動。

關於這個改變,Uber給出了兩個緣由:

  • 想要統一iOS和Andorid的軟件架構,以及更好地互相借鑑開發經驗和教訓,於是須要改變iOS中視圖驅動的設計
  • 想要建立一個沒有View,只有業務邏輯的模塊,所以生命週期須要由Interactor管理

爭議

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給出的第一個基於團隊統一的理由。

其餘設計

文章裏還給出了一些頗有參考價值的內容,好比:

  • 對Interactor進行注入的Component
  • 視圖樹變成了Router樹
  • Interactor不直接維護Model,而是經過對應的Service來維護Model
  • 父模塊和子模塊之間經過Interactor來通訊

Uber的這個方案講了不少其餘方案沒有提到的方面,好比依賴注入、如何引入子模塊等問題。不過這個方案並無開源。

方案一:最完整的VIPER

首先總結出一個絕對標準的VIPER,各部分遵循隔離關係,同時考慮到依賴注入、子模塊通訊、模塊間解耦等問題,將VIPER的各部分的職責變得更加明確,也新增了幾個角色。示例圖以下,各角色的顏色和Clean Architecture圖中各層的顏色對應:

thorough viper

示例代碼將用一個筆記應用做爲演示。

View

View能夠是一個UIView + UIViewController,也能夠只是一個custom UIView,也能夠是一個自定義的用於管理UIView的Manager,只要它實現了View的接口就能夠。

View層的職責:

  • 展現界面,組合各類UIView,並在UIViewController內管理各類控件的佈局、更新
  • View對外暴露各類用於更新UI的接口,而本身不主動更新UI
  • View持有一個由外部注入的eventHandler對象,將View層的事件發送給eventHandler
  • View持有一個由外部注入的viewDataSource對象,在View的渲染過程當中,會從viewDataSource獲取一些用於展現的數據,viewDataSource的接口命名應該儘可能和具體業務無關
  • View向Presenter提供routeSource,也就是用於界面跳轉的源界面

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

Presenter由View持有,它的職責有:

  • 接收並處理來自View的事件
  • 維護和View相關的各類狀態和配置,好比界面是否使用夜間模式等
  • 調用Interactor提供的Use Case執行業務邏輯
  • 向Interactor提供View中的數據,讓Interactor生成須要的Model
  • 接收並處理來自Interactor的業務事件回調事件
  • 通知View進行更新操做
  • 經過Wireframe跳轉到其餘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
複製代碼

Interactor

Ineractor的職責:

  • 實現和封裝各類業務的Use Case,供外部調用
  • 維護和業務相關的各類狀態,好比是否正在編輯筆記
  • Interactor能夠獲取各類Manager和Service,用於組合實現業務邏輯,這些Manager和Service應該是由外部注入的依賴,而不是直接引用具體的類
  • 經過DataManager維護Model
  • 監聽各類外部的業務事件並處理,必要時將事件發送給eventHandler
  • Interactor持有一個由外部注入的eventHandler對象,將須要外部處理的業務事件發送給eventHandler,或者經過eventHandler接口對某些數據操做的過程進行回調
  • Interactor持有一個由外部注入的dataSource對象,用於獲取View上的數據,以更新Model

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
複製代碼

Service

向Interactor提供各類封裝好的服務,例如數據庫的訪問、存儲,調用定位功能等。Service由Application在執行路由時注入到Builder裏,再由Buidler注入到Interactor裏。也能夠只注入一個Service Router,在運行時再經過這個Service Router懶加載須要的Service,至關於注入了一個提供Router功能的Service。

Service能夠看做是沒有View的VIPER,也有本身的路由和Builder。

Wireframe

翻譯成中文叫線框,用於表達從一個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

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
複製代碼

Adapter

由Application實現,負責在模塊通訊時進行一些接口的轉換,例如兩個模塊使用了相同業務功能的某個Service,使用的protocol實現同樣,可是protocol名字不同,就能夠在路由時,在Adapter裏進行一次轉換。甚至只要定義的邏輯同樣,依賴參數的名字和數據類型也能夠容許不一樣。這樣就能讓模塊不依賴於某個具體的protocol,而是依賴於protocol實際定義的依賴和接口。

注意這裏的Adapter和Clean Architecture裏的Interface Adapter是不同的。這裏的Adapter就是字面意義上的接口轉換,而Clean Architecture裏的Interface Adapter層更加抽象,是Use Case層與具體實現技術之間的轉換,囊括了更多的角色。

Builder

負責初始化整個模塊,配置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引用了一個封裝好的子View控件,連帶着引入了子View的整個VIPER
  • Interactor使用了一個Service

通訊方式

子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

若是你須要把一個模塊從MVC重構到VIPER,能夠先按照這個步驟:

  • 整理Controller中的代碼,把不一樣職責的代碼用pragma mark分隔好
  • 整理好後,按照各部分的職責,將代碼分散到VIPER的各個角色中,此時View、Presenter、Interactor之間能夠直接互相引用
  • 把View、Presenter、Interactor進行解耦,抽出接口,互相之間依賴接口進行交互

下面就是第一步裏在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就是一個很適合成爲中介者的角色。

架構圖以下:

final viper

View

  • View能夠直接經過Router引入另外一個子View,不須要經過Presenter的路由來引入
  • View中的一些delegate若是變化的可能性不大,能夠直接讓Presenter實現(例如UITableViewDataSource),不用再封裝一遍後交給Presenter
  • View不能出現Model類

Presenter

  • Presenter能夠直接調用Router執行路由,不用再經過Wireframe封裝一遍
  • Presenter的接口參數中能夠出現Model類,可是不能導入Model類的頭文件而且使用Model類,只能用於參數傳遞
  • Presenter中不建議導入UIKit,除非能保證不會使用那些會影響控件渲染的方法

Interactor

  • 一些app中經常使用的Service能夠直接引入,不須要經過外部注入的方式來使用
  • Interactor能夠用一個Service Router來動態獲取Service

路由和依賴注入

改變得最多的就是路由部分。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和代碼模板

針對兩個方案,同時寫了兩個相同功能的Demo,能夠比較一下代碼上的區別。地址在:ZIKViper。注意,Demo須要先用pod install安裝一下依賴庫。

項目裏也提供了Xcode File Template用於快速生成VIPER代碼模板。把.xctemplate後綴的文件夾拷貝到~/Library/Developer/Xcode/Templates/目錄下,就能夠在Xcode的New->File->Template裏選擇代碼模板快速生成代碼。

參考

相關文章
相關標籤/搜索