前言
看了下上篇博客的發表時間到這篇博客,竟然過了11個月,罪過,罪過。這一年時間也是夠折騰的,年初離職跳槽到鵝廠,單獨負責一個社區項目,忙的天昏地暗,忙的差不多了,轉眼就到了7月。
七月流火,心也跟著燥熱起來了,眼瞅著移動端這發展趨勢從05年開始就一直在走下坡路了,想著再這么下去不行,得找條后路備著。網上看了看,覺得前端不錯,最近炒的挺火熱的,那就學學看吧,買了html,css,js的幾本書,花了個把月的閑暇時間看完了,順便做了幾個demo,突然覺得好無聊。大概是iOS也是寫界面,前端還是寫界面,寫的有些麻木了。之前一直有學Python,寫過一些爬蟲、用Django也寫過后臺,感覺還挺好玩的,Python在大數據和AI領域也大放異彩,想借此機會學學。
雖然這兩個領域進入門檻比較高,但是就目前發展勢頭來看,應該是一個發展趨勢,互聯網過去十年的浪潮是移動互聯網,下一個十年的浪潮很可能就是AI了。所以早做準備,從零開始學吧。其實干程序員這行,焦慮是無法避免的,因為自己那點知識儲備和日新月異的技術發展相比起來,簡直滄海一粟,不由讓人感嘆:吾生也有涯,而學無涯。
很多人都在追逐新技術的過程中迷失了自己,越學越焦慮,因為發現自己無論怎么學,都趕不上技術發展的腳步。我倒覺得如其去追逐那些還不知道能不能落地的新技術,還不如扎扎實實打好基本功,比如系統、數據結構、算法、網絡,新技術層出不窮,亂花漸入迷人眼,但是歸根到底也是在這些基礎知識上面建立起來的。
關于如何學習,有時間咱們單獨開一篇聊聊。下面進入今天正題,聊一聊在iOS開發領域里面幾大架構的應用,包括MVC、MVP、MVVM、VIPER,做iOS開發一般都是比較熟悉MVC的,因為Apple已經為我們量身定制了適合iOS開發的MVC架構。
但是在寫代碼的過程中大家肯定會有這些疑問:為什么我的VC越來越大,為什么感覺apple的MVC怪怪的不像真正的MVC,網絡請求邏輯到底放在哪層,網上很火熱的MVVM是否值得學習,VIPER又是什么鬼?
我希望下面的文字能為大家解除這些疑惑,我會在多個維度對這幾個框架進行對比分析,指出他們的優劣,然后結合一個具體的DEMO用不同的架構去實現,讓大家對這些架構有一個直觀的了解。當然這些都只是做拋磚引玉之用,闡述的也是我的個人理解,如有錯誤,歡迎指出,大家一起探討進步~~
MVC
1、MVC的理想模型
MVC的理想模型如下圖所示:
各層的職責如下所示:
- Models:?數據層,負責數據的處理和獲取的數據接口層。
- Views: 展示層(GUI),對于 iOS 來說所有以 UI 開頭的類基本都屬于這層。
- Controller: 控制器層,它是 Model 和 View 之間的膠水或者說是中間人。一般來說,當用戶對 View 有操作時它負責去修改相應 Model;當 Model 的值發生變化時它負責去更新對應 View。
如上圖所示,M和View應該是完全隔離的,由C作為中間人來負責二者的交互,同時三者是完全獨立分開的,這樣可以保證M和V的可測試性和復用性,但是一般由于C都是為特別的應用場景下的M和V做中介者,所以很難復用。
2、MVC在iOS里面的實現
但是實際上在iOS里面MVC的實現方式很難做到如上所述的那樣,因為由于Apple的規范,一個界面的呈現都需要構建一個viewcontroller,而每個viewcontroller都帶有一個根view,這就導致C和V緊密耦合在一起構成了iOS里面的C層,這明顯違背了MVC的初衷。
apple里面的MVC真正寫起來大概如下圖所示:
這也是massive controller的由來,具體的下面再講
那么apple為什么要這么干呢?完整的可以參考下apple對于MVC的解釋,下面的引用是我摘自其中一段。簡單來說就是iOS里面的viewcontroller其實是view和controller的組合,目的就是為了提高開發效率,簡化操作。
摘自上面的鏈接
One can merge the MVC roles played by an object, making an object, for example, fulfill both the controller and view roles—in which case, it would be called a view controller. In the same way, you can also have model-controller objects. For some applications, combining roles like this is an acceptable design.
A model controller is a controller that concerns itself mostly with the model layer. It “owns” the model; its primary responsibilities are to manage the model and communicate with view objects. Action methods that apply to the model as a whole are typically implemented in a model controller. The document architecture provides a number of these methods for you; for example, an NSDocument object (which is a central part of the document architecture) automatically handles action methods related to saving files.
A view controller is a controller that concerns itself mostly with the view layer. It “owns” the interface (the views); its primary responsibilities are to manage the interface and communicate with the model. Action methods concerned with data displayed in a view are typically implemented in a view controller. An NSWindowController object (also part of the document architecture) is an example of a view controller.
對于簡單界面來說,viewcontroller結構確實可以提高開發效率,但是一旦需要構建復雜界面,那么viewcontroller很容易就會出現代碼膨脹,邏輯滿天飛的問題。
另外我想說一句,apple搞出viewcontroller(VC)這么個玩意初衷可能是好的,寫起來方便,提高開發效率嘛。確實應付簡單頁面沒啥問題,但是有一個很大的弊端就是容易把新手代入歧途,認為真正的MVC就是這么干的,導致很多新手都把本來view層的代碼都堆到了VC,比如在VC里面構建view、view的顯示邏輯,甚至在VC里面發起網絡請求。
這也是我當初覺得VC很怪異的一個地方,因為它沒辦法歸類到MVC的任何一層,直到看到了apple文檔的那段話,才知道VC原來是個組合體。
下面來談談現有iOS架構下MVC各層的職責,這里要注意下,下面的Controller層指的是iOS里面的VC組合體
3、iOS的MVC各層職責
controller層(VC):
- 生成view,然后組裝view
- 響應View的事件和作為view的代理
- 調用model的數據獲取接口,拿到返回數據,處理加工,渲染到view顯示
- 處理view的生命周期
- 處理界面之間的跳轉
model層:
- 業務邏輯封裝
- 提供數據接口給controller使用
- 數據持久化存儲和讀取
- 作為數據模型存儲數據
view層:
- 界面元素搭建,動畫效果,數據展示,
- 接受用戶操作并反饋視覺效果
PS:
model層的業務邏輯一般都是和后臺數據交互的邏輯,還有一些抽象的業務邏輯,比如格式化日期字符串為NSDateFormatter類型等
4、massive controller
從上面的MVC各層職責劃分就可以看出來C干了多少事,這還是做了明確的職責劃分的情況下,更不用提新手把各種view和model層的功能都堆到C層后的慘不忍睹。
在復雜界面里面的VC代碼輕松超過千行,我之間就見過超過5000行代碼的VC,找個方法只能靠搜索,分分鐘想死的節奏。
造成massive controller的原因的罪魁禍首就是apple的把view和Cotroller組合在一起,讓VC同時做view和C的事,導致代碼量激增,也違背了MVC原則。
下面來舉一個簡單的例子,先聲明下我下面列舉的例子主要來著這篇博客:
這篇文章質量很高,對三種模式的講解比較深入,關鍵還有例子來做橫向對比,這是其他文章沒有的。大家可以先看看這篇文章,本文的demo來自這篇文章,但是我按照自己的理解在其基礎上做了一些修改,大家可以自己對比下,做出自己的選擇。
還有一些圖也是借鑒該篇文字,在此感謝作者~
先看兩張圖:
這個界面分為三個部分,頂部的個人信息展示,下面有兩張列表,分別展示博客和草稿內容。
我們先來看看一般新手都是怎么實現的
//UserVC
- (void)viewDidLoad {
[super viewDidLoad];
[[UserApi new] fetchUserInfoWithUserId:132 completionHandler:^(NSError *error, id result) {
if (error) {
[self showToastWithText:@"獲取用戶信息失敗了~"];
} else {
self.userIconIV.image = ...
self.userSummaryLabel.text = ...
...
}
}];
[[userApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) {
if (error) {
[self showErrorInView:self.tableView info:...];
} else {
[self.blogs addObjectsFromArray:result];
[self.tableView reloadData];
}
}];
[[userApi new] fetchUserDraftsWithUserId:132 completionHandler:^(NSError *error, id result) {
//if Error...略
[self.drafts addObjectsFromArray:result];
[self.draftTableView reloadData];
}];
}
//...略
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.blogTableView) {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"];
cell.blog = self.blogs[indexPath.row];
return cell;
} else {
DraftCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DraftCell"];
cell.draft = self.drafts[indexPath.row];
return cell;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.blogTableView){
[self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:self.blogs[indexPath.row]] animated:YES];
}else{
[self.navigationController pushViewController:[draftDetailViewController instanceWithdraft:self.drafts[indexPath.row]] animated:YES];
}
//DraftCell
- (void)setDraft:(draft)draft {
_draft = draft;
self.draftEditDate = ...
}
//BlogCell
- (void)setBlog:(Blog)blog {
...同上
}
model:
Blog.h
=========
#import <Foundation/Foundation.h>
@interface Blog : NSObject
- (instancetype)initWithBlogId:(NSUInteger)blogId;
@property (copy, nonatomic) NSString *blogTitle;
@property (copy, nonatomic) NSString *blogSummary;
@property (assign, nonatomic) BOOL isLiked;
@property (assign, nonatomic) NSUInteger blogId;
@property (assign, nonatomic) NSUInteger likeCount;
@property (assign, nonatomic) NSUInteger shareCount;
@end
~~~~~~~~~~~~~~~~~~~~~
blog.m
========
#import "Blog.h"
@implementation Blog
@end
如果后續再增加需求,那么userVC的代碼就會越來越多,這就是我們上面說的massive controller出現了。維護性和可測試性無從談起,我們是按照apple的MVC架構寫的呀,為什么會出現這種問題呢?
暫且按下不表,我們先看另外一個問題,先把這個問題搞清楚了,對于后續文章的理解大有裨益。
5、Model層的誤解
我看到很多所謂的MVC的M層實現就如上面所示,只有幾個干巴巴的屬性。我之前也是一直這么寫的,但是我一直覺得有疑惑,覺得這樣寫的話,怎么可能算的上一個單獨的層呢?說是數據模型還差不多。
那么實現正確的M層姿勢應該是什么樣的呢?
大家具體可以看下面這篇文章,對于M層講解的非常不錯,但是對于文中的MVVM的理解我不敢茍同,大家見仁見智吧
下面的引用也是摘自這篇文章:
理解Model層:
首先要正確的理解MVC中的M是什么?他是數據模型嗎?答案是NO。他的正確定義是業務模型。也就是你所有業務數據和業務實現邏輯都應該定義在M層里面,而且業務邏輯的實現和定義應該和具體的界面無關,也就是和視圖以及控制之間沒有任何的關系,它是可以獨立存在的,您甚至可以將業務模型單獨編譯出一個靜態庫來提供給第三方或者其他系統使用。
在上面經典MVC圖中也很清晰的描述了這一點: 控制負責調用模型,而模型則將處理結果發送通知給控制,控制再通知視圖刷新。因此我們不能將M簡單的理解為一個個干巴巴的只有屬性而沒有方法的數據模型。
其實這里面涉及到一個最基本的設計原則,那就是面向對象的基本設計原則:就是什么是類?類應該是一個個具有不同操作和不同屬性的對象的抽象(類是屬性和方法的集合)。 我想現在任何一個系統里面都沒有出現過一堆只有數據而沒有方法的數據模型的集合被定義為一個單獨而抽象的模型層來供大家使用吧。 我們不能把一個保存數據模型的文件夾來當做一個層,這并不符合橫向切分的規則。
Model層實現的正確姿勢:
定義的M層中的代碼應該和V層和C層完全無關的,也就是M層的對象是不需要依賴任何C層和V層的對象而獨立存在的。整個框架的設計最優結構是V層不依賴C層而獨立存在,M層不依賴C層和V層獨立存在,C層負責關聯二者,V層只負責展示,M層持有數據和業務的具體實現,而C層則處理事件響應以及業務的調用以及通知界面更新。三者之間一定要明確的定義為單向依賴,而不應該出現雙向依賴
M層要完成對業務邏輯實現的封裝,一般業務邏輯最多的是涉及到客戶端和服務器之間的業務交互。M層里面要完成對使用的網絡協議(HTTP, TCP,其他)、和服務器之間交互的數據格式(XML, JSON,其他)、本地緩存和數據庫存儲(COREDATA, SQLITE,其他)等所有業務細節的封裝,而且這些東西都不能暴露給C層。所有供C層調用的都是M層里面一個個業務類所提供的成員方法來實現。也就是說C層是不需要知道也不應該知道和客戶端和服務器通信所使用的任何協議,以及數據報文格式,以及存儲方面的內容。這樣的好處是客戶端和服務器之間的通信協議,數據格式,以及本地存儲的變更都不會影響任何的應用整體框架,因為提供給C層的接口不變,只需要升級和更新M層的代碼就可以了。比如說我們想將網絡請求庫從ASI換成AFN就只要在M層變化就可以了,整個C層和V層的代碼不變。
文章還給出了實現的例子,我就不粘貼過來了,大家自己過去看看
總結來說:
M層不應該是數據模型,放幾個屬性就完事了。而應該是承載業務邏輯和數據存儲獲取的職責一層。
6、如何構建構建正確的MVC
現在我們來看看到底該如何在iOS下面構建一個正確的MVC呢?
首先先達成一個共識:viewcontroller不是C層,而是V和C兩層的混合體。
我們看到在標準的iOS下的MVC實現里面,C層做了大部分事情,大體分為五個部分(見上面MVC各層職責),因為他是兩個層的混合,為了給VC減負,我們現在把VC只當做一個view的容器來使用。
這里我要解釋下什么叫做view的容器,我們知道apple的VC有一個self.view,所有要顯示在界面的上面的view都必須通過addsubview來添加到這個根view上面來。同時VC還控制著view的生命周期。那么我們可不可以把VC看成一個管理各個View的容器?
大家可以看這篇文章加深理解下我上面說的view container的概念:
此時VC的職責簡化為如下三條職責:
- 生成子view并添加到自己的self.view上面
- 管理view的生命周期
- 通知每個子C去獲取數據
前面兩點很好理解吧,上面已經講過了。第三點我們接著往下看
消失的C層
回到我們上面說的第四點的例子,什么原因造成VC的代碼越來越臃腫呢?
因為我們對于view和model層的職責都劃分的比較清楚,前者負責數據展示,后者負責數據獲取,那么那些模棱兩可的代碼,放在這兩層感覺都不合適,就都丟到了VC里面,導致VC日益膨脹。
此時的代碼組織如下圖所示:
通過這張圖可以發現, 用戶信息頁面(userVC)作為業務場景Scene需要展示多種數據M(Blog/Draft/UserInfo), 所以對應的有多個View(blogTableView/draftTableView/image…), 但是, 每個MV之間并沒有一個連接層C, 本來應該分散到各個C層處理的邏輯全部被打包丟到了Scene(userVC)這一個地方處理, 也就是M-C-V變成了MM…-Scene-…VV, C層就這樣莫名其妙的消失了.
另外, 作為V的兩個cell直接耦合了M(blog/draft), 這意味著這兩個V的輸入被綁死到了相應的M上, 復用無從談起.
最后, 針對這個業務場景的測試異常麻煩, 因為業務初始化和銷毀被綁定到了VC的生命周期上, 而相應的邏輯也關聯到了和View的點擊事件, 測試只能Command+R, 點點點…
那么怎么實現正確的MVC呢?
如下圖所示,該界面的信息分為三部分:個人信息、博客列表信息、草稿列表信息。我們應該也按照這三部分分成三個小的MVC,然后通過VC拼接組裝這三個子MVC來完成整個界面。
具體代碼組織架構如下:
UserVC作為業務場景, 需要展示三種數據, 對應的就有三個MVC, 這三個MVC負責各自模塊的數據獲取, 數據處理和數據展示, 而UserVC需要做的就是配置好這三個MVC, 并在合適的時機通知各自的C層進行數據獲取, 各個C層拿到數據后進行相應處理, 處理完成后渲染到各自的View上, UserVC最后將已經渲染好的各個View進行布局即可
具體的代碼見最后的demo里面MVC文件夾。
關于demo的代碼,我想說明一點自己的看法:在demo里面網絡數據的獲取,作者放到了一個單獨的文件UserAPIManager
里面。我覺得最好是放在和業務相關的demo里面,因為接口一旦多起來,一個文件很容易膨脹,如果按照業務分為多個文件,那么還不如干脆放在model里面更加清晰。
PS:
圖中的blogTableViewHelper對應代碼中的blogTableViewController,其他幾個helper同樣的
此時作為VC的userVC只需要做三件事:
- 生成子view并添加到自己的self.view上面
- 管理view的生命周期
- 通知每個子C去獲取數據
userVC的代碼大大減少,而且此時邏輯更加清楚,而且因為每個模塊的展示和交互是自管理的, 所以userVC只需要負責和自身業務強相關的部分即可。
另外如果需要在另外一個VC上面展示博客列表數據,那么只需要把博客列表的view添加到VC的view上面,然后通過博客列表的controller獲取下數據就可以了,這樣就達到了復用的目的。
我們通過上面的方法,把userVC里面的代碼分到了三個子MVC里面,架構更加清晰明了,對于更加復雜的頁面,我們可以做更細致的分解,同時每個子MVC其實還可以拆分成更細的MVC。具體的拆分粒度大家視頁面復雜度靈活變通,如果預計到一個頁面的業務邏輯后續會持續增加,還不如剛開始就拆分成不同的子MVC去實現。如果只是簡單的頁面,那么直接把所有的邏輯都寫到VC里面也沒事。
7、MVC優缺點
優點
上面的MVC改造主要是把VC和C加以區分,讓MVC成為真正的MVC,而不是讓VC當成C來用,經過改造后的MVC對付一般場景應該綽綽有余了。不管界面多復雜,都可以拆分成更小的MVC然后再組裝起來。
寫代碼就是一個不斷重構的過程,當項目越來越大,單獨功能可以抽離出來作為一個大模塊,打包成pod庫(這個是組件化相關的知識點,后面我也會寫一篇博客)。同時在模塊內部你又可以分層拆分。爭取做到單一原則,不要在一個類里面啥都往里面堆
總結下MVC的優點有如下幾點:
- 代碼復用: 三個小模塊的V(cell/userInfoView)對外只暴露Set方法, 對M甚至C都是隔離狀態, 復用完全沒有問題. 三個大模塊的MVC也可以用于快速構建相似的業務場景(大模塊的復用比小模塊會差一些, 下文我會說明).
- 代碼臃腫: 因為Scene大部分的邏輯和布局都轉移到了相應的MVC中, 我們僅僅是拼裝MVC的便構建了兩個不同的業務場景, 每個業務場景都能正常的進行相應的數據展示, 也有相應的邏輯交互, 而完成這些東西, 加空格也就100行代碼左右(當然, 這里我忽略了一下Scene的布局代碼).
- 易拓展性: 無論產品未來想加回收站還是防御塔, 我需要的只是新建相應的MVC模塊, 加到對應的Scene即可.
- 可維護性: 各個模塊間職責分離, 哪里出錯改哪里, 完全不影響其他模塊. 另外, 各個模塊的代碼其實并不算多, 哪一天即使寫代碼的人離職了, 接手的人根據錯誤提示也能快速定位出錯模塊.
- 易測試性: 很遺憾, 業務的初始化依然綁定在Scene的生命周期中, 而有些邏輯也仍然需要UI的點擊事件觸發, 我們依然只能Command+R, 點點點…
缺點
經過上面的改造,MVC架構已經足夠清晰了,按照應用場景(一般都是單頁面)進行大的拆分,然后在根據業務拆分成小的MVC。不行就接著拆,拆層,拆模塊。
但是MVC的最大弊端就是C的代碼沒法復用,所以能把C層的代碼拆出來就盡量拆,我們來看看現在C層的功能還有哪些了
- 作為View和Model的中介者,從model獲取數據,經過數據加工,渲染到view上面顯示
- 響應view的點擊事件,然后執行相應的業務邏輯
- 作為view的代理和數據源
- 暴露接口給SceneVC來驅動自己獲取數據
這就導致一個問題:
業務邏輯和業務展示強耦合: 可以看到, 有些業務邏輯(頁面跳轉/點贊/分享…)是直接散落在V層的, 這意味著我們在測試這些邏輯時, 必須首先生成對應的V, 然后才能進行測試. 顯然, 這是不合理的. 因為業務邏輯最終改變的是數據M, 我們的關注點應該在M上, 而不是展示M的V
舉個例子吧,比如demo中的點贊功能代碼如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
BlogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.title = cellHelper.blogTitleText;
cell.summary = cellHelper.blogSummaryText;
cell.likeState = cellHelper.isLiked;
cell.likeCountText = cellHelper.blogLikeCountText;
cell.shareCountText = cellHelper.blogShareCountText;
//點贊的業務邏輯
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已經贊過它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//點贊的業務展示
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
return cell;
}
通過代碼可以清晰的看到,必須生成cell,然后點擊cell上面的點贊按鈕,才可以觸發點贊的業務邏輯。
但是業務邏輯一般改變的model數據,view只是拿到model的數據進行展示。現在卻把這兩個原本獨立的事情合在一起了。導致業務邏輯沒法單獨測試了。
下面提到的MVP正是為了解決這一問題而誕生的,我們接著往下看。
MVP
下面關于MVP文字,有部分文字和圖片摘抄自該文章,在此感謝作者,之前忘記放上鏈接,向作者道歉:
淺談 MVC、MVP 和 MVVM 架構模式
1、概述
MVC的缺點在于并沒有區分業務邏輯和業務展示, 這對單元測試很不友好. MVP針對以上缺點做了優化, 它將業務邏輯和業務展示也做了一層隔離, 對應的就變成了MVCP.
M和V功能不變, 原來的C現在只負責布局, 而所有的業務邏輯全都轉移到了P層。P層處理完了業務邏輯,如果要更改view的顯示,那么可以通過回調來實現,這樣可以減輕耦合,同時可以單獨測試P層的業務邏輯
MVP的變種及定義比較多,但是最終廣為人知的是Martin Fowler 的發表的關于Presentation Model描述,也就是下面將要介紹的MVP。具體看下面這篇文章:
Martin Fowler 發表的 Presentation Model 文章
MVP從視圖層中分離了行為(事件響應)和狀態(屬性,用于數據展示),它創建了一個視圖的抽象,也就是presenter層,而視圖就是P層的『渲染』結果。P層中包含所有的視圖渲染需要的動態信息,包括視圖的內容(text、color)、組件是否啟用(enable),除此之外還會將一些方法暴露給視圖用于某些事件的響應。
2、MVP架構和各層職責對比
MVP的架構圖如下所示:
在 MVP 中,Presenter 可以理解為松散的控制器,其中包含了視圖的 UI 業務邏輯,所有從視圖發出的事件,都會通過代理給 Presenter 進行處理;同時,Presenter 也通過視圖暴露的接口與其進行通信。
各層職責如下
VC層
- view的布局和組裝
- view的生命周期控制
- 通知各個P層去獲取數據然后渲染到view上面展示
controller層
- 生成view,實現view的代理和數據源
- 綁定view和presenter
- 調用presenter執行業務邏輯
model層
- 和MVC的model層類似
view層
- 監聽P層的數據更新通知, 刷新頁面展示.(MVC里由C層負責)
- 在點擊事件觸發時, 調用P層的對應方法, 并對方法執行結果進行展示.(MVC里由C層負責)
- 界面元素布局和動畫
- 反饋用戶操作
Presenter層職責
- 實現view的事件處理邏輯,暴露相應的接口給view的事件調用
- 調用model的接口獲取數據,然后加工數據,封裝成view可以直接用來顯示的數據和狀態
- 處理界面之間的跳轉(這個根據實際情況來確定放在P還是C)
我們來分析下View層的職責,其中3、4兩點和MVC的view類似,但是1、2兩點不同,主要是因為業務邏輯從C轉移到了P,那么view的事件響應和狀態變化肯定就依賴P來實現了。
這里又有兩種不同的實現方式:
- 讓P持有V,P通過V的暴露接口改變V的顯示數據和狀態,P通過V的事件回調來執行自身的業務邏輯
- 讓V持有P,V通過P的代理回調來改變自身的顯示數據和狀態,V直接調用P的接口來執行事件響應對應的業務邏輯
第一種方式保持了view的純粹,只是作為被動view來展示數據和更改狀態,但是卻導致了P耦合了V,這樣業務邏輯和業務展示有糅合到了一起,和上面的MVC一樣了。
第二種方式保證了P的純粹,讓P只做業務邏輯,至于業務邏輯引發的數據顯示的變化,讓view實現對應的代理事件來實現即可。這增加了view的復雜和view對于P的耦合。
Demo中采用了第二種方式,但是demo中的view依賴是具體的presenter,如果是一個view對應多個presenter,那么可以考慮把presenter暴露的方法和屬性抽象成protocol。讓view依賴抽象而不是具體實現。
3、被動式圖模式的MVP
目前常見的 MVP 架構模式其實都是它的變種:Passive View 和 Supervising Controller。我們先來開下第一種,也是用的比較多的一種
MVP 的第一個主要變種就是被動視圖(Passive View);顧名思義,在該變種的架構模式中,視圖層是被動的,它本身不會改變自己的任何的狀態,它只是定義控價的樣式和布局,本身是沒有任何邏輯的。
然后對外暴露接口,外界通過這些接口來渲染數據到view來顯示,所有的狀態都是通過 Presenter 來間接改變的(一般都是在view里面實現Presenter的代理來改變的)。這樣view可以最大程度被復用,可測試性也大大提高
可以參考這篇文章Passive View
通信方式
- 當視圖接收到來自用戶的事件時,會將事件轉交給 Presenter 進行處理;
- 被動的視圖實現presentr的代理,當需要更新視圖時 Presenter回調代理來更新視圖的內容,這樣讓presenter專注于業務邏輯,view專注于顯示邏輯
- Presenter 負責對模型進行操作和更新,在需要時取出其中存儲的信息;
- 當模型層改變時,可以將改變的信息發送給觀察者 Presenter;
4、監督控制器模式的MVP
在監督控制器中,視圖層接管了一部分視圖邏輯,主要就是同步簡單的視圖和模型的狀態;而監督控制器就需要負責響應用戶的輸入以及一部分更加復雜的視圖、模型狀態同步工作。
對于用戶輸入的處理,監督控制器的做法與標準 MVP 中的 Presenter 完全相同。但是對于視圖、模型的數據同步工作,使用類似于下面要講到MVVM中的雙向綁定機制來實現二者的相互映射。
如下圖所示:
監督控制器中的視圖和模型層之間增加了兩者之間的耦合,也就增加了整個架構的復雜性。和被動式圖的MVP不同的是:視圖和模型之間新增了的依賴,就是雙向的數據綁定;視圖通過聲明式的語法與模型中的簡單屬性進行綁定,當模型發生改變時,會通知其觀察者視圖作出相應的更新。
通過這種方式能夠減輕監督控制器的負擔,減少其中簡單的代碼,將一部分邏輯交由視圖進行處理;這樣也就導致了視圖同時可以被 Presenter 和數據綁定兩種方式更新,相比于被動視圖,監督控制器的方式也降低了視圖的可測試性和封裝性。
可以參考這篇文章Supervising Controller
5、如何構建正確的MVP
MVC的缺點在于并沒有區分業務邏輯和業務展示, 這對單元測試很不友好。 MVP針對以上缺點做了優化, 它將業務邏輯和業務展示也做了一層隔離, 對應的就變成了MVCP。 M和V功能不變, 原來的C現在只負責view的生成和作為view的代理(view的布局依然由SceneVC來完成), 而所有的業務邏輯全都轉移到了P層.
我們用MVP把上面的界面重構一次,架構圖如下所示:
業務場景沒有變化, 依然是展示三種數據, 只是三個MVC替換成了三個MVP(圖中我只畫了Blog模塊), UserVC負責配置三個MVP(新建各自的VP, 通過VP建立C, C會負責建立VP之間的綁定關系), 并在合適的時機通知各自的P層(之前是通知C層)進行數據獲取。
各個P層在獲取到數據后進行相應處理, 處理完成后會通知綁定的View數據有所更新, V收到更新通知后從P獲取格式化好的數據進行頁面渲染, UserVC最后將已經渲染好的各個View進行布局即可.
另外, V層C層不再處理任何業務邏輯, 所有事件觸發全部調用P層的相應命令。
具體代碼大家看demo就行了,下面我抽出點贊功能來對比分析下MVC和MVP的實現有何不同
MVP點贊代碼
blogViewController.m
//點贊事件
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.presenter = self.presenter.allDatas[indexPath.row];//PV綁定
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
[weakCell.presenter likeBlogWithCompletionHandler:^(NSError *error, id result) {
!error ?: [weakCell showToastWithText:error.domain];
}];
}];
return cell;
}
==========================================
BlogCellPresenter.m
- (void)likeBlogWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
if (self.blog.isLiked) {
!completionHandler ?: completionHandler([NSError errorWithDomain:@"你已經贊過了哦~" code:123 userInfo:nil], nil);
} else {
BOOL response = [self.view respondsToSelector:@selector(blogPresenterDidUpdateLikeState:)];
self.blog.isLiked = YES;
self.blog.likeCount += 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
[[UserAPIManager new] likeBlogWithBlogId:self.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
self.blog.isLiked = NO;
self.blog.likeCount -= 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
}
!completionHandler ?: completionHandler(error, result);
}];
}
}
==========================================
BlogViewCell.m
#pragma mark - BlogCellPresenterCallBack
- (void)blogPresenterDidUpdateLikeState:(BlogCellPresenter *)presenter {
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.likeButton setTitleColor:presenter.isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)blogPresenterDidUpdateShareState:(BlogCellPresenter *)presenter {
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}
#pragma mark - Action
- (IBAction)onClickLikeButton:(UIButton *)sender {
!self.didLikeHandler ?: self.didLikeHandler();
}
#pragma mark - Setter
- (void)setPresenter:(BlogCellPresenter *)presenter {
_presenter = presenter;
presenter.view = self;
self.titleLabel.text = presenter.blogTitleText;
self.summaryLabel.text = presenter.blogSummaryText;
self.likeButton.selected = presenter.isLiked;
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}
MVC的點贊功能
blogViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
BlogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.title = cellHelper.blogTitleText;
cell.summary = cellHelper.blogSummaryText;
cell.likeState = cellHelper.isLiked;
cell.likeCountText = cellHelper.blogLikeCountText;
cell.shareCountText = cellHelper.blogShareCountText;
//點贊的業務邏輯
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已經贊過它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//點贊的業務展示
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
return cell;
}
===========================================
BlogViewCell.m
- (IBAction)onClickLikeButton:(UIButton *)sender {
!self.didLikeHandler ?: self.didLikeHandler();
}
#pragma mark - Interface
- (void)setTitle:(NSString *)title {
self.titleLabel.text = title;
}
- (void)setSummary:(NSString *)summary {
self.summaryLabel.text = summary;
}
- (void)setLikeState:(BOOL)isLiked {
[self.likeButton setTitleColor:isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)setLikeCountText:(NSString *)likeCountText {
[self.likeButton setTitle:likeCountText forState:UIControlStateNormal];
}
- (void)setShareCountText:(NSString *)shareCountText {
[self.shareButton setTitle:shareCountText forState:UIControlStateNormal];
}
從上面的代碼對比可以看出來,MVP的代碼量比MVC多出來整整一倍,但是MVP在層次上更加清晰,業務邏輯和業務展示徹底分離,讓presenter和view可以單獨測試,而MVC則把這兩者混在一起,沒法單獨測試。實際項目中大家可以自己根據項目需求來選擇。
下面是MVC下點贊的邏輯
//點贊的業務邏輯
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已經贊過它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//點贊的業務展示
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
可以看到業務邏輯(改變model數據)和業務展示(改變cell的數據)糅雜在一起,如果我要測試點贊這個業務邏輯,那么就必須生成cell,然后點擊cell的按鈕,去觸發點贊的業務邏輯才可以測試
再看看MVP下的點贊邏輯的實現
業務邏輯:
BlogCellPresenter.m
- (void)likeBlogWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
if (self.blog.isLiked) {
!completionHandler ?: completionHandler([NSError errorWithDomain:@"你已經贊過了哦~" code:123 userInfo:nil], nil);
} else {
BOOL response = [self.view respondsToSelector:@selector(blogPresenterDidUpdateLikeState:)];
self.blog.isLiked = YES;
self.blog.likeCount += 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
[[UserAPIManager new] likeBlogWithBlogId:self.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
self.blog.isLiked = NO;
self.blog.likeCount -= 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
}
!completionHandler ?: completionHandler(error, result);
}];
}
}
業務展示:
BlogViewCell.m
#pragma mark - BlogCellPresenterCallBack
- (void)blogPresenterDidUpdateLikeState:(BlogCellPresenter *)presenter {
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.likeButton setTitleColor:presenter.isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)blogPresenterDidUpdateShareState:(BlogCellPresenter *)presenter {
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}
可以看到在MVP里面業務邏輯和業務展示是分在不同的地方實現,那么就可以分開測試二者了,而不想MVC那樣想測試下業務邏輯,還必須生成一個view,這不合理,因為業務邏輯改變的model的數據,和view無關。
MVP相對于MVC, 它其實只做了一件事情, 即分割業務展示和業務邏輯. 展示和邏輯分開后, 只要我們能保證V在收到P的數據更新通知后能正常刷新頁面, 那么整個業務就沒有問題. 因為V收到的通知其實都是來自于P層的數據獲取/更新操作, 所以我們只要保證P層的這些操作都是正常的就可以了. 即我們只用測試P層的邏輯, 不必關心V層的情況
MVVM
1、概述
MVVM是由微軟提出來的,但是這個架構也是在下面這篇文章的基礎上發展起來的:
Martin Fowler 發表的 Presentation Model 文章
這篇文章上面就提到過,就是MVP的原型,也就是說MVVM其實是在MVP的基礎上發展起來的。那么MVVM在MVP的基礎上改良了啥呢?答案就是數據綁定,下面會慢慢鋪開來講。網上關于MVVM的定義太多,沒有一個統一的說法,有的甚至完全相反。關于權威的MVVM解釋,大家可以看下微軟的官方文檔:
里面關于MVVM提出的動機,解決的痛點,各層的職責都解釋的比較清楚。要追本溯源看下MVVM的前世今生,那么上面的Martin Fowler發表的文章也可以看看
2005 年,John Gossman 在他的博客上公布了Introduction to Model/View/ViewModel pattern for building WPF apps 一文。MVVM 與 Martin Fowler 所說的 PM 模式其實是完全相同的,Fowler 提出的 PM 模式是一種與平臺無關的創建視圖抽象的方法,而 Gossman 的 MVVM 是專門用于 WPF 框架來簡化用戶界面的創建的模式;我們可以認為 MVVM 是在 WPF 平臺上對于 PM 模式的實現。
從 Model-View-ViewModel 這個名字來看,它由三個部分組成,也就是 Model、View 和 ViewModel;其中視圖模型(ViewModel)其實就是 MVP 模式中的P,在 MVVM 中叫做VM。
MVVM架構圖:
除了我們非常熟悉的 Model、View 和 ViewModel 這三個部分,在 MVVM 的實現中,還引入了隱式的一個 Binder層,這也是MVVM相對MVP的進步,而聲明式的數據和命令的綁定在 MVVM 模式中就是通過binder層來完成的,RAC是iOS下binder的優雅實現,當然MVVM沒有RAC也完全可以運行。
下圖展示了iOS下的MVC是如何拆分成MVVM的:
MVVM和MVP相對于MVC最大的改進在于:P或者VM創建了一個視圖的抽象,將視圖中的狀態和行為抽離出來形成一個新的抽象。這可以把業務邏輯(P/VM)和業務展示(V)分離開單獨測試,并且達到復用的目的,邏輯結構更加清晰
2、MVVM各層職責
MVVM各層的職責和MVP的類似,VM對應P層,只是在MVVM的View層多了數據綁定的操作
3、MVVM相對于MVP做的改進
上面提到過MVVM相對于MVC的改進是對VM/P和view做了雙向的數據和命令綁定,那么這么做的好處是什么呢?還是看上面MVP的點贊的例子
MVP的點贊邏輯如下:
點擊cell按鈕--->調用P的點贊邏輯---->點贊成功后,P改變M的數據--->P回調Cell的代理方法改變cell的顯示(點贊成功,贊的個數加1,同時點贊數變紅,否則不改變贊的個數也不變色)
上面就是一個事件完整過程,可以看到要通過四步來完成,而且每次都要把P的狀態同步到view,當事件多起來的時候,這樣寫就很麻煩了。那有沒有一種簡單的機制,讓view的行為和狀態和P的行為狀態同步呢?
答案就是MVVM的binder機制。
點贊的MVP的代碼看上面MVP章節即可,我們來看下在MVVM下的點贊如何實現的:
BlogCellViewModel.h
- (BOOL)isLiked;
- (NSString *)blogTitleText;
- (NSString *)blogSummaryText;
- (NSString *)blogLikeCount;
- (NSString *)blogShareCount;
- (RACCommand *)likeBlogCommand;
========================================
BlogCellViewModel.m
@weakify(self);
self.likeBlogCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
RACSubject *subject = [RACSubject subject];
if (self.isLiked) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.isLiked = NO;
self.blogLikeCount = self.blog.likeCount - 1;
[subject sendCompleted];
});
} else {
self.isLiked = YES;
self.blogLikeCount = self.blog.likeCount + 1;
[[UserAPIManager new] likeBlogWithBlogId:self.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
self.isLiked = NO;
self.blogLikeCount = self.blog.likeCount - 1;
}
error ? [subject sendError:error] : [subject sendCompleted];
}];
}
return subject;
}];
- (void)awakeFromNib {
[super awakeFromNib];
//數據綁定操作
@weakify(self);
RAC(self.titleLabel, text) = RACObserve(self, viewModel.blogTitleText);
RAC(self.summaryLabel, text) = RACObserve(self, viewModel.blogSummaryText);
RAC(self.likeButton, selected) = [RACObserve(self, viewModel.isLiked) ignore:nil];
[RACObserve(self, viewModel.blogLikeCount) subscribeNext:^(NSString *title) {
@strongify(self);
[self.likeButton setTitle:title forState:UIControlStateNormal];
}];
[RACObserve(self, viewModel.blogShareCount) subscribeNext:^(NSString *title) {
@strongify(self);
[self.shareButton setTitle:title forState:UIControlStateNormal];
}];
}
- (IBAction)onClickLikeButton:(UIButton *)sender {
//事件響應
if (!self.viewModel.isLiked) {
[[self.viewModel.likeBlogCommand execute:nil] subscribeError:^(NSError *error) {
[self showToastWithText:error.domain];
}];
} else {
[self showAlertWithTitle:@"提示" message:@"確定取消點贊嗎?" confirmHandler:^(UIAlertAction *confirmAction) {
[[self.viewModel.likeBlogCommand execute:nil] subscribeError:^(NSError *error) {
[self showToastWithText:error.domain];
}];
}];
}
}
可以看到相對MVP的view觸發P的業務邏輯,然后P再回調改變View的顯示的操作,使用MVVM的數據綁定來實現讓邏輯更加清晰,代碼也更少。這就是MVVM相對于MVP的改進之處
VIPER
1、概述
前面講到的幾個架構大多脫胎于MVC,但是VIPER和MVC沒有啥關系,是一個全新的架構。從一點就可以看出來:前面幾個MVX框架在iOS下是無法擺脫Apple的viewcontroller影響的,但是VIPER徹底弱化了VC的概念,讓VC變成了真正意義上的View。把VC的職責進行了徹底的拆分,分散到各個子層里面了
下圖就是VIPER的架構圖
從上面可以看出VIPER應該是所有架構里面職責劃分最為明確的,真正做到了SOLID原則。其他架構因為有VC的存在,或多或少都會導致各層的職責劃分不明確。但是也由于VIPER的分層過多,并且是唯一一個把界面路由功能單獨分離出來放到一個單獨的類里面處理,所有的事件響應和界面跳轉都需要自己處理,這導致代碼復雜度大大增加。
Apple苦心孤詣的給我們搞出一個VC,雖然會導致層次耦合,但是也確實簡化了開發流程,而VIPER則是徹底拋棄了VC,重新進行分層,做到了每個模塊都可以單獨測試和復用,但是也導致了代碼過多、邏輯比較繞的問題。
就我個人經驗來說,其實只要做好分層和規劃,MVC架構足夠應付大多數場景。有些文章上來就說MVVM是為了解決C層臃腫, MVC難以測試的問題, 其實并不是這樣的. 按照架構演進順序來看, C層臃腫大部分是沒有拆分好MVC模塊, 好好拆分就行了, 用不著MVVM。 而MVC難以測試也可以用MVP來解決, 只是MVP也并非完美, 在VP之間的數據交互太繁瑣, 所以才引出了MVVM。 而VIPER則是跳出了MVX架構,自己開辟一條新的路。
VIPER是非常干凈的架構。它將每個模塊與其他模塊隔離開來。因此,更改或修復錯誤非常簡單,因為您只需要更新特定的模塊。此外,VIPER還為單元測試創建了一個非常好的環境。由于每個模塊獨立于其他模塊,因此保持了低耦合。在開發人員之間劃分工作也很簡單。
不應該在小項目中使用VIPER,因為MVP或MVC就足夠了
關于到底是否應該在項目中使用VIPER,大家可以看下Quora上面的討論:
Should I use Viper architecture for my next iOS application, or it is still very new to use?
2、VIPER各層職責
- Interactor(交互器) - 這是應用程序的主干,因為它包含應用程序中用例描述的業務邏輯。交互器負責從數據層獲取數據,并執行特定場景下的業務邏輯,其實現完全獨立于用戶界面。
- Presenter(展示器) - 它的職責是從用戶操作的Interactor獲取數據,創建一個Entities實例,并將其傳送到View以顯示它。
- Entities(實體) - 純粹的數據對象。不包括數據訪問層,因為這是 Interactor 的職責。
- Router(路由) - 負責 VIPER 模塊之間的跳轉
- View(視圖)- 視圖的責任是將用戶操作發送給演示者,并顯示presenter告訴它的任何內容
PS:
數據的獲取應該單獨放到一個層,而不應該放到Interactor里面
可以看到一個應用場景的所有功能點都被分離成功能完全獨立的層,每個層的職責都是單一的。在VIPER架構中,每個塊對應于具有特定任務,輸入和輸出的對象。它與裝配線中的工作人員非常相似:一旦工作人員完成其對象上的作業,該對象將傳遞給下一個工作人員,直到產品完成。
層之間的連接表示對象之間的關系,以及它們彼此傳遞的信息類型。通過協議給出從一個實體到另一個實體的通信。
這種架構模式背后的想法是隔離應用程序的依賴關系,平衡實體之間的責任分配。基本上,VIPER架構將您的應用程序邏輯分為較小的功能層,每個功能都具有嚴格的預定責任。這使得更容易測試層之間邊界的交互。它適用于單元測試,并使您的代碼更可重用。
3、VIPER 架構的主要優點
- 簡化復雜項目。由于模塊獨立,VIPER對于大型團隊來說真的很好。
- 使其可擴展。使開發人員盡可能無縫地同時處理它
- 代碼達到了可重用性和可測試性
- 根據應用程序的作用劃分應用程序組件,設定明確的責任
- 可以輕松添加新功能
- 由于您的UI邏輯與業務邏輯分離,因此可以輕松編寫自動化測試
- 它鼓勵分離使得更容易采用TDD的關注。Interactor包含獨立于任何UI的純邏輯,這使得通過測試輕松開車
- 創建清晰明確的接口,獨立于其他模塊。這使得更容易更改界面向用戶呈現各種模塊的方式。
- 通過單一責任原則,通過崩潰報告更容易地跟蹤問題
- 使源代碼更清潔,更緊湊和可重用
- 減少開發團隊內的沖突數量
- 適用SOLID原則
- 使代碼看起來類似。閱讀別人的代碼變得更快。
VIPER架構有很多好處,但重要的是要將其用于大型和復雜的項目。由于所涉及的元素數量,這種架構在啟動新的小型項目時會導致開銷,因此VIPER架構可能會對無意擴展的小型項目造成過高的影響。因此,對于這樣的項目,最好使用別的東西,例如MVC。
4、如何構建正確的VIPER
我們來構建一個小的VIPER應用,我不想把上面的demo用VIPER再重寫一次了,因為太麻煩了,所以就寫一個簡單的demo給大家演示下VIPER,但是麻雀雖小五臟俱全,該有的功能都有了。
如上圖所示,有兩個界面contactlist和addcontact,在contactlist的右上角點擊添加按鈕,跳轉到addcontact界面,輸入firstname和secondname后點擊done按鈕,回到contactlist界面,新添加的用戶就顯示在該界面上了。
先看下項目的架構,如下所示:
可以看到每個界面都有6個文件夾,還有兩個界面公用的Entities文件夾,每個文件夾對應一個分層,除了VIPER的五層之外,每個界面還有兩個文件夾:Protocols和DataManager層。
Protocols定義的VIPER的每層需要遵守的協議,每層對外暴露的操作都經過protocol抽象了,這樣可以針對抽象編程。DataManager定義的是數據操作,包括從本地和網絡獲取、存儲數據的操作。
下面先來看看Protocols類的實現:
import UIKit
/**********************PRESENTER OUTPUT***********************/
// PRESENTER -> VIEW
protocol ContactListViewProtocol: class {
var presenter: ContactListPresenterProtocol? { get set }
func didInsertContact(_ contact: ContactViewModel)
func reloadInterface(with contacts: [ContactViewModel])
}
// PRESENTER -> router
protocol ContactListRouterProtocol: class {
static func createContactListModule() -> UIViewController
func presentAddContactScreen(from view: ContactListViewProtocol)
}
//PRESENTER -> INTERACTOR
protocol ContactListInteractorInputProtocol: class {
var presenter: ContactListInteractorOutputProtocol? { get set }
var localDatamanager: ContactListLocalDataManagerInputProtocol? { get set }
func retrieveContacts()
}
/**********************INTERACTOR OUTPUT***********************/
// INTERACTOR -> PRESENTER
protocol ContactListInteractorOutputProtocol: class {
func didRetrieveContacts(_ contacts: [Contact])
}
//INTERACTOR -> LOCALDATAMANAGER
protocol ContactListLocalDataManagerInputProtocol: class {
func retrieveContactList() throws -> [Contact]
}
/**********************VIEW OUTPUT***********************/
// VIEW -> PRESENTER
protocol ContactListPresenterProtocol: class {
var view: ContactListViewProtocol? { get set }
var interactor: ContactListInteractorInputProtocol? { get set }
var wireFrame: ContactListRouterProtocol? { get set }
func viewDidLoad()
func addNewContact(from view: ContactListViewProtocol)
}
其實從該類中就可以清晰看到VIPER各層之間的數據流向,非常清晰。
然后就是各層去具體實現這些協議了,這里就不貼代碼了,大家可以去demo里面看。下面主要講一下路由層,這是VIPER所獨有的,其他的MVX架構都是把路由放到了VC里面做,而VIPER架構因為徹底摒棄了VC,所以把界面之間的路由單獨做了一層。
下面來具體看看
ContactListRouter
import UIKit
class ContactListRouter: ContactListRouterProtocol {
//生成ContactList的View
class func createContactListModule() -> UIViewController {
let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController")
if let view = navController.childViewControllers.first as? ContactListView {
let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter()
let interactor: ContactListInteractorInputProtocol = ContactListInteractor()
let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager()
let router: ContactListRouterProtocol = ContactListRouter()
//綁定VIPER各層
view.presenter = presenter
presenter.view = view
presenter.wireFrame = router
presenter.interactor = interactor
interactor.presenter = presenter
interactor.localDatamanager = localDataManager
return navController
}
return UIViewController()
}
//導航到AddContact界面
func presentAddContactScreen(from view: ContactListViewProtocol) {
guard let delegate = view.presenter as? AddModuleDelegate else {
return
}
let addContactsView = AddContactRouter.createAddContactModule(with: delegate)
if let sourceView = view as? UIViewController {
sourceView.present(addContactsView, animated: true, completion: nil)
}
}
static var mainStoryboard: UIStoryboard {
return UIStoryboard(name: "Main", bundle: Bundle.main)
}
}
ContactListRouter有三個功能:
- 生成ContactList的view
- 綁定ContactList場景下VIPER各層
- 路由到AddContact界面
第一個功能被APPDelegate調用:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let contactsList = ContactListRouter.createContactListModule()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = contactsList
window?.makeKeyAndVisible()
return true
}
第二個功能點擊ContactList的界面的右上角添加按鈕調用:
class ContactListView: UIViewController {
var presenter: ContactListPresenterProtocol?
//點擊添加按鈕,調用presenter的對應業務邏輯
@IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) {
presenter?.addNewContact(from: self)
}
}
=================
//presenter實現添加按鈕的業務邏輯,調用router的跳轉邏輯,調到AddContact界面
class ContactListPresenter: ContactListPresenterProtocol {
weak var view: ContactListViewProtocol?
var interactor: ContactListInteractorInputProtocol?
var router: ContactListRouterProtocol?
func addNewContact(from view: ContactListViewProtocol) {
router?.presentAddContactScreen(from: view)
}
}
同樣的AddContact的router層的功能也類似,大家可以自己去領會。從上面的代碼可以看到VIPER架構的最大特點就是實現了SOLID原則,每層只做自己的事情,職責劃分的非常清楚,自己的任務處理完后就交給下一個層處理。
看完上面的代碼是不是覺得這也太繞了吧,是的,我也這么覺得,但是不得不說VIPER的優點也有很多,上面已經列舉了。所以如果是中小型的項目,還是用MVX架構吧,如果MVX架構依然hold不住你的每個類都在膨脹,那么試試VIPER你可能會有新的發現。
其實我倒覺得VIPER徹底放棄Apple的VC有點得不償失,個人還是喜歡用VC來做界面路由,而不是單獨搞一個router層去路由,這樣既借鑒了VIPER的優點,有兼顧了VC的好處,具體的看最后的demo,我這里就不展開說了,大家做一個對比應該就有了解。
5、VIPER參考書籍
號稱是唯一一本介紹VIPER的書籍,然而完整版只有俄語的,不過我們有萬能的谷歌翻譯,只要不是火星文都可以看啦~
6、VIPER代碼模板生成器
由于VIPER架構的類比較多,還要寫一堆模塊之間的協議,如果每次都要手寫的話,太心累了~ 所以大家可以試試下面的代碼生成器,一次生成VIPER的代碼模板