前言
看了下上篇博客的發(fā)表時間到這篇博客诡壁,竟然過了11個月,罪過荠割,罪過妹卿。這一年時間也是夠折騰的,年初離職跳槽到鵝廠蔑鹦,單獨負(fù)責(zé)一個社區(qū)項目夺克,忙的天昏地暗,忙的差不多了嚎朽,轉(zhuǎn)眼就到了7月铺纽。
七月流火,心也跟著燥熱起來了哟忍,眼瞅著移動端這發(fā)展趨勢從05年開始就一直在走下坡路了狡门,想著再這么下去不行,得找條后路備著魁索。網(wǎng)上看了看融撞,覺得前端不錯,最近炒的挺火熱的粗蔚,那就學(xué)學(xué)看吧尝偎,買了html,css,js的幾本書致扯,花了個把月的閑暇時間看完了肤寝,順便做了幾個demo毅该,突然覺得好無聊阀圾。大概是iOS也是寫界面,前端還是寫界面脓斩,寫的有些麻木了耍群。之前一直有學(xué)Python义桂,寫過一些爬蟲、用Django也寫過后臺蹈垢,感覺還挺好玩的慷吊,Python在大數(shù)據(jù)和AI領(lǐng)域也大放異彩,想借此機會學(xué)學(xué)曹抬。
雖然這兩個領(lǐng)域進入門檻比較高溉瓶,但是就目前發(fā)展勢頭來看,應(yīng)該是一個發(fā)展趨勢谤民,互聯(lián)網(wǎng)過去十年的浪潮是移動互聯(lián)網(wǎng)堰酿,下一個十年的浪潮很可能就是AI了。所以早做準(zhǔn)備张足,從零開始學(xué)吧触创。其實干程序員這行,焦慮是無法避免的兢榨,因為自己那點知識儲備和日新月異的技術(shù)發(fā)展相比起來嗅榕,簡直滄海一粟,不由讓人感嘆:吾生也有涯吵聪,而學(xué)無涯凌那。
很多人都在追逐新技術(shù)的過程中迷失了自己,越學(xué)越焦慮吟逝,因為發(fā)現(xiàn)自己無論怎么學(xué)帽蝶,都趕不上技術(shù)發(fā)展的腳步。我倒覺得如其去追逐那些還不知道能不能落地的新技術(shù)块攒,還不如扎扎實實打好基本功励稳,比如系統(tǒng)、數(shù)據(jù)結(jié)構(gòu)囱井、算法驹尼、網(wǎng)絡(luò),新技術(shù)層出不窮庞呕,亂花漸入迷人眼新翎,但是歸根到底也是在這些基礎(chǔ)知識上面建立起來的程帕。
關(guān)于如何學(xué)習(xí),有時間咱們單獨開一篇聊聊地啰。下面進入今天正題愁拭,聊一聊在iOS開發(fā)領(lǐng)域里面幾大架構(gòu)的應(yīng)用,包括MVC亏吝、MVP岭埠、MVVM、VIPER蔚鸥,做iOS開發(fā)一般都是比較熟悉MVC的惜论,因為Apple已經(jīng)為我們量身定制了適合iOS開發(fā)的MVC架構(gòu)。
但是在寫代碼的過程中大家肯定會有這些疑問:為什么我的VC越來越大株茶,為什么感覺apple的MVC怪怪的不像真正的MVC来涨,網(wǎng)絡(luò)請求邏輯到底放在哪層,網(wǎng)上很火熱的MVVM是否值得學(xué)習(xí)启盛,VIPER又是什么鬼?
我希望下面的文字能為大家解除這些疑惑技羔,我會在多個維度對這幾個框架進行對比分析僵闯,指出他們的優(yōu)劣,然后結(jié)合一個具體的DEMO用不同的架構(gòu)去實現(xiàn)藤滥,讓大家對這些架構(gòu)有一個直觀的了解鳖粟。當(dāng)然這些都只是做拋磚引玉之用,闡述的也是我的個人理解拙绊,如有錯誤向图,歡迎指出,大家一起探討進步~~
MVC
1标沪、MVC的理想模型
MVC的理想模型如下圖所示:
各層的職責(zé)如下所示:
- Models:?數(shù)據(jù)層榄攀,負(fù)責(zé)數(shù)據(jù)的處理和獲取的數(shù)據(jù)接口層。
- Views: 展示層(GUI)金句,對于 iOS 來說所有以 UI 開頭的類基本都屬于這層檩赢。
- Controller: 控制器層,它是 Model 和 View 之間的膠水或者說是中間人违寞。一般來說贞瞒,當(dāng)用戶對 View 有操作時它負(fù)責(zé)去修改相應(yīng) Model;當(dāng) Model 的值發(fā)生變化時它負(fù)責(zé)去更新對應(yīng) View趁曼。
如上圖所示军浆,M和View應(yīng)該是完全隔離的,由C作為中間人來負(fù)責(zé)二者的交互挡闰,同時三者是完全獨立分開的乒融,這樣可以保證M和V的可測試性和復(fù)用性,但是一般由于C都是為特別的應(yīng)用場景下的M和V做中介者,所以很難復(fù)用簇抵。
2庆杜、MVC在iOS里面的實現(xiàn)
但是實際上在iOS里面MVC的實現(xiàn)方式很難做到如上所述的那樣,因為由于Apple的規(guī)范碟摆,一個界面的呈現(xiàn)都需要構(gòu)建一個viewcontroller晃财,而每個viewcontroller都帶有一個根view,這就導(dǎo)致C和V緊密耦合在一起構(gòu)成了iOS里面的C層典蜕,這明顯違背了MVC的初衷断盛。
apple里面的MVC真正寫起來大概如下圖所示:
這也是massive controller的由來,具體的下面再講
那么apple為什么要這么干呢愉舔?完整的可以參考下apple對于MVC的解釋钢猛,下面的引用是我摘自其中一段。簡單來說就是iOS里面的viewcontroller其實是view和controller的組合轩缤,目的就是為了提高開發(fā)效率命迈,簡化操作。
摘自上面的鏈接
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結(jié)構(gòu)確實可以提高開發(fā)效率壶愤,但是一旦需要構(gòu)建復(fù)雜界面,那么viewcontroller很容易就會出現(xiàn)代碼膨脹馏鹤,邏輯滿天飛的問題征椒。
另外我想說一句,apple搞出viewcontroller(VC)這么個玩意初衷可能是好的湃累,寫起來方便勃救,提高開發(fā)效率嘛。確實應(yīng)付簡單頁面沒啥問題治力,但是有一個很大的弊端就是容易把新手代入歧途蒙秒,認(rèn)為真正的MVC就是這么干的,導(dǎo)致很多新手都把本來view層的代碼都堆到了VC琴许,比如在VC里面構(gòu)建view税肪、view的顯示邏輯,甚至在VC里面發(fā)起網(wǎng)絡(luò)請求榜田。
這也是我當(dāng)初覺得VC很怪異的一個地方益兄,因為它沒辦法歸類到MVC的任何一層,直到看到了apple文檔的那段話箭券,才知道VC原來是個組合體净捅。
下面來談?wù)劕F(xiàn)有iOS架構(gòu)下MVC各層的職責(zé),這里要注意下辩块,下面的Controller層指的是iOS里面的VC組合體
3蛔六、iOS的MVC各層職責(zé)
controller層(VC):
- 生成view荆永,然后組裝view
- 響應(yīng)View的事件和作為view的代理
- 調(diào)用model的數(shù)據(jù)獲取接口,拿到返回數(shù)據(jù)国章,處理加工具钥,渲染到view顯示
- 處理view的生命周期
- 處理界面之間的跳轉(zhuǎn)
model層:
- 業(yè)務(wù)邏輯封裝
- 提供數(shù)據(jù)接口給controller使用
- 數(shù)據(jù)持久化存儲和讀取
- 作為數(shù)據(jù)模型存儲數(shù)據(jù)
view層:
- 界面元素搭建,動畫效果液兽,數(shù)據(jù)展示骂删,
- 接受用戶操作并反饋視覺效果
PS:
model層的業(yè)務(wù)邏輯一般都是和后臺數(shù)據(jù)交互的邏輯,還有一些抽象的業(yè)務(wù)邏輯四啰,比如格式化日期字符串為NSDateFormatter類型等
4宁玫、massive controller
從上面的MVC各層職責(zé)劃分就可以看出來C干了多少事,這還是做了明確的職責(zé)劃分的情況下柑晒,更不用提新手把各種view和model層的功能都堆到C層后的慘不忍睹欧瘪。
在復(fù)雜界面里面的VC代碼輕松超過千行,我之間就見過超過5000行代碼的VC匙赞,找個方法只能靠搜索佛掖,分分鐘想死的節(jié)奏。
造成massive controller的原因的罪魁禍?zhǔn)拙褪莂pple的把view和Cotroller組合在一起涌庭,讓VC同時做view和C的事苦囱,導(dǎo)致代碼量激增,也違背了MVC原則脾猛。
下面來舉一個簡單的例子,先聲明下我下面列舉的例子主要來著這篇博客:
這篇文章質(zhì)量很高鱼鸠,對三種模式的講解比較深入猛拴,關(guān)鍵還有例子來做橫向?qū)Ρ龋@是其他文章沒有的蚀狰。大家可以先看看這篇文章愉昆,本文的demo來自這篇文章,但是我按照自己的理解在其基礎(chǔ)上做了一些修改麻蹋,大家可以自己對比下跛溉,做出自己的選擇。
還有一些圖也是借鑒該篇文字扮授,在此感謝作者~
先看兩張圖:
這個界面分為三個部分芳室,頂部的個人信息展示,下面有兩張列表刹勃,分別展示博客和草稿內(nèi)容堪侯。
我們先來看看一般新手都是怎么實現(xiàn)的
//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
如果后續(xù)再增加需求,那么userVC的代碼就會越來越多荔仁,這就是我們上面說的massive controller出現(xiàn)了伍宦。維護性和可測試性無從談起芽死,我們是按照apple的MVC架構(gòu)寫的呀,為什么會出現(xiàn)這種問題呢次洼?
暫且按下不表关贵,我們先看另外一個問題,先把這個問題搞清楚了卖毁,對于后續(xù)文章的理解大有裨益揖曾。
5、Model層的誤解
我看到很多所謂的MVC的M層實現(xiàn)就如上面所示势篡,只有幾個干巴巴的屬性翩肌。我之前也是一直這么寫的,但是我一直覺得有疑惑禁悠,覺得這樣寫的話念祭,怎么可能算的上一個單獨的層呢?說是數(shù)據(jù)模型還差不多碍侦。
那么實現(xiàn)正確的M層姿勢應(yīng)該是什么樣的呢粱坤?
大家具體可以看下面這篇文章,對于M層講解的非常不錯瓷产,但是對于文中的MVVM的理解我不敢茍同站玄,大家見仁見智吧
論MVVM偽框架結(jié)構(gòu)和MVC中M的實現(xiàn)機制
下面的引用也是摘自這篇文章:
理解Model層:
首先要正確的理解MVC中的M是什么?他是數(shù)據(jù)模型嗎濒旦?答案是NO株旷。他的正確定義是業(yè)務(wù)模型。也就是你所有業(yè)務(wù)數(shù)據(jù)和業(yè)務(wù)實現(xiàn)邏輯都應(yīng)該定義在M層里面尔邓,而且業(yè)務(wù)邏輯的實現(xiàn)和定義應(yīng)該和具體的界面無關(guān)晾剖,也就是和視圖以及控制之間沒有任何的關(guān)系,它是可以獨立存在的梯嗽,您甚至可以將業(yè)務(wù)模型單獨編譯出一個靜態(tài)庫來提供給第三方或者其他系統(tǒng)使用齿尽。
在上面經(jīng)典MVC圖中也很清晰的描述了這一點: 控制負(fù)責(zé)調(diào)用模型,而模型則將處理結(jié)果發(fā)送通知給控制灯节,控制再通知視圖刷新循头。因此我們不能將M簡單的理解為一個個干巴巴的只有屬性而沒有方法的數(shù)據(jù)模型。
其實這里面涉及到一個最基本的設(shè)計原則炎疆,那就是面向?qū)ο蟮幕驹O(shè)計原則:就是什么是類卡骂?類應(yīng)該是一個個具有不同操作和不同屬性的對象的抽象(類是屬性和方法的集合)。 我想現(xiàn)在任何一個系統(tǒng)里面都沒有出現(xiàn)過一堆只有數(shù)據(jù)而沒有方法的數(shù)據(jù)模型的集合被定義為一個單獨而抽象的模型層來供大家使用吧磷雇。 我們不能把一個保存數(shù)據(jù)模型的文件夾來當(dāng)做一個層偿警,這并不符合橫向切分的規(guī)則。
Model層實現(xiàn)的正確姿勢:
定義的M層中的代碼應(yīng)該和V層和C層完全無關(guān)的唯笙,也就是M層的對象是不需要依賴任何C層和V層的對象而獨立存在的螟蒸。整個框架的設(shè)計最優(yōu)結(jié)構(gòu)是V層不依賴C層而獨立存在盒使,M層不依賴C層和V層獨立存在,C層負(fù)責(zé)關(guān)聯(lián)二者七嫌,V層只負(fù)責(zé)展示少办,M層持有數(shù)據(jù)和業(yè)務(wù)的具體實現(xiàn),而C層則處理事件響應(yīng)以及業(yè)務(wù)的調(diào)用以及通知界面更新诵原。三者之間一定要明確的定義為單向依賴英妓,而不應(yīng)該出現(xiàn)雙向依賴
M層要完成對業(yè)務(wù)邏輯實現(xiàn)的封裝,一般業(yè)務(wù)邏輯最多的是涉及到客戶端和服務(wù)器之間的業(yè)務(wù)交互绍赛。M層里面要完成對使用的網(wǎng)絡(luò)協(xié)議(HTTP, TCP蔓纠,其他)、和服務(wù)器之間交互的數(shù)據(jù)格式(XML, JSON,其他)吗蚌、本地緩存和數(shù)據(jù)庫存儲(COREDATA, SQLITE,其他)等所有業(yè)務(wù)細(xì)節(jié)的封裝腿倚,而且這些東西都不能暴露給C層。所有供C層調(diào)用的都是M層里面一個個業(yè)務(wù)類所提供的成員方法來實現(xiàn)蚯妇。也就是說C層是不需要知道也不應(yīng)該知道和客戶端和服務(wù)器通信所使用的任何協(xié)議敷燎,以及數(shù)據(jù)報文格式,以及存儲方面的內(nèi)容箩言。這樣的好處是客戶端和服務(wù)器之間的通信協(xié)議硬贯,數(shù)據(jù)格式,以及本地存儲的變更都不會影響任何的應(yīng)用整體框架陨收,因為提供給C層的接口不變饭豹,只需要升級和更新M層的代碼就可以了。比如說我們想將網(wǎng)絡(luò)請求庫從ASI換成AFN就只要在M層變化就可以了务漩,整個C層和V層的代碼不變墨状。
文章還給出了實現(xiàn)的例子,我就不粘貼過來了菲饼,大家自己過去看看
總結(jié)來說:
M層不應(yīng)該是數(shù)據(jù)模型,放幾個屬性就完事了列赎。而應(yīng)該是承載業(yè)務(wù)邏輯和數(shù)據(jù)存儲獲取的職責(zé)一層宏悦。
6、如何構(gòu)建構(gòu)建正確的MVC
現(xiàn)在我們來看看到底該如何在iOS下面構(gòu)建一個正確的MVC呢包吝?
首先先達成一個共識:viewcontroller不是C層饼煞,而是V和C兩層的混合體。
我們看到在標(biāo)準(zhǔn)的iOS下的MVC實現(xiàn)里面诗越,C層做了大部分事情砖瞧,大體分為五個部分(見上面MVC各層職責(zé)),因為他是兩個層的混合嚷狞,為了給VC減負(fù)块促,我們現(xiàn)在把VC只當(dāng)做一個view的容器來使用荣堰。
這里我要解釋下什么叫做view的容器,我們知道apple的VC有一個self.view竭翠,所有要顯示在界面的上面的view都必須通過addsubview來添加到這個根view上面來振坚。同時VC還控制著view的生命周期。那么我們可不可以把VC看成一個管理各個View的容器斋扰?
大家可以看這篇文章加深理解下我上面說的view container的概念:
iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案
此時VC的職責(zé)簡化為如下三條職責(zé):
- 生成子view并添加到自己的self.view上面
- 管理view的生命周期
- 通知每個子C去獲取數(shù)據(jù)
前面兩點很好理解吧渡八,上面已經(jīng)講過了。第三點我們接著往下看
消失的C層
回到我們上面說的第四點的例子传货,什么原因造成VC的代碼越來越臃腫呢屎鳍?
因為我們對于view和model層的職責(zé)都劃分的比較清楚,前者負(fù)責(zé)數(shù)據(jù)展示问裕,后者負(fù)責(zé)數(shù)據(jù)獲取逮壁,那么那些模棱兩可的代碼,放在這兩層感覺都不合適僻澎,就都丟到了VC里面貌踏,導(dǎo)致VC日益膨脹。
此時的代碼組織如下圖所示:
通過這張圖可以發(fā)現(xiàn), 用戶信息頁面(userVC)作為業(yè)務(wù)場景Scene需要展示多種數(shù)據(jù)M(Blog/Draft/UserInfo), 所以對應(yīng)的有多個View(blogTableView/draftTableView/image…), 但是, 每個MV之間并沒有一個連接層C, 本來應(yīng)該分散到各個C層處理的邏輯全部被打包丟到了Scene(userVC)這一個地方處理, 也就是M-C-V變成了MM…-Scene-…VV, C層就這樣莫名其妙的消失了.
另外, 作為V的兩個cell直接耦合了M(blog/draft), 這意味著這兩個V的輸入被綁死到了相應(yīng)的M上, 復(fù)用無從談起.
最后, 針對這個業(yè)務(wù)場景的測試異常麻煩, 因為業(yè)務(wù)初始化和銷毀被綁定到了VC的生命周期上, 而相應(yīng)的邏輯也關(guān)聯(lián)到了和View的點擊事件, 測試只能Command+R, 點點點…
那么怎么實現(xiàn)正確的MVC呢窟勃?
如下圖所示祖乳,該界面的信息分為三部分:個人信息、博客列表信息秉氧、草稿列表信息眷昆。我們應(yīng)該也按照這三部分分成三個小的MVC,然后通過VC拼接組裝這三個子MVC來完成整個界面汁咏。
具體代碼組織架構(gòu)如下:
UserVC作為業(yè)務(wù)場景, 需要展示三種數(shù)據(jù), 對應(yīng)的就有三個MVC, 這三個MVC負(fù)責(zé)各自模塊的數(shù)據(jù)獲取, 數(shù)據(jù)處理和數(shù)據(jù)展示, 而UserVC需要做的就是配置好這三個MVC, 并在合適的時機通知各自的C層進行數(shù)據(jù)獲取, 各個C層拿到數(shù)據(jù)后進行相應(yīng)處理, 處理完成后渲染到各自的View上, UserVC最后將已經(jīng)渲染好的各個View進行布局即可
具體的代碼見最后的demo里面MVC文件夾亚斋。
關(guān)于demo的代碼,我想說明一點自己的看法:在demo里面網(wǎng)絡(luò)數(shù)據(jù)的獲取攘滩,作者放到了一個單獨的文件UserAPIManager
里面帅刊。我覺得最好是放在和業(yè)務(wù)相關(guān)的demo里面,因為接口一旦多起來漂问,一個文件很容易膨脹赖瞒,如果按照業(yè)務(wù)分為多個文件,那么還不如干脆放在model里面更加清晰蚤假。
PS:
圖中的blogTableViewHelper對應(yīng)代碼中的blogTableViewController灭袁,其他幾個helper同樣的
此時作為VC的userVC只需要做三件事:
- 生成子view并添加到自己的self.view上面
- 管理view的生命周期
- 通知每個子C去獲取數(shù)據(jù)
userVC的代碼大大減少殿遂,而且此時邏輯更加清楚,而且因為每個模塊的展示和交互是自管理的, 所以userVC只需要負(fù)責(zé)和自身業(yè)務(wù)強相關(guān)的部分即可。
另外如果需要在另外一個VC上面展示博客列表數(shù)據(jù)鳖藕,那么只需要把博客列表的view添加到VC的view上面芙委,然后通過博客列表的controller獲取下數(shù)據(jù)就可以了,這樣就達到了復(fù)用的目的。
我們通過上面的方法箍土,把userVC里面的代碼分到了三個子MVC里面,架構(gòu)更加清晰明了泵殴,對于更加復(fù)雜的頁面涮帘,我們可以做更細(xì)致的分解,同時每個子MVC其實還可以拆分成更細(xì)的MVC笑诅。具體的拆分粒度大家視頁面復(fù)雜度靈活變通调缨,如果預(yù)計到一個頁面的業(yè)務(wù)邏輯后續(xù)會持續(xù)增加,還不如剛開始就拆分成不同的子MVC去實現(xiàn)吆你。如果只是簡單的頁面弦叶,那么直接把所有的邏輯都寫到VC里面也沒事。
7妇多、MVC優(yōu)缺點
優(yōu)點
上面的MVC改造主要是把VC和C加以區(qū)分伤哺,讓MVC成為真正的MVC,而不是讓VC當(dāng)成C來用者祖,經(jīng)過改造后的MVC對付一般場景應(yīng)該綽綽有余了立莉。不管界面多復(fù)雜,都可以拆分成更小的MVC然后再組裝起來七问。
寫代碼就是一個不斷重構(gòu)的過程蜓耻,當(dāng)項目越來越大,單獨功能可以抽離出來作為一個大模塊械巡,打包成pod庫(這個是組件化相關(guān)的知識點刹淌,后面我也會寫一篇博客)。同時在模塊內(nèi)部你又可以分層拆分讥耗。爭取做到單一原則有勾,不要在一個類里面啥都往里面堆
總結(jié)下MVC的優(yōu)點有如下幾點:
- 代碼復(fù)用: 三個小模塊的V(cell/userInfoView)對外只暴露Set方法, 對M甚至C都是隔離狀態(tài), 復(fù)用完全沒有問題. 三個大模塊的MVC也可以用于快速構(gòu)建相似的業(yè)務(wù)場景(大模塊的復(fù)用比小模塊會差一些, 下文我會說明).
- 代碼臃腫: 因為Scene大部分的邏輯和布局都轉(zhuǎn)移到了相應(yīng)的MVC中, 我們僅僅是拼裝MVC的便構(gòu)建了兩個不同的業(yè)務(wù)場景, 每個業(yè)務(wù)場景都能正常的進行相應(yīng)的數(shù)據(jù)展示, 也有相應(yīng)的邏輯交互, 而完成這些東西, 加空格也就100行代碼左右(當(dāng)然, 這里我忽略了一下Scene的布局代碼).
- 易拓展性: 無論產(chǎn)品未來想加回收站還是防御塔, 我需要的只是新建相應(yīng)的MVC模塊, 加到對應(yīng)的Scene即可.
- 可維護性: 各個模塊間職責(zé)分離, 哪里出錯改哪里, 完全不影響其他模塊. 另外, 各個模塊的代碼其實并不算多, 哪一天即使寫代碼的人離職了, 接手的人根據(jù)錯誤提示也能快速定位出錯模塊.
- 易測試性: 很遺憾, 業(yè)務(wù)的初始化依然綁定在Scene的生命周期中, 而有些邏輯也仍然需要UI的點擊事件觸發(fā), 我們依然只能Command+R, 點點點…
缺點
經(jīng)過上面的改造,MVC架構(gòu)已經(jīng)足夠清晰了古程,按照應(yīng)用場景(一般都是單頁面)進行大的拆分蔼卡,然后在根據(jù)業(yè)務(wù)拆分成小的MVC。不行就接著拆挣磨,拆層菲宴,拆模塊。
但是MVC的最大弊端就是C的代碼沒法復(fù)用趋急,所以能把C層的代碼拆出來就盡量拆,我們來看看現(xiàn)在C層的功能還有哪些了
- 作為View和Model的中介者势誊,從model獲取數(shù)據(jù)呜达,經(jīng)過數(shù)據(jù)加工,渲染到view上面顯示
- 響應(yīng)view的點擊事件粟耻,然后執(zhí)行相應(yīng)的業(yè)務(wù)邏輯
- 作為view的代理和數(shù)據(jù)源
- 暴露接口給SceneVC來驅(qū)動自己獲取數(shù)據(jù)
這就導(dǎo)致一個問題:
業(yè)務(wù)邏輯和業(yè)務(wù)展示強耦合: 可以看到, 有些業(yè)務(wù)邏輯(頁面跳轉(zhuǎn)/點贊/分享…)是直接散落在V層的, 這意味著我們在測試這些邏輯時, 必須首先生成對應(yīng)的V, 然后才能進行測試. 顯然, 這是不合理的. 因為業(yè)務(wù)邏輯最終改變的是數(shù)據(jù)M, 我們的關(guān)注點應(yīng)該在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;
//點贊的業(yè)務(wù)邏輯
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已經(jīng)贊過它了~"];
} 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;
//點贊的業(yè)務(wù)展示
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
return cell;
}
通過代碼可以清晰的看到眉踱,必須生成cell,然后點擊cell上面的點贊按鈕霜威,才可以觸發(fā)點贊的業(yè)務(wù)邏輯谈喳。
但是業(yè)務(wù)邏輯一般改變的model數(shù)據(jù),view只是拿到model的數(shù)據(jù)進行展示「昶茫現(xiàn)在卻把這兩個原本獨立的事情合在一起了婿禽。導(dǎo)致業(yè)務(wù)邏輯沒法單獨測試了。
下面提到的MVP正是為了解決這一問題而誕生的大猛,我們接著往下看扭倾。
MVP
下面關(guān)于MVP文字,有部分文字和圖片摘抄自該文章挽绩,在此感謝作者膛壹,之前忘記放上鏈接,向作者道歉:
淺談 MVC唉堪、MVP 和 MVVM 架構(gòu)模式
1模聋、概述
MVC的缺點在于并沒有區(qū)分業(yè)務(wù)邏輯和業(yè)務(wù)展示, 這對單元測試很不友好. MVP針對以上缺點做了優(yōu)化, 它將業(yè)務(wù)邏輯和業(yè)務(wù)展示也做了一層隔離, 對應(yīng)的就變成了MVCP.
M和V功能不變, 原來的C現(xiàn)在只負(fù)責(zé)布局, 而所有的業(yè)務(wù)邏輯全都轉(zhuǎn)移到了P層。P層處理完了業(yè)務(wù)邏輯唠亚,如果要更改view的顯示链方,那么可以通過回調(diào)來實現(xiàn),這樣可以減輕耦合趾撵,同時可以單獨測試P層的業(yè)務(wù)邏輯
MVP的變種及定義比較多侄柔,但是最終廣為人知的是Martin Fowler 的發(fā)表的關(guān)于Presentation Model描述,也就是下面將要介紹的MVP占调。具體看下面這篇文章:
Martin Fowler 發(fā)表的 Presentation Model 文章
MVP從視圖層中分離了行為(事件響應(yīng))和狀態(tài)(屬性暂题,用于數(shù)據(jù)展示),它創(chuàng)建了一個視圖的抽象究珊,也就是presenter層薪者,而視圖就是P層的『渲染』結(jié)果。P層中包含所有的視圖渲染需要的動態(tài)信息剿涮,包括視圖的內(nèi)容(text言津、color)、組件是否啟用(enable)取试,除此之外還會將一些方法暴露給視圖用于某些事件的響應(yīng)悬槽。
2、MVP架構(gòu)和各層職責(zé)對比
MVP的架構(gòu)圖如下所示:
在 MVP 中瞬浓,Presenter 可以理解為松散的控制器初婆,其中包含了視圖的 UI 業(yè)務(wù)邏輯,所有從視圖發(fā)出的事件,都會通過代理給 Presenter 進行處理磅叛;同時屑咳,Presenter 也通過視圖暴露的接口與其進行通信。
各層職責(zé)如下
VC層
- view的布局和組裝
- view的生命周期控制
- 通知各個P層去獲取數(shù)據(jù)然后渲染到view上面展示
controller層
- 生成view弊琴,實現(xiàn)view的代理和數(shù)據(jù)源
- 綁定view和presenter
- 調(diào)用presenter執(zhí)行業(yè)務(wù)邏輯
model層
- 和MVC的model層類似
view層
- 監(jiān)聽P層的數(shù)據(jù)更新通知, 刷新頁面展示.(MVC里由C層負(fù)責(zé))
- 在點擊事件觸發(fā)時, 調(diào)用P層的對應(yīng)方法, 并對方法執(zhí)行結(jié)果進行展示.(MVC里由C層負(fù)責(zé))
- 界面元素布局和動畫
- 反饋用戶操作
Presenter層職責(zé)
- 實現(xiàn)view的事件處理邏輯兆龙,暴露相應(yīng)的接口給view的事件調(diào)用
- 調(diào)用model的接口獲取數(shù)據(jù),然后加工數(shù)據(jù)敲董,封裝成view可以直接用來顯示的數(shù)據(jù)和狀態(tài)
- 處理界面之間的跳轉(zhuǎn)(這個根據(jù)實際情況來確定放在P還是C)
我們來分析下View層的職責(zé)紫皇,其中3、4兩點和MVC的view類似臣缀,但是1坝橡、2兩點不同,主要是因為業(yè)務(wù)邏輯從C轉(zhuǎn)移到了P精置,那么view的事件響應(yīng)和狀態(tài)變化肯定就依賴P來實現(xiàn)了计寇。
這里又有兩種不同的實現(xiàn)方式:
- 讓P持有V,P通過V的暴露接口改變V的顯示數(shù)據(jù)和狀態(tài)脂倦,P通過V的事件回調(diào)來執(zhí)行自身的業(yè)務(wù)邏輯
- 讓V持有P番宁,V通過P的代理回調(diào)來改變自身的顯示數(shù)據(jù)和狀態(tài),V直接調(diào)用P的接口來執(zhí)行事件響應(yīng)對應(yīng)的業(yè)務(wù)邏輯
第一種方式保持了view的純粹赖阻,只是作為被動view來展示數(shù)據(jù)和更改狀態(tài)蝶押,但是卻導(dǎo)致了P耦合了V,這樣業(yè)務(wù)邏輯和業(yè)務(wù)展示有糅合到了一起火欧,和上面的MVC一樣了棋电。
第二種方式保證了P的純粹,讓P只做業(yè)務(wù)邏輯苇侵,至于業(yè)務(wù)邏輯引發(fā)的數(shù)據(jù)顯示的變化赶盔,讓view實現(xiàn)對應(yīng)的代理事件來實現(xiàn)即可。這增加了view的復(fù)雜和view對于P的耦合榆浓。
Demo中采用了第二種方式于未,但是demo中的view依賴是具體的presenter,如果是一個view對應(yīng)多個presenter陡鹃,那么可以考慮把presenter暴露的方法和屬性抽象成protocol烘浦。讓view依賴抽象而不是具體實現(xiàn)。
3萍鲸、被動式圖模式的MVP
目前常見的 MVP 架構(gòu)模式其實都是它的變種:Passive View 和 Supervising Controller闷叉。我們先來開下第一種,也是用的比較多的一種
MVP 的第一個主要變種就是被動視圖(Passive View)脊阴;顧名思義握侧,在該變種的架構(gòu)模式中捌肴,視圖層是被動的,它本身不會改變自己的任何的狀態(tài)藕咏,它只是定義控價的樣式和布局,本身是沒有任何邏輯的秽五。
然后對外暴露接口孽查,外界通過這些接口來渲染數(shù)據(jù)到view來顯示,所有的狀態(tài)都是通過 Presenter 來間接改變的(一般都是在view里面實現(xiàn)Presenter的代理來改變的)坦喘。這樣view可以最大程度被復(fù)用盲再,可測試性也大大提高
可以參考這篇文章Passive View
通信方式
- 當(dāng)視圖接收到來自用戶的事件時,會將事件轉(zhuǎn)交給 Presenter 進行處理瓣铣;
- 被動的視圖實現(xiàn)presentr的代理答朋,當(dāng)需要更新視圖時 Presenter回調(diào)代理來更新視圖的內(nèi)容,這樣讓presenter專注于業(yè)務(wù)邏輯棠笑,view專注于顯示邏輯
- Presenter 負(fù)責(zé)對模型進行操作和更新梦碗,在需要時取出其中存儲的信息;
- 當(dāng)模型層改變時蓖救,可以將改變的信息發(fā)送給觀察者 Presenter洪规;
4、監(jiān)督控制器模式的MVP
在監(jiān)督控制器中循捺,視圖層接管了一部分視圖邏輯斩例,主要就是同步簡單的視圖和模型的狀態(tài);而監(jiān)督控制器就需要負(fù)責(zé)響應(yīng)用戶的輸入以及一部分更加復(fù)雜的視圖从橘、模型狀態(tài)同步工作念赶。
對于用戶輸入的處理,監(jiān)督控制器的做法與標(biāo)準(zhǔn) MVP 中的 Presenter 完全相同恰力。但是對于視圖叉谜、模型的數(shù)據(jù)同步工作,使用類似于下面要講到MVVM中的雙向綁定機制來實現(xiàn)二者的相互映射牺勾。
如下圖所示:
監(jiān)督控制器中的視圖和模型層之間增加了兩者之間的耦合正罢,也就增加了整個架構(gòu)的復(fù)雜性。和被動式圖的MVP不同的是:視圖和模型之間新增了的依賴驻民,就是雙向的數(shù)據(jù)綁定翻具;視圖通過聲明式的語法與模型中的簡單屬性進行綁定,當(dāng)模型發(fā)生改變時回还,會通知其觀察者視圖作出相應(yīng)的更新裆泳。
通過這種方式能夠減輕監(jiān)督控制器的負(fù)擔(dān),減少其中簡單的代碼柠硕,將一部分邏輯交由視圖進行處理工禾;這樣也就導(dǎo)致了視圖同時可以被 Presenter 和數(shù)據(jù)綁定兩種方式更新运提,相比于被動視圖,監(jiān)督控制器的方式也降低了視圖的可測試性和封裝性闻葵。
可以參考這篇文章Supervising Controller
5民泵、如何構(gòu)建正確的MVP
MVC的缺點在于并沒有區(qū)分業(yè)務(wù)邏輯和業(yè)務(wù)展示, 這對單元測試很不友好。 MVP針對以上缺點做了優(yōu)化, 它將業(yè)務(wù)邏輯和業(yè)務(wù)展示也做了一層隔離, 對應(yīng)的就變成了MVCP槽畔。 M和V功能不變, 原來的C現(xiàn)在只負(fù)責(zé)view的生成和作為view的代理(view的布局依然由SceneVC來完成), 而所有的業(yè)務(wù)邏輯全都轉(zhuǎn)移到了P層.
我們用MVP把上面的界面重構(gòu)一次栈妆,架構(gòu)圖如下所示:
業(yè)務(wù)場景沒有變化, 依然是展示三種數(shù)據(jù), 只是三個MVC替換成了三個MVP(圖中我只畫了Blog模塊), UserVC負(fù)責(zé)配置三個MVP(新建各自的VP, 通過VP建立C, C會負(fù)責(zé)建立VP之間的綁定關(guān)系), 并在合適的時機通知各自的P層(之前是通知C層)進行數(shù)據(jù)獲取。
各個P層在獲取到數(shù)據(jù)后進行相應(yīng)處理, 處理完成后會通知綁定的View數(shù)據(jù)有所更新, V收到更新通知后從P獲取格式化好的數(shù)據(jù)進行頁面渲染, UserVC最后將已經(jīng)渲染好的各個View進行布局即可.
另外, V層C層不再處理任何業(yè)務(wù)邏輯, 所有事件觸發(fā)全部調(diào)用P層的相應(yīng)命令厢钧。
具體代碼大家看demo就行了鳞尔,下面我抽出點贊功能來對比分析下MVC和MVP的實現(xiàn)有何不同
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:@"你已經(jīng)贊過了哦~" 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;
//點贊的業(yè)務(wù)邏輯
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已經(jīng)贊過它了~"];
} 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;
//點贊的業(yè)務(wù)展示
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在層次上更加清晰寥假,業(yè)務(wù)邏輯和業(yè)務(wù)展示徹底分離,讓presenter和view可以單獨測試霞扬,而MVC則把這兩者混在一起糕韧,沒法單獨測試。實際項目中大家可以自己根據(jù)項目需求來選擇祥得。
下面是MVC下點贊的邏輯
//點贊的業(yè)務(wù)邏輯
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已經(jīng)贊過它了~"];
} 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;
//點贊的業(yè)務(wù)展示
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
可以看到業(yè)務(wù)邏輯(改變model數(shù)據(jù))和業(yè)務(wù)展示(改變cell的數(shù)據(jù))糅雜在一起兔沃,如果我要測試點贊這個業(yè)務(wù)邏輯,那么就必須生成cell级及,然后點擊cell的按鈕乒疏,去觸發(fā)點贊的業(yè)務(wù)邏輯才可以測試
再看看MVP下的點贊邏輯的實現(xiàn)
業(yè)務(wù)邏輯:
BlogCellPresenter.m
- (void)likeBlogWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
if (self.blog.isLiked) {
!completionHandler ?: completionHandler([NSError errorWithDomain:@"你已經(jīng)贊過了哦~" 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);
}];
}
}
業(yè)務(wù)展示:
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里面業(yè)務(wù)邏輯和業(yè)務(wù)展示是分在不同的地方實現(xiàn),那么就可以分開測試二者了饮焦,而不想MVC那樣想測試下業(yè)務(wù)邏輯怕吴,還必須生成一個view,這不合理县踢,因為業(yè)務(wù)邏輯改變的model的數(shù)據(jù)转绷,和view無關(guān)。
MVP相對于MVC, 它其實只做了一件事情, 即分割業(yè)務(wù)展示和業(yè)務(wù)邏輯. 展示和邏輯分開后, 只要我們能保證V在收到P的數(shù)據(jù)更新通知后能正常刷新頁面, 那么整個業(yè)務(wù)就沒有問題. 因為V收到的通知其實都是來自于P層的數(shù)據(jù)獲取/更新操作, 所以我們只要保證P層的這些操作都是正常的就可以了. 即我們只用測試P層的邏輯, 不必關(guān)心V層的情況
MVVM
1硼啤、概述
MVVM是由微軟提出來的议经,但是這個架構(gòu)也是在下面這篇文章的基礎(chǔ)上發(fā)展起來的:
Martin Fowler 發(fā)表的 Presentation Model 文章
這篇文章上面就提到過,就是MVP的原型谴返,也就是說MVVM其實是在MVP的基礎(chǔ)上發(fā)展起來的煞肾。那么MVVM在MVP的基礎(chǔ)上改良了啥呢?答案就是數(shù)據(jù)綁定嗓袱,下面會慢慢鋪開來講籍救。網(wǎng)上關(guān)于MVVM的定義太多,沒有一個統(tǒng)一的說法渠抹,有的甚至完全相反蝙昙。關(guān)于權(quán)威的MVVM解釋闪萄,大家可以看下微軟的官方文檔:
里面關(guān)于MVVM提出的動機,解決的痛點奇颠,各層的職責(zé)都解釋的比較清楚败去。要追本溯源看下MVVM的前世今生,那么上面的Martin Fowler發(fā)表的文章也可以看看
2005 年烈拒,John Gossman 在他的博客上公布了Introduction to Model/View/ViewModel pattern for building WPF apps 一文为迈。MVVM 與 Martin Fowler 所說的 PM 模式其實是完全相同的,F(xiàn)owler 提出的 PM 模式是一種與平臺無關(guān)的創(chuàng)建視圖抽象的方法缺菌,而 Gossman 的 MVVM 是專門用于 WPF 框架來簡化用戶界面的創(chuàng)建的模式;我們可以認(rèn)為 MVVM 是在 WPF 平臺上對于 PM 模式的實現(xiàn)搜锰。
從 Model-View-ViewModel 這個名字來看伴郁,它由三個部分組成,也就是 Model蛋叼、View 和 ViewModel焊傅;其中視圖模型(ViewModel)其實就是 MVP 模式中的P,在 MVVM 中叫做VM狈涮。
MVVM架構(gòu)圖:
除了我們非常熟悉的 Model狐胎、View 和 ViewModel 這三個部分,在 MVVM 的實現(xiàn)中歌馍,還引入了隱式的一個 Binder層握巢,這也是MVVM相對MVP的進步,而聲明式的數(shù)據(jù)和命令的綁定在 MVVM 模式中就是通過binder層來完成的松却,RAC是iOS下binder的優(yōu)雅實現(xiàn)暴浦,當(dāng)然MVVM沒有RAC也完全可以運行。
下圖展示了iOS下的MVC是如何拆分成MVVM的:
MVVM和MVP相對于MVC最大的改進在于:P或者VM創(chuàng)建了一個視圖的抽象晓锻,將視圖中的狀態(tài)和行為抽離出來形成一個新的抽象歌焦。這可以把業(yè)務(wù)邏輯(P/VM)和業(yè)務(wù)展示(V)分離開單獨測試,并且達到復(fù)用的目的砚哆,邏輯結(jié)構(gòu)更加清晰
2独撇、MVVM各層職責(zé)
MVVM各層的職責(zé)和MVP的類似,VM對應(yīng)P層躁锁,只是在MVVM的View層多了數(shù)據(jù)綁定的操作
3纷铣、MVVM相對于MVP做的改進
上面提到過MVVM相對于MVC的改進是對VM/P和view做了雙向的數(shù)據(jù)和命令綁定,那么這么做的好處是什么呢灿里?還是看上面MVP的點贊的例子
MVP的點贊邏輯如下:
點擊cell按鈕--->調(diào)用P的點贊邏輯---->點贊成功后关炼,P改變M的數(shù)據(jù)--->P回調(diào)Cell的代理方法改變cell的顯示(點贊成功,贊的個數(shù)加1匣吊,同時點贊數(shù)變紅儒拂,否則不改變贊的個數(shù)也不變色)
上面就是一個事件完整過程寸潦,可以看到要通過四步來完成,而且每次都要把P的狀態(tài)同步到view社痛,當(dāng)事件多起來的時候见转,這樣寫就很麻煩了。那有沒有一種簡單的機制蒜哀,讓view的行為和狀態(tài)和P的行為狀態(tài)同步呢斩箫?
答案就是MVVM的binder機制。
點贊的MVP的代碼看上面MVP章節(jié)即可撵儿,我們來看下在MVVM下的點贊如何實現(xiàn)的:
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];
//數(shù)據(jù)綁定操作
@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 {
//事件響應(yīng)
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觸發(fā)P的業(yè)務(wù)邏輯乘客,然后P再回調(diào)改變View的顯示的操作,使用MVVM的數(shù)據(jù)綁定來實現(xiàn)讓邏輯更加清晰淀歇,代碼也更少易核。這就是MVVM相對于MVP的改進之處
VIPER
1、概述
前面講到的幾個架構(gòu)大多脫胎于MVC浪默,但是VIPER和MVC沒有啥關(guān)系牡直,是一個全新的架構(gòu)。從一點就可以看出來:前面幾個MVX框架在iOS下是無法擺脫Apple的viewcontroller影響的纳决,但是VIPER徹底弱化了VC的概念碰逸,讓VC變成了真正意義上的View。把VC的職責(zé)進行了徹底的拆分阔加,分散到各個子層里面了
下圖就是VIPER的架構(gòu)圖
從上面可以看出VIPER應(yīng)該是所有架構(gòu)里面職責(zé)劃分最為明確的饵史,真正做到了SOLID原則。其他架構(gòu)因為有VC的存在胜榔,或多或少都會導(dǎo)致各層的職責(zé)劃分不明確约急。但是也由于VIPER的分層過多,并且是唯一一個把界面路由功能單獨分離出來放到一個單獨的類里面處理苗分,所有的事件響應(yīng)和界面跳轉(zhuǎn)都需要自己處理厌蔽,這導(dǎo)致代碼復(fù)雜度大大增加。
Apple苦心孤詣的給我們搞出一個VC摔癣,雖然會導(dǎo)致層次耦合奴饮,但是也確實簡化了開發(fā)流程,而VIPER則是徹底拋棄了VC择浊,重新進行分層戴卜,做到了每個模塊都可以單獨測試和復(fù)用,但是也導(dǎo)致了代碼過多琢岩、邏輯比較繞的問題投剥。
就我個人經(jīng)驗來說,其實只要做好分層和規(guī)劃担孔,MVC架構(gòu)足夠應(yīng)付大多數(shù)場景江锨。有些文章上來就說MVVM是為了解決C層臃腫, MVC難以測試的問題, 其實并不是這樣的. 按照架構(gòu)演進順序來看, C層臃腫大部分是沒有拆分好MVC模塊, 好好拆分就行了, 用不著MVVM吃警。 而MVC難以測試也可以用MVP來解決, 只是MVP也并非完美, 在VP之間的數(shù)據(jù)交互太繁瑣, 所以才引出了MVVM。 而VIPER則是跳出了MVX架構(gòu)啄育,自己開辟一條新的路酌心。
VIPER是非常干凈的架構(gòu)。它將每個模塊與其他模塊隔離開來挑豌。因此安券,更改或修復(fù)錯誤非常簡單,因為您只需要更新特定的模塊氓英。此外侯勉,VIPER還為單元測試創(chuàng)建了一個非常好的環(huán)境。由于每個模塊獨立于其他模塊铝阐,因此保持了低耦合壳鹤。在開發(fā)人員之間劃分工作也很簡單。
不應(yīng)該在小項目中使用VIPER饰迹,因為MVP或MVC就足夠了
關(guān)于到底是否應(yīng)該在項目中使用VIPER,大家可以看下Quora上面的討論:
Should I use Viper architecture for my next iOS application, or it is still very new to use?
2余舶、VIPER各層職責(zé)
- Interactor(交互器) - 這是應(yīng)用程序的主干啊鸭,因為它包含應(yīng)用程序中用例描述的業(yè)務(wù)邏輯。交互器負(fù)責(zé)從數(shù)據(jù)層獲取數(shù)據(jù)匿值,并執(zhí)行特定場景下的業(yè)務(wù)邏輯赠制,其實現(xiàn)完全獨立于用戶界面。
- Presenter(展示器) - 它的職責(zé)是從用戶操作的Interactor獲取數(shù)據(jù)挟憔,創(chuàng)建一個Entities實例钟些,并將其傳送到View以顯示它。
- Entities(實體) - 純粹的數(shù)據(jù)對象绊谭。不包括數(shù)據(jù)訪問層政恍,因為這是 Interactor 的職責(zé)。
- Router(路由) - 負(fù)責(zé) VIPER 模塊之間的跳轉(zhuǎn)
- View(視圖)- 視圖的責(zé)任是將用戶操作發(fā)送給演示者达传,并顯示presenter告訴它的任何內(nèi)容
PS:
數(shù)據(jù)的獲取應(yīng)該單獨放到一個層篙耗,而不應(yīng)該放到Interactor里面
可以看到一個應(yīng)用場景的所有功能點都被分離成功能完全獨立的層,每個層的職責(zé)都是單一的宪赶。在VIPER架構(gòu)中宗弯,每個塊對應(yīng)于具有特定任務(wù),輸入和輸出的對象搂妻。它與裝配線中的工作人員非常相似:一旦工作人員完成其對象上的作業(yè)蒙保,該對象將傳遞給下一個工作人員,直到產(chǎn)品完成欲主。
層之間的連接表示對象之間的關(guān)系邓厕,以及它們彼此傳遞的信息類型逝嚎。通過協(xié)議給出從一個實體到另一個實體的通信。
這種架構(gòu)模式背后的想法是隔離應(yīng)用程序的依賴關(guān)系邑狸,平衡實體之間的責(zé)任分配懈糯。基本上单雾,VIPER架構(gòu)將您的應(yīng)用程序邏輯分為較小的功能層赚哗,每個功能都具有嚴(yán)格的預(yù)定責(zé)任。這使得更容易測試層之間邊界的交互硅堆。它適用于單元測試屿储,并使您的代碼更可重用。
3渐逃、VIPER 架構(gòu)的主要優(yōu)點
- 簡化復(fù)雜項目够掠。由于模塊獨立,VIPER對于大型團隊來說真的很好茄菊。
- 使其可擴展疯潭。使開發(fā)人員盡可能無縫地同時處理它
- 代碼達到了可重用性和可測試性
- 根據(jù)應(yīng)用程序的作用劃分應(yīng)用程序組件,設(shè)定明確的責(zé)任
- 可以輕松添加新功能
- 由于您的UI邏輯與業(yè)務(wù)邏輯分離面殖,因此可以輕松編寫自動化測試
- 它鼓勵分離使得更容易采用TDD的關(guān)注竖哩。Interactor包含獨立于任何UI的純邏輯,這使得通過測試輕松開車
- 創(chuàng)建清晰明確的接口脊僚,獨立于其他模塊相叁。這使得更容易更改界面向用戶呈現(xiàn)各種模塊的方式。
- 通過單一責(zé)任原則辽幌,通過崩潰報告更容易地跟蹤問題
- 使源代碼更清潔增淹,更緊湊和可重用
- 減少開發(fā)團隊內(nèi)的沖突數(shù)量
- 適用SOLID原則
- 使代碼看起來類似。閱讀別人的代碼變得更快乌企。
VIPER架構(gòu)有很多好處虑润,但重要的是要將其用于大型和復(fù)雜的項目名眉。由于所涉及的元素數(shù)量靠胜,這種架構(gòu)在啟動新的小型項目時會導(dǎo)致開銷,因此VIPER架構(gòu)可能會對無意擴展的小型項目造成過高的影響脯颜。因此虽画,對于這樣的項目舞蔽,最好使用別的東西,例如MVC码撰。
4渗柿、如何構(gòu)建正確的VIPER
我們來構(gòu)建一個小的VIPER應(yīng)用,我不想把上面的demo用VIPER再重寫一次了,因為太麻煩了朵栖,所以就寫一個簡單的demo給大家演示下VIPER颊亮,但是麻雀雖小五臟俱全,該有的功能都有了陨溅。
如上圖所示终惑,有兩個界面contactlist和addcontact,在contactlist的右上角點擊添加按鈕门扇,跳轉(zhuǎn)到addcontact界面雹有,輸入firstname和secondname后點擊done按鈕,回到contactlist界面臼寄,新添加的用戶就顯示在該界面上了霸奕。
先看下項目的架構(gòu),如下所示:
可以看到每個界面都有6個文件夾吉拳,還有兩個界面公用的Entities文件夾质帅,每個文件夾對應(yīng)一個分層,除了VIPER的五層之外留攒,每個界面還有兩個文件夾:Protocols和DataManager層煤惩。
Protocols定義的VIPER的每層需要遵守的協(xié)議,每層對外暴露的操作都經(jīng)過protocol抽象了炼邀,這樣可以針對抽象編程魄揉。DataManager定義的是數(shù)據(jù)操作,包括從本地和網(wǎng)絡(luò)獲取汤善、存儲數(shù)據(jù)的操作。
下面先來看看Protocols類的實現(xiàn):
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各層之間的數(shù)據(jù)流向票彪,非常清晰红淡。
然后就是各層去具體實現(xiàn)這些協(xié)議了,這里就不貼代碼了降铸,大家可以去demo里面看在旱。下面主要講一下路由層,這是VIPER所獨有的推掸,其他的MVX架構(gòu)都是把路由放到了VC里面做桶蝎,而VIPER架構(gòu)因為徹底摒棄了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()
}
//導(dǎo)航到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調(diào)用:
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的界面的右上角添加按鈕調(diào)用:
class ContactListView: UIViewController {
var presenter: ContactListPresenterProtocol?
//點擊添加按鈕登渣,調(diào)用presenter的對應(yīng)業(yè)務(wù)邏輯
@IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) {
presenter?.addNewContact(from: self)
}
}
=================
//presenter實現(xiàn)添加按鈕的業(yè)務(wù)邏輯,調(diào)用router的跳轉(zhuǎn)邏輯毡泻,調(diào)到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層的功能也類似胜茧,大家可以自己去領(lǐng)會。從上面的代碼可以看到VIPER架構(gòu)的最大特點就是實現(xiàn)了SOLID原則,每層只做自己的事情呻顽,職責(zé)劃分的非常清楚雹顺,自己的任務(wù)處理完后就交給下一個層處理。
看完上面的代碼是不是覺得這也太繞了吧廊遍,是的嬉愧,我也這么覺得,但是不得不說VIPER的優(yōu)點也有很多喉前,上面已經(jīng)列舉了没酣。所以如果是中小型的項目,還是用MVX架構(gòu)吧被饿,如果MVX架構(gòu)依然hold不住你的每個類都在膨脹四康,那么試試VIPER你可能會有新的發(fā)現(xiàn)。
其實我倒覺得VIPER徹底放棄Apple的VC有點得不償失狭握,個人還是喜歡用VC來做界面路由闪金,而不是單獨搞一個router層去路由,這樣既借鑒了VIPER的優(yōu)點论颅,有兼顧了VC的好處哎垦,具體的看最后的demo,我這里就不展開說了恃疯,大家做一個對比應(yīng)該就有了解漏设。
5、VIPER參考書籍
號稱是唯一一本介紹VIPER的書籍今妄,然而完整版只有俄語的郑口,不過我們有萬能的谷歌翻譯,只要不是火星文都可以看啦~
6盾鳞、VIPER代碼模板生成器
由于VIPER架構(gòu)的類比較多犬性,還要寫一堆模塊之間的協(xié)議,如果每次都要手寫的話腾仅,太心累了~ 所以大家可以試試下面的代碼生成器乒裆,一次生成VIPER的代碼模板