iOS架構淺談從 MVC、MVP 到 MVVM

germo發表於2021-09-09

概述

做了這麼多年的客戶端研發一直在使用蘋果爸爸推薦的MVC架構模式。MVC從應用層面進行分層開發,極大最佳化了我們的程式碼結構,簡單易上手,很容易被程式設計師所接受。程式設計師剛接手一個新專案,如果是MVC的架構模式,會減少程式碼熟悉時間,快速的進行開發和維護工作,實際上對於多人開發維護的專案,MVC仍然是非常好的架構模式,這也是這種架構模式經久不衰的原因。
但是任何事物都有兩面性,隨著專案需求的增加,業務邏輯、網路請求、代理方法等都往Controller層加塞,導致Controller層變得越來越臃腫,動輒上千行的程式碼量絕對是維護人員的噩夢,因此在MVC基礎上逐漸衍生出來了MVP、MVVM等架構模式。
本文是基於OC程式碼進行闡述的,使用iOS開發經典的 TableView 列表來分析每個架構模式。相信看了這篇文章你會有所領悟。當然一千個人眼中有一千種哈姆雷特,具體在業務開發中使用哪種模式需要你自己去衡量。

1.傳統的MVC設計模式

圖片描述
M: Model 資料層,負責網路資料的處理,資料持久化儲存和讀取等工作
V: View 檢視層,負責呈現從資料層傳遞的資料渲染工作,以及與使用者的互動工作
C: Controller控制器,負責連線Model層跟View層,響應View的事件和作為View的代理,以及介面跳轉和生命週期的處理等任務

使用者的互動邏輯

使用者點選 View(檢視) --> 檢視響應事件 -->透過代理傳遞事件到Controller–>發起網路請求更新Model—>Model處理完資料–>代理或通知給Controller–>改變檢視樣式–>完成

可以看到Controller強引用View與Model,而View與Model是分離的,所以就可以保證Model和View的可測試性和複用性,但是Controller不行,因為Controller是Model和View的中介,所以不能複用,或者說很難複用。

iOS開發實際使用的MVC架構

圖片描述 在我們實際開發中使用的MVC模式可以看到,View與Controller耦合在一起了。這是由於每一個介面的建立都需要一個Controller,而每一個Controller裡面必然會帶一個View,這就導致了C和V的耦合。這種結構確實可以提高開發效率,但是一旦介面複雜就會造成Controller變得非常臃腫和難以維護。

MVC程式碼示例

我們要實現一個簡單的列表頁面,每行cell都一個按鈕,點選按鈕前面數字➕1操作。
圖片描述 核心程式碼:

// Controller
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    __weak typeof(self) wealSelf = self;
    MVCTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell_identifer"];
    if(cell == nil){
        cell = [[MVCTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell_identifer"];
    }
    DemoModel *model = self.dataArray[indexPath.row];
    [cell loadDataWithModel:model];
    cell.clickBtn = ^{
        NSLog(@"id===%ld",model.num);
        [wealSelf changeNumWithModel:model];
    };
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}
/*
* 使用者點選事件透過Block傳遞過來後,在Controller層處理更新Mdoel以及更新檢視的邏輯
*/
- (void)changeNumWithModel:(DemoModel*)model{
    
    model.num++;
    NSIndexPath *path = [NSIndexPath indexPathForRow:model.Id inSection:0];
    [self.mainTabelView reloadRowsAtIndexPaths:@[path] withRowAnimation:UITableViewRowAnimationLeft];
}

複製程式碼
  • 可以看到使用者點選事件透過Block傳遞過來後,在Controller層處理更新Mdoel以及更新檢視的邏輯

2.MVP設計模式

圖片描述M: Model 資料層,負責網路資料的處理,資料持久化儲存和讀取等工作
V: View 檢視層,負責呈現從資料層傳遞的資料渲染工作,以及與使用者的互動,這裡把Controller層也合併到檢視層
P: Presenter層,負責檢視需要資料的獲取,獲取到資料後重新整理檢視。響應View的事件和作為View的代理。

可以看到 MVP模式跟原始的MVC模式非常相似,完全實現了View與Model層的分離,而且把業務邏輯放在了Presenter層中,檢視需要的所有資料都從Presenter獲取,而View與 Presenter透過協議進行事件的傳遞。

使用者的互動邏輯

使用者點選 View(檢視) --> 檢視響應事件 -->透過代理傳遞事件到Presenter–>發起網路請求更新Model–>Model處理完資料–>代理或通知給檢視(View或是Controller)–>改變檢視樣式–>完成

MVP程式碼示例

圖片描述

//DemoProtocal
import 

@protocol DemoProtocal 
@optional
//使用者點選按鈕 觸發事件: UI改變傳值到model資料改變  UI --- > Model 點選cell 按鈕
-(void)didClickCellAddBtnWithIndexPathRow:(NSInteger)index;
//model資料改變傳值到UI介面重新整理 Model --- > UI
-(void)reloadUI;
@end
複製程式碼
  • 我們把所有的代理抽象出來,成為一個Protocal檔案。這兩個方法的作用:
  • -(void)didClickCellAddBtnWithIndexPathRow:(NSInteger)index;:Cell檢視呼叫它去Presenter層實現點選邏輯的處理
  • -(void)reloadUI;: Presenter呼叫它去更新主檢視View或者Controller
//Presenter.h
#import 
#import 
#import "DemoProtocal.h"

NS_ASSUME_NONNULL_BEGIN

@interface Presenter : NSObject
@property (nonatomic, strong,readonly) NSMutableArray *dataArray;
@property (nonatomic, weak) iddelegate;//協議,去更新主檢視UI
// 更新 TableView UI 根據需求
- (void)requestDataAndUpdateUI;
//更新 cell UI
- (void)updateCell:(UITableViewCell*)cell withIndex:(NSInteger)index;
@end
複製程式碼
  • dataArray : 檢視需要的資料來源
  • - (void)requestDataAndUpdateUI;:主檢視Controller呼叫,去更新自己的UI
  • - (void)updateCell:(UITableViewCell*)cell withIndex:(NSInteger)index;:更新 Cell的UI
//Controller 層
- (void)iniData{
    self.presenter = [[Presenter alloc] init];
    self.presenter.delegate = self;
    [self.presenter requestDataAndUpdateUI];
}
...

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.presenter.dataArray.count;
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    MVPTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell_identifer"];
    if(cell == nil){
        cell = [[MVPTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell_identifer"];
    }
    //更新cell UI 資料
    [self.presenter updateCell:cell withIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}

#pragma mark - DemoProtocal
//Presenter 的代理回撥 資料更新了通知View去更新檢視
- (void)reloadUI{
    [self.mainTabelView reloadData];
}
複製程式碼
  • Controller層初始化Presenter,呼叫其方法更新自己的UI,可以看到網路資料的獲取,處理都在Presenter中,處理完成後透過協議回撥給Controller去reload資料
//Cell
- (void)addBtnDown:(UIButton*)btn{
    NSLog(@"%s",__func__);
    if([self.delegate respondsToSelector:@selector(didClickCellAddBtnWithIndexPathRow:)]){
        [self.delegate didClickCellAddBtnWithIndexPathRow:self.index];
    }
}
複製程式碼
  • Cell層點選事件透過協議呼叫,而這個協議方法的實現是在Presenter中實現的。

MVP模式也有自身的缺點,所有的使用者操作和更新UI的回撥需要定義,隨著互動越來越複雜,這些定義都要有很大一坨程式碼。邏輯過於複雜的情況下,Present本身也會變得臃腫。所以衍生出了MVVM模式。

3.MVVM+RAC設計模式

圖片描述
M: Model 資料層,負責網路資料的處理,資料持久化儲存和讀取等工作
V: View 檢視層,此時的檢視層包括Controller,負責呈現從資料層傳遞的資料渲染工作,以及與使用者的互動
VM:ViewModel層,負責檢視需要資料的獲取,獲取到資料後重新整理檢視。響應View的事件和作為View的代理等工作。
透過架構圖可以看到,MVVM模式跟MVP模式基本類似。主要區別是在MVP基礎上加入了雙向繫結機制。當被繫結物件某個值的變化時,繫結物件會自動感知,無需被繫結物件主動通知繫結物件。可以使用KVO和RAC實現。我們這裡採用了RAC的實現方式。關於RAC如果不熟悉的小夥伴可以點,我們這篇文章不在涉及。

MVVM程式碼示例

圖片描述

我們這裡包括兩層檢視:主檢視Controller以及Cell,分別對應兩層ViewModel:ViewModel和CellViewModel

//ViewModel.h

@interface ViewModel : NSObject
//傳送資料請求的Rac,可以去訂閱獲取 請求結果
@property (nonatomic,strong,readonly) RACCommand *requestCommand;
@property (nonatomic,strong) NSArray *dataArr;//返回子級物件的ViewModel
- (CellViewModel *)itemViewModelForIndex:(NSInteger)index;
@end
複製程式碼
  • RACCommand *requestCommand:提供供主檢視呼叫的命令,呼叫它去獲取網路資料
  • NSArray *dataArr: 提供供主檢視使用的資料來源,注意這裡不能用NSMutableArray,因為NSMutableArray不支援KVO,不能被RACObserve。
  • - (CellViewModel *)itemViewModelForIndex:(NSInteger)index; 根據Cell的index返回它需要的的ViewModel
@interface CellViewModel : NSObject

@property (nonatomic,copy,readonly) NSString *titleStr;

@property (nonatomic,copy,readonly) NSString *numStr;

@property (nonatomic,copy,readonly) RACCommand *addCommand;

- (instancetype)initWithModel:(DemoModel *)model;

@end
複製程式碼
  • CellViewModel: 暴露出Cell渲染需要的所有資料
  • RACCommand *addCommand;: 按鈕點選事件的指令,觸發後需要在CellViewModel裡面做處理。
//controller
- (void)iniData{
    self.viewModel = [[ViewModel alloc] init];
    // 傳送請求
    RACSignal *signal = [self.viewModel.requestCommand execute:@{@"page":@"1"}];
    [signal subscribeNext:^(id x) {
        NSLog(@"x=======%@",x);
        if([x boolValue] == 1){//請求成功
            [self.mainTabelView reloadData];
        }
    }];
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    MVVMTableVIewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell_identifer"];
    if(cell == nil){
        cell = [[MVVMTableVIewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell_identifer"];
    }
    //更新cell UI 資料
    cell.cellViewModel = [self.viewModel itemViewModelForIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
        
    return cell;
}
複製程式碼
  • iniData:初始化ViewModel,併傳送請求命令。這裡可以監聽這個完成訊號,進行重新整理檢視操作
  • cell.cellViewModel = [self.viewModel itemViewModelForIndex:indexPath.row]; 根據主檢視的ViewModel去獲取Cell的ViewModel,實現cell的資料繫結。
//TableViewCell

    RAC(self.titleLabel,text) = RACObserve(self, cellViewModel.titleStr);
    RAC(self.numLabel,text) = RACObserve(self, cellViewModel.numStr);

    [[self.addBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        NSLog(@">>>>>");
        [self.cellViewModel.addCommand execute:nil];
    }];
複製程式碼
  • 在Cell裡面進行與ViewModel的資料繫結,這邊有個注意Racobserve左邊只有self右邊才有viewModel.titleStr這樣就避Cell重用的問題。
  • [self.cellViewModel.addCommand execute:nil];:按鈕的點選方法觸發,事件的處理在CellViewModel中。

總結

  • 經過幾十年的發展和演變MVC模式出現了各種各樣的變種,並在不同的平臺上有著自己的實現。在實際專案開發中,根據具體的業務需求找到適合的架構才是最好的,架構本身並沒有好壞之分。
  • 最後對文中的MVC、MVP、MVVM架構的描述也摻雜了作者的主觀意見,如果對文中的內容有疑問,歡迎提出不同的意見進行討論。

作者:QiShare
連結:
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1868/viewspace-2797149/,如需轉載,請註明出處,否則將追究法律責任。

相關文章