深入分析MVC、MVP、MVVM、VIPER

西木柚子發表於2017-11-03

前言

看了下上篇部落格的發表時間到這篇部落格,竟然過了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的組合,目的就是為了提高開發效率,簡化操作。

apple mvc 規範

摘自上面的連結

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原則。

下面來舉一個簡單的例子,先宣告下我下面列舉的例子主要來著這篇部落格:

雜談: MVC/MVP/MVVM

這篇文章質量很高,對三種模式的講解比較深入,關鍵還有例子來做橫向對比,這是其他文章沒有的。大家可以先看看這篇文章,本文的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的理解我不敢苟同,大家見仁見智吧

論MVVM偽框架結構和MVC中M的實現機制

下面的引用也是摘自這篇文章:

理解Model層:

首先要正確的理解MVC中的M是什麼?他是資料模型嗎?答案是NO。他的正確定義是業務模型。也就是你所有業務資料和業務實現邏輯都應該定義在M層裡面,而且業務邏輯的實現和定義應該和具體的介面無關,也就是和檢視以及控制之間沒有任何的關係,它是可以獨立存在的,您甚至可以將業務模型單獨編譯出一個靜態庫來提供給第三方或者其他系統使用。

在上面經典MVC圖中也很清晰的描述了這一點: 控制負責呼叫模型,而模型則將處理結果傳送通知給控制,控制再通知檢視重新整理。因此我們不能將M簡單的理解為一個個乾巴巴的只有屬性而沒有方法的資料模型。

其實這裡面涉及到一個最基本的設計原則,那就是物件導向的基本設計原則:就是什麼是類?類應該是一個個具有不同操作和不同屬性的物件的抽象(類是屬性和方法的集合)。 我想現在任何一個系統裡面都沒有出現過一堆只有資料而沒有方法的資料模型的集合被定義為一個單獨而抽象的模型層來供大家使用吧。 我們不能把一個儲存資料模型的資料夾來當做一個層,這並不符合橫向切分的規則。

Model層實現的正確姿勢:

  1. 定義的M層中的程式碼應該和V層和C層完全無關的,也就是M層的物件是不需要依賴任何C層和V層的物件而獨立存在的。整個框架的設計最優結構是V層不依賴C層而獨立存在,M層不依賴C層和V層獨立存在,C層負責關聯二者,V層只負責展示,M層持有資料和業務的具體實現,而C層則處理事件響應以及業務的呼叫以及通知介面更新。三者之間一定要明確的定義為單向依賴,而不應該出現雙向依賴

  2. 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的概念:

iOS應用架構談 view層的組織和呼叫方案

此時VC的職責簡化為如下三條職責:

  1. 生成子view並新增到自己的self.view上面
  2. 管理view的生命週期
  3. 通知每個子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只需要做三件事:

  1. 生成子view並新增到自己的self.view上面
  2. 管理view的生命週期
  3. 通知每個子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的優點有如下幾點:

  1. 程式碼複用: 三個小模組的V(cell/userInfoView)對外只暴露Set方法, 對M甚至C都是隔離狀態, 複用完全沒有問題. 三個大模組的MVC也可以用於快速構建相似的業務場景(大模組的複用比小模組會差一些, 下文我會說明).
  2. 程式碼臃腫: 因為Scene大部分的邏輯和佈局都轉移到了相應的MVC中, 我們僅僅是拼裝MVC的便構建了兩個不同的業務場景, 每個業務場景都能正常的進行相應的資料展示, 也有相應的邏輯互動, 而完成這些東西, 加空格也就100行程式碼左右(當然, 這裡我忽略了一下Scene的佈局程式碼).
  3. 易擴充性: 無論產品未來想加回收站還是防禦塔, 我需要的只是新建相應的MVC模組, 加到對應的Scene即可.
  4. 可維護性: 各個模組間職責分離, 哪裡出錯改哪裡, 完全不影響其他模組. 另外, 各個模組的程式碼其實並不算多, 哪一天即使寫程式碼的人離職了, 接手的人根據錯誤提示也能快速定位出錯模組.
  5. 易測試性: 很遺憾, 業務的初始化依然繫結在Scene的生命週期中, 而有些邏輯也仍然需要UI的點選事件觸發, 我們依然只能Command+R, 點點點…

缺點

經過上面的改造,MVC架構已經足夠清晰了,按照應用場景(一般都是單頁面)進行大的拆分,然後在根據業務拆分成小的MVC。不行就接著拆,拆層,拆模組。

但是MVC的最大弊端就是C的程式碼沒法複用,所以能把C層的程式碼拆出來就儘量拆,我們來看看現在C層的功能還有哪些了

  1. 作為View和Model的中介者,從model獲取資料,經過資料加工,渲染到view上面顯示
  2. 響應view的點選事件,然後執行相應的業務邏輯
  3. 作為view的代理和資料來源
  4. 暴露介面給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來實現了。

這裡又有兩種不同的實現方式:

  1. 讓P持有V,P通過V的暴露介面改變V的顯示資料和狀態,P通過V的事件回撥來執行自身的業務邏輯
  2. 讓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

通訊方式

  1. 當檢視接收到來自使用者的事件時,會將事件轉交給 Presenter 進行處理;
  2. 被動的檢視實現presentr的代理,當需要更新檢視時 Presenter回撥代理來更新檢視的內容,這樣讓presenter專注於業務邏輯,view專注於顯示邏輯
  3. Presenter 負責對模型進行操作和更新,在需要時取出其中儲存的資訊;
  4. 當模型層改變時,可以將改變的資訊傳送給觀察者 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解釋,大家可以看下微軟的官方文件:

The MVVM Pattern

裡面關於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有三個功能:

  1. 生成ContactList的view
  2. 繫結ContactList場景下VIPER各層
  3. 路由到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參考書籍

The-Book-of-VIPER

號稱是唯一一本介紹VIPER的書籍,然而完整版只有俄語的,不過我們有萬能的谷歌翻譯,只要不是火星文都可以看啦~

6、VIPER程式碼模板生成器

由於VIPER架構的類比較多,還要寫一堆模組之間的協議,如果每次都要手寫的話,太心累了~ 所以大家可以試試下面的程式碼生成器,一次生成VIPER的程式碼模板

viper-module-generator

Generamba

ViperCode


Demo下載

VIPER-DEMO

MVX架構DEMO

改造後的VIERP Demo

相關文章