IOS- 框架模式(簡述 MVC,MVP,MVVM 和 VIPER)

MV(X)系列概要

  做iOS開發也有一段時間了,最近閒暇之餘總結了一下幾個iOS框架,每個人對架構和設計模式都有不同的理解,在此記錄下我的一些小見解,僅供參考,歡迎批評指正。

當今我們已經有很架構設計模式方面的選擇:

A.MVC

B.MVP

C.MVVM

D.VIPER

前三種設計模式都把一個應用中的實體分爲以下三類:

A: Models–負責主要的數據或者操作數據的數據訪問層,可以想象 Perspn 和 PersonDataProvider 類。

B: Views–負責展示層(GUI),對於iOS環境可以聯想一下以 UI 開頭的所有類。

C: Controller/Presenter/ViewModel–負責協調 Model 和 View,通常根據用戶在View上的動作在Model上作出對應的更改,同時將更改的信息返回到View上.

MVC的過去

在我們探討Apple的MVC模式之前,我們來看下傳統的MVC模式。
這裏寫圖片描述

在這裏,View並沒有任何界限,僅僅是簡單的在Controller中呈現出Model的變化。想象一下,就像網頁一樣,在點擊了跳轉到某個其他頁面的連接之後就會完全的重新加載頁面。儘管在iOS平臺上實現這這種MVC模式是沒有任何難度的,但是它並不會爲我們解決架構問題帶來任何裨益。因爲它本身也是,三個實體間相互都有通信,而且是緊密耦合的。這很顯然會大大降低了三者的複用性,而這正是我們不願意看到的。

蘋果推薦的MVC–願景

控制器來作爲視圖和模型中間的中轉站,這樣視圖和模型之間互相就沒有直接聯繫了。這樣的話,控制器的可複用性就變得最低,但是這個對於我們來說也是可接受的,因爲我們需要有一個地方來放那些不方便放在模型中的複雜業務邏輯。
這裏寫圖片描述

理論上來講,這種結構看起來非常直接,但是是不是覺得有點不對勁?甚至聽到過有人叫 MVC 爲重控制器模式。此外,對於 iOS 開發者來說,給控制器減輕負擔已經成爲一個重要的話題。爲什麼蘋果會採用僅僅改進過一點點的傳統 MVC 模式呢?

現實(Reality)

這裏寫圖片描述

Cocoa MVC 鼓勵你寫重控制器是因爲它們互相之間在視圖的生命週期中互相牽扯,以至於很難將它們分開。雖然你也可能有一些辦法把一些業務邏輯和數據轉模型的工作放到 Model 中來,但是對於把負擔分攤到 View 上卻沒有什麼辦法,大多數情況中,View 的所有功能就是給 Controller 發送動作(action),而 Controller 則會成爲所有東西的代理或者數據源,並且通常會發送或者取消網絡請求等等等等,你可以盡情想象。

評估:

(1) 劃分(Distribution)–View 和 Model 確實是分開了,但是View 和 Controller 緊緊的聯繫在一起
(2)可測試性–由於功能劃分的不好,你可能只能測試你的 Model
(3)易用性–與其他模式相比代碼量最小,另外,每個人都對他很熟悉,即使是一個不是非常有經驗的開發者也能進行維護

就開發速度而言,Cocoa MVC 是最好的架構模式。

MVP

Cocoa MVC’s promises delivered

這裏寫圖片描述

它看上去是不是非常像 Apple’s MVC ?是的,確實很像,並且叫做MVP(Passive View variant)。但是等一下,這意味着 Apple’s MVC 實質上就是 MVP 嗎?不是的,還記得 View 是緊緊和 Controller 聯繫在一起的吧,而在 MVP 中,作爲中轉站的 Presenter 與視圖控制器的生命週期沒有任何關聯,並且 View 很容易被模擬,所以在 Presenter 中沒有任何頁面佈局的代碼,但是 Presenter 有責任通過數據和狀態來更新 View。

MVP 的特點:

(1) 劃分(distribution)–大部分的任務都被劃分到 Presenter 和 Model 中,而 View不太靈活(例子中的 Model 也不太靈活)
(2) 可測試性–非常出色,我們可以通過 View 來測試大部分的業務邏輯
(3)易用性–在我們那個不切實際的小例子裏,MVP 的理念是非常清晰的,但是代碼量是 MVC 模式的兩倍

MVP 在 iOS 中使用意味着非常好的可測試和非常多的代碼

MVVM

概述

引用自iOS應用架構談:
http://www.cocoachina.com/ios/20150525/11919.html

  MVVM的出現主要是爲了解決在開發過程中Controller越來越龐大的問題,變得難以維護,所以MVVM把數據加工的任務從Controller中解放了出來,使得Controller只需要專注於數據調配的工作,ViewModel則去負責數據加工並通過通知機制讓View響應ViewModel的改變。

  MVVM是基於胖Model的架構思路建立的,然後在胖Model中拆出兩部分:Model和ViewModel。ViewModel本質上算是Model層(因爲是胖Model裏面分出來的一部分),所以View並不適合直接持有ViewModel,因爲ViewModel有可能並不是只服務於特定的一個View,使用更加鬆散的綁定關係能夠降低ViewModel和View之間的耦合度。

  還有一個讓人很容易忽略的問題,大部分國內外資料闡述MVVM的時候都是這樣排布的:
  

View<->ViewModel <->Model

造成了MVVM不需要Controller的錯覺,現在似乎發展成業界開始出現MVVM是不需要Controller的聲音了。其實MVVM是一定需要Controller的參與的,雖然MVVM在一定程度上弱化了Controller的存在感,並且給Controller做了減負瘦身(這也是MVVM的主要目的)。但是,這並不代表MVVM中不需要Controller,MMVC和MVVM他們之間的關係應該是這樣:

View <-> C <-> ViewModel <->Model

所以使用MVVM之後,就不需要Controller的說法是不正確的。嚴格來說MVVM其實是MVCVM。從中可以得知,Controller夾在View和ViewModel之間做的其中一個主要事情就是將View和ViewModel進行綁定。在邏輯上,Controller知道應當展示哪個View,Controller也知道應當使用哪個ViewModel,然而View和ViewModel它們之間是互相不知道的,所以Controller就負責控制他們的綁定關係,所以叫Controller/控制器就是這個原因。

  前面講了那麼多,其實歸根結底就是一句話:在MVC的基礎上,把C拆出一個ViewModel專門負責數據處理的事情,就是MVVM。然後,爲了讓View和ViewModel之間能夠有比較鬆散的綁定關係,於是我們使用ReactiveCocoa,KVO,Notification,block,delegate和target-action都可以用來做數據通信,從而來實現綁定,但都不如ReactiveCocoa提供的RACSignal來的優雅,如果不用ReactiveCocoa,綁定關係可能就做不到那麼鬆散那麼好,但並不影響它還是MVVM。

MVVM(View-ViewManger-C-ViewModel-Model)

這裏寫圖片描述

View - 用來呈現用戶界面
ViewManger - 用來處理View的常規事件,負責管理View
Controller - 負責ViewManger和ViewModel之間的綁定,負責控制器本身的生命週期。
ViewModel - 存放各種業務邏輯和網絡請求
Model - 用來呈現數據

這裏寫圖片描述

這種設計的目的是保持View和Model的高度純潔,提高可擴展性和複用度。在日常開發中,ViewModel是爲了拆分Controller業務邏輯而存在的,所以ViewModel需要提供公共的服務接口,以便爲Controller提供數據。而ViewManger的作用相當於一個小管家,幫助Controller來分別管理每個subView,ViewManger負責接管來自View的事件,也負責接收來自Controller的模型數據,而View進行自己所負責的視圖數據綁定工作。Controller則是最後的大家長,負責將ViewModel和ViewManger進行綁定,進行數據轉發工作。把合適的數據模型分發給合適的視圖管理者。

  日常開發中,往往一個視圖頁面交由一個控制器進行管理,而一個頁面上又有N個小的子頁面,這就要求我們來對這些視圖進行合適的分層處理,拆分視圖,將這些視圖進行封裝,將複雜View抽象成獨立的類,不必暴露出具體的實現細節。這樣做的好處是,簡化應用層的層級複雜度,同時也方便進行管理,視圖結構就會變得很清晰。子視圖具體的內部事件,可通過代理模式或者Block交由ViewManger處理,因爲視圖是可以複用的,而其中的事件響應代碼往往是根據不同的業務是有差異的。所以可能會有下面兩種情況出現:
  
- View很純潔,需要複用View,若業務邏輯變化則切換ViewManger。
- ViewManger也比較純潔,若業務邏輯不變,而View需要大變,則切換View即可,保證View中的protocol或者block一致即可<最好是通過協議提前規範好>。

 這樣就實現了互相的封裝,兩者之間只通過protocol或者block進行交流通信,降低了代碼的耦合度。儘量使用protocol和category來制定對象之間的通信規範,來降低代碼的侵入性。

  這樣的架構設計,就像一條生產線,ViewModel進行數據的採集和加工,Controller則進行數據的裝配和轉發工作,ViewManger進行接收轉發分配來的數據,從而進行負責View的展示工作和管理View的事件。這樣,不管哪個環節,都是可以更換的,同時也提高了複用性。

架構講解

這裏寫圖片描述

 以上圖做爲講解demo,最然很簡單,但是也能夠很好的闡述了,理解思想纔是最重要的。
首先我們來拆分這個頁面,第一個爲控制器。暫且命名爲MyController,上面有兩個直接子視圖,按鈕MyBtn和頁面比較複雜的子視圖MyView,MyView中有MyViewBtn1和MyViewBtn2還有一個MyViewLabel視圖。
具體結構如下:

MyController
MyBtn
MyView
MyViewBtn1
MyViewBtn2
MyViewLabel
  界面分析完了,現在可以進行代碼的架構工作了。
首先需要建立一個ViewModel,使它能夠源源不斷的進行數據的生產,並提供數據給MyController;然後建立一個ViewManger負責管理MyView,當然,Model模型數據必不可少。這些工作完成之後,代碼結構變爲:

Controller - - 存放MyController
ViewModel - - 存放MyViewModel
View - - 存放MyView
ViewManger - - 存放MyViewManger
Model - - 存放MyModel

  控制器中的代碼結構如下圖:
  
這裏寫圖片描述

當用戶點擊MyBtn按鈕觸發動作時,控制器就會就將ViewMode中加載的數據模型轉發分配給ViewManger中的回調函數- (void)smk_viewMangerWithModel:(NSDictionary * (^) ( ))dictBlock接收。

// 兩種消息傳遞方式,開發時任選其一即可
- (void)smk_viewMangerWithSubView:(UIView *)subView {

    __weak typeof(self.thirdView) weakThirdView =  self.thirdView;
    __weak typeof(self) weakSelf = self;

    // btnClickBlock
    weakThirdView.btnClickBlock = ^() {
        [weakSelf smk_viewMangerWithHandleOfSubView:weakThirdView info:@"click"];
    };
}

// 兩種消息傳遞方式,開發時任選其一即可
- (void)smk_view:(__kindof UIView *)view withEvents:(NSDictionary *)events {

    NSLog(@"----------%@", events);

    if ([[events allKeys] containsObject:@"jump"]) {
        FirstVC *firstVC = [UIViewController svv_viewControllerWithStoryBoardName:@"Main" identifier:@"FirstVCID"];
        [view.sui_currentVC.navigationController pushViewController:firstVC animated:YES];
    }

}

  其中,MyViewModel中的加載代碼如下,如上所述,它的工作就是分解以前控制器做的一些事情。

- (void)smk_viewModelWithGetDataSuccessHandler:(void (^)())successHandler {
    // 博客中省略,查看詳細請參考demo
}

- (instancetype)getRandomData {
    if (self.smk_dataArrayList.count > 0) {
        u_int32_t index = arc4random_uniform((u_int32_t)self.smk_dataArrayList.count);
        return self.smk_dataArrayList[index];
    }
    return nil;
}
  MyViewManger中的代碼如下,它實現了MVVMViewMangerProtocol協議的三個方法:

// 此方法用來接收處理來自所管理View的一些事件。
- (void)smk_viewMangerWithSubView:(UIView *)subView;
// 此方法將view的父視圖傳遞過來,用來佈局當前View
- (void)smk_viewMangerWithSuperView:(UIView *)superView;
// 根據所傳入的view和info信息分別實現具體的方法
- (void)smk_viewMangerWithHandleOfSubView:(UIView *)subView info:(NSString *)info;
// 兩種消息傳遞方式,開發時任選其一即可
- (void)smk_viewMangerWithSubView:(UIView *)subView {

    __weak typeof(self.thirdView) weakThirdView =  self.thirdView;
    __weak typeof(self) weakSelf = self;

    // btnClickBlock
    weakThirdView.btnClickBlock = ^() {
        [weakSelf smk_viewMangerWithHandleOfSubView:weakThirdView info:@"click"];
    };
}

// 兩種消息傳遞方式,開發時任選其一即可 ---> 視圖delegate
- (void)smk_view:(__kindof UIView *)view withEvents:(NSDictionary *)events {

    NSLog(@"----------%@", events);

    if ([[events allKeys] containsObject:@"jump"]) {
        FirstVC *firstVC = [UIViewController svv_viewControllerWithStoryBoardName:@"Main" identifier:@"FirstVCID"];
        [view.sui_currentVC.navigationController pushViewController:firstVC animated:YES];
    }

}

- (void)smk_viewMangerWithSuperView:(UIView *)superView {
    self.thirdView.frame = CGRectMake(0, 66, [UIScreen mainScreen].bounds.size.width, 200);
    [superView addSubview:self.thirdView];
}

- (void)smk_viewMangerWithHandleOfSubView:(UIView *)view info:(NSString *)info {

    if ([info isEqualToString:@"click"]) {
        [view configureViewWithCustomObj:self.smk_model];
    }
}
  MyView中的代碼如下,主要是負責管理自身的內部控件視圖,並根據業務邏輯需要定義了一些基本事件,通過交給ViewManger來實現:

- (IBAction)testBtnClick:(UIButton *)sender {

    if (self.btnClickBlock) {
        self.btnClickBlock();
    }
}

- (IBAction)jumpOtherVC:(UIButton *)sender {

    if (self.delegate && [self.delegate respondsToSelector:@selector(smk_view:withEvents:)]) {
        [self.delegate smk_view:self withEvents:@{@"jump": @"vc"}];
    }
}

// 根據傳入的model配置需要顯示的內容
- (void)configureViewWithCustomObj:(id)obj {
    if (!obj) return;
    ThirdModel *thirdModel = (ThirdModel *)obj;
    self.testLabel.text = thirdModel.title;
}

  這樣把各個部分區分開來,是不是感覺代碼結構十分清晰了呢,當然可以根據個人習慣來進行修改,代碼實現因人而異,但是思想確是互通的。把合適的業務邏輯交給最合適的對象去處理實現,只需要遵守這麼一個基本原則就可以了。

  至於是否採用更輕量級的ViewController做法,即 通過將各個 protocol 的實現挪到 ViewController 之外,來爲 ViewController 瘦身 ,衆口不一。以UITableView爲例,我的做法是:

如果只是在頁面上進行簡單的展示,並不設計負責的業務邏輯時,會將UITableViewDelegate與UITableViewDataSource單獨放到一個Handler鍾進行處理,抽象出tableHander,由MVVMTableDataDelegate進行負責管理;
當然,事實上,實際開發中,每個tableView頁面都很複雜,有很多邏輯要處理,這時候只能考慮將protocol重新請回Controller中了,因爲View層與ViewController層本身是持有與被持有的依賴關係,所以任何類作爲ViewController的類內實例來實現協議回調,實際上都是在跨層調用,所以,隨着時間和業務邏輯的愈來愈複雜,就註定要以額外的接口爲代價,換言之,ViewController 的內聚性變差了。
  總之,具體情況具體分析,採用最合適的方式來處理應對不同的問題。兵來將擋,水來土掩。本文的相關Demo見github,實現的功能並不複雜,僅供參考,歡迎補充。

項目傳送門:https://github.com/lovemo/MVVMFramework

VIPER

從樂高玩具的搭建經驗轉換到 iOS app 的設計

VIPER是我們最後一個要介紹的架構,它不是MV(X)系列的架構。

到現在爲止,我們應該都覺得職責劃分的顆粒度還是不錯的。在 VIPER 中對於職責的劃分提出了另一種方式,這次我們有五層:

這裏寫圖片描述

  • Interactor–包括和數據相關的業務邏輯(Entities)或者網絡請求,比如創建entities 類的對象或者把它們從服務器中抓取出來。爲了達到這些目的你會用到通常會被看做外部依賴而不被看做 VIPER 單元的一些服務(Services)和管理者(Managers)
  • Presenter–包括 UI 相關(UIKit 之外)的一些業務邏輯,調用 Interactor 中的一些方法
  • Entities–純粹的數據對象,並非是數據訪問層,數據訪問是 Interactor 層的任務
  • Router–負責 VIPER 模塊之間的切換

它的特性:

(1)劃分(distribution)–毫無疑問,在層次職責劃分方面,VIPER 是最棒的
(2)可測試性–理所當然的,非常好的層次劃分帶來好的可測試性
(3)易用性–想你想的那樣,上述兩個方面都犧牲(系統)可維護性的,你需要編寫許多的僅有少量功能的接口

參考鏈接:
http://www.cocoachina.com/ios/20160108/14916.html
http://www.jianshu.com/p/87ac2f075a5b
http://www.jianshu.com/p/5161acddca53

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章