深刻分析MVC、MVP、MVVM、VIPER

前言

看了下上篇博客的發表時間到這篇博客,居然過了11個月,罪過,罪過。這一年時間也是夠折騰的,年初離職跳槽到鵝廠,單獨負責一個社區項目,忙的天昏地暗,忙的差很少了,轉眼就到了7月。css

七月流火,心也跟着燥熱起來了,眼瞅着移動端這發展趨勢從05年開始就一直在走下坡路了,想着再這麼下去不行,得找條後路備着。網上看了看,以爲前端不錯,最近炒的挺火熱的,那就學學看吧,買了html,css,js的幾本書,花了個把月的閒暇時間看完了,順便作了幾個demo,忽然以爲好無聊。大概是iOS也是寫界面,前端仍是寫界面,寫的有些麻木了。以前一直有學Python,寫過一些爬蟲、用Django也寫事後臺,感受還挺好玩的,Python在大數據和AI領域也大放異彩,想借此機會學學。html

雖然這兩個領域進入門檻比較高,可是就目前發展勢頭來看,應該是一個發展趨勢,互聯網過去十年的浪潮是移動互聯網,下一個十年的浪潮極可能就是AI了。因此早作準備,從零開始學吧。其實幹程序員這行,焦慮是沒法避免的,由於本身那點知識儲備和突飛猛進的技術發展相比起來,簡直滄海一粟,不禁讓人感嘆:吾生也有涯,而學無涯。前端

不少人都在追逐新技術的過程當中迷失了本身,越學越焦慮,由於發現本身不管怎麼學,都趕不上技術發展的腳步。我倒以爲如其去追逐那些還不知道能不能落地的新技術,還不如紮紮實實打好基本功,好比系統、數據結構、算法、網絡,新技術層出不窮,亂花漸入迷人眼,可是歸根到底也是在這些基礎知識上面創建起來的。ios

關於如何學習,有時間我們單獨開一篇聊聊。下面進入今天正題,聊一聊在iOS開發領域裏面幾大架構的應用,包括MVC、MVP、MVVM、VIPER,作iOS開發通常都是比較熟悉MVC的,由於Apple已經爲咱們量身定製了適合iOS開發的MVC架構。git

可是在寫代碼的過程當中你們確定會有這些疑問:爲何個人VC愈來愈大,爲何感受apple的MVC怪怪的不像真正的MVC,網絡請求邏輯到底放在哪層,網上很火熱的MVVM是否值得學習,VIPER又是什麼鬼?程序員

我但願下面的文字能爲你們解除這些疑惑,我會在多個維度對這幾個框架進行對比分析,指出他們的優劣,而後結合一個具體的DEMO用不一樣的架構去實現,讓你們對這些架構有一個直觀的瞭解。固然這些都只是作拋磚引玉之用,闡述的也是個人我的理解,若有錯誤,歡迎指出,你們一塊兒探討進步~~github


MVC

一、MVC的理想模型

MVC的理想模型以下圖所示:web

各層的職責以下所示:算法

  • Models: 數據層,負責數據的處理和獲取的數據接口層。
  • Views: 展現層(GUI),對於 iOS 來講全部以 UI 開頭的類基本都屬於這層。
  • Controller: 控制器層,它是 Model 和 View 之間的膠水或者說是中間人。通常來講,當用戶對 View 有操做時它負責去修改相應 Model;當 Model 的值發生變化時它負責去更新對應 View。

如上圖所示,M和View應該是徹底隔離的,由C做爲中間人來負責兩者的交互,同時三者是徹底獨立分開的,這樣能夠保證M和V的可測試性和複用性,可是通常因爲C都是爲特別的應用場景下的M和V作中介者,因此很難複用。數據庫

二、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組合體

三、iOS的MVC各層職責

controller層(VC):

  • 生成view,而後組裝view
  • 響應View的事件和做爲view的代理
  • 調用model的數據獲取接口,拿到返回數據,處理加工,渲染到view顯示
  • 處理view的生命週期
  • 處理界面之間的跳轉

model層:

  • 業務邏輯封裝
  • 提供數據接口給controller使用
  • 數據持久化存儲和讀取
  • 做爲數據模型存儲數據

view層:

  • 界面元素搭建,動畫效果,數據展現,
  • 接受用戶操做並反饋視覺效果

PS:
model層的業務邏輯通常都是和後臺數據交互的邏輯,還有一些抽象的業務邏輯,好比格式化日期字符串爲NSDateFormatter類型等

四、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架構寫的呀,爲何會出現這種問題呢?

暫且按下不表,咱們先看另一個問題,先把這個問題搞清楚了,對於後續文章的理解大有裨益。

五、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層不該該是數據模型,放幾個屬性就完事了。而應該是承載業務邏輯和數據存儲獲取的職責一層。

六、如何構建構建正確的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裏面也沒事。

七、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 架構模式

一、概述

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),除此以外還會將一些方法暴露給視圖用於某些事件的響應。

二、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層的職責,其中三、4兩點和MVC的view相似,可是一、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依賴抽象而不是具體實現。

三、被動式圖模式的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;

四、監督控制器模式的MVP

在監督控制器中,視圖層接管了一部分視圖邏輯,主要就是同步簡單的視圖和模型的狀態;而監督控制器就須要負責響應用戶的輸入以及一部分更加複雜的視圖、模型狀態同步工做。

對於用戶輸入的處理,監督控制器的作法與標準 MVP 中的 Presenter 徹底相同。可是對於視圖、模型的數據同步工做,使用相似於下面要講到MVVM中的雙向綁定機制來實現兩者的相互映射。

以下圖所示:

監督控制器中的視圖和模型層之間增長了二者之間的耦合,也就增長了整個架構的複雜性。和被動式圖的MVP不一樣的是:視圖和模型之間新增了的依賴,就是雙向的數據綁定;視圖經過聲明式的語法與模型中的簡單屬性進行綁定,當模型發生改變時,會通知其觀察者視圖做出相應的更新。

經過這種方式可以減輕監督控制器的負擔,減小其中簡單的代碼,將一部分邏輯交由視圖進行處理;這樣也就致使了視圖同時能夠被 Presenter 和數據綁定兩種方式更新,相比於被動視圖,監督控制器的方式也下降了視圖的可測試性和封裝性。

能夠參考這篇文章Supervising Controller

五、如何構建正確的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

一、概述

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)分離開單獨測試,而且達到複用的目的,邏輯結構更加清晰

二、MVVM各層職責

MVVM各層的職責和MVP的相似,VM對應P層,只是在MVVM的View層多了數據綁定的操做

三、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

一、概述

前面講到的幾個架構大多脫胎於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?

二、VIPER各層職責

  • Interactor(交互器) - 這是應用程序的主幹,由於它包含應用程序中用例描述的業務邏輯。交互器負責從數據層獲取數據,並執行特定場景下的業務邏輯,其實現徹底獨立於用戶界面。
  • Presenter(展現器) - 它的職責是從用戶操做的Interactor獲取數據,建立一個Entities實例,並將其傳送到View以顯示它。
  • Entities(實體) - 純粹的數據對象。不包括數據訪問層,由於這是 Interactor 的職責。
  • Router(路由) - 負責 VIPER 模塊之間的跳轉
  • View(視圖)- 視圖的責任是將用戶操做發送給演示者,並顯示presenter告訴它的任何內容

PS:
數據的獲取應該單獨放到一個層,而不該該放到Interactor裏面

能夠看到一個應用場景的全部功能點都被分離成功能徹底獨立的層,每一個層的職責都是單一的。在VIPER架構中,每一個塊對應於具備特定任務,輸入和輸出的對象。它與裝配線中的工做人員很是類似:一旦工做人員完成其對象上的做業,該對象將傳遞給下一個工做人員,直到產品完成。

層之間的鏈接表示對象之間的關係,以及它們彼此傳遞的信息類型。經過協議給出從一個實體到另外一個實體的通訊。

這種架構模式背後的想法是隔離應用程序的依賴關係,平衡實體之間的責任分配。基本上,VIPER架構將您的應用程序邏輯分爲較小的功能層,每一個功能都具備嚴格的預約責任。這使得更容易測試層之間邊界的交互。它適用於單元測試,並使您的代碼更可重用。

三、VIPER 架構的主要優勢

  • 簡化複雜項目。因爲模塊獨立,VIPER對於大型團隊來講真的很好。
  • 使其可擴展。使開發人員儘量無縫地同時處理它
  • 代碼達到了可重用性和可測試性
  • 根據應用程序的做用劃分應用程序組件,設定明確的責任
  • 能夠輕鬆添加新功能
  • 因爲您的UI邏輯與業務邏輯分離,所以能夠輕鬆編寫自動化測試
  • 它鼓勵分離使得更容易採用TDD的關注。Interactor包含獨立於任何UI的純邏輯,這使得經過測試輕鬆開車
  • 建立清晰明確的接口,獨立於其餘模塊。這使得更容易更改界面向用戶呈現各類模塊的方式。
  • 經過單一責任原則,經過崩潰報告更容易地跟蹤問題
  • 使源代碼更清潔,更緊湊和可重用
  • 減小開發團隊內的衝突數量
  • 適用SOLID原則
  • 使代碼看起來相似。閱讀別人的代碼變得更快。

VIPER架構有不少好處,但重要的是要將其用於大型和複雜的項目。因爲所涉及的元素數量,這種架構在啓動新的小型項目時會致使開銷,所以VIPER架構可能會對無心擴展的小型項目形成太高的影響。所以,對於這樣的項目,最好使用別的東西,例如MVC。

四、如何構建正確的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,我這裏就不展開說了,你們作一個對比應該就有了解。

五、VIPER參考書籍

The-Book-of-VIPER

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

六、VIPER代碼模板生成器

因爲VIPER架構的類比較多,還要寫一堆模塊之間的協議,若是每次都要手寫的話,太心累了~ 因此你們能夠試試下面的代碼生成器,一次生成VIPER的代碼模板

viper-module-generator

Generamba

ViperCode


Demo下載

VIPER-DEMO

MVX架構DEMO

改造後的VIERP Demo

相關文章
相關標籤/搜索