iOS架構(gòu)淺談從 MVC、MVP 到 MVVM

概述

做了這么多年的客戶端研發(fā)一直在使用蘋果爸爸推薦的MVC架構(gòu)模式掸屡。MVC從應(yīng)用層面進(jìn)行分層開(kāi)發(fā)封寞,極大優(yōu)化了我們的代碼結(jié)構(gòu),簡(jiǎn)單易上手折晦,很容易被程序員所接受钥星。程序員剛接手一個(gè)新項(xiàng)目,如果是MVC的架構(gòu)模式满着,會(huì)減少代碼熟悉時(shí)間谦炒,快速的進(jìn)行開(kāi)發(fā)和維護(hù)工作,實(shí)際上對(duì)于多人開(kāi)發(fā)維護(hù)的項(xiàng)目风喇,MVC仍然是非常好的架構(gòu)模式宁改,這也是這種架構(gòu)模式經(jīng)久不衰的原因。
但是任何事物都有兩面性魂莫,隨著項(xiàng)目需求的增加还蹲,業(yè)務(wù)邏輯、網(wǎng)絡(luò)請(qǐng)求耙考、代理方法等都往Controller層加塞谜喊,導(dǎo)致Controller層變得越來(lái)越臃腫,動(dòng)輒上千行的代碼量絕對(duì)是維護(hù)人員的噩夢(mèng)倦始,因此在MVC基礎(chǔ)上逐漸衍生出來(lái)了MVP斗遏、MVVM等架構(gòu)模式。

本文是基于OC代碼進(jìn)行闡述的鞋邑,使用iOS開(kāi)發(fā)經(jīng)典的 TableView 列表來(lái)分析每個(gè)架構(gòu)模式诵次。相信看了這篇文章你會(huì)有所領(lǐng)悟账蓉。當(dāng)然一千個(gè)人眼中有一千種哈姆雷特,具體在業(yè)務(wù)開(kāi)發(fā)中使用哪種模式需要你自己去衡量逾一。

1.傳統(tǒng)的MVC設(shè)計(jì)模式

MVC


M: Model 數(shù)據(jù)層铸本,負(fù)責(zé)網(wǎng)絡(luò)數(shù)據(jù)的處理,數(shù)據(jù)持久化存儲(chǔ)和讀取等工作

V: View 視圖層遵堵,負(fù)責(zé)呈現(xiàn)從數(shù)據(jù)層傳遞的數(shù)據(jù)渲染工作箱玷,以及與用戶的交互工作

C: Controller控制器,負(fù)責(zé)連接Model層跟View層鄙早,響應(yīng)View的事件和作為View的代理汪茧,以及界面跳轉(zhuǎn)和生命周期的處理等任務(wù)

用戶的交互邏輯

用戶點(diǎn)擊 View(視圖) --> 視圖響應(yīng)事件 -->通過(guò)代理傳遞事件到Controller-->發(fā)起網(wǎng)絡(luò)請(qǐng)求更新Model--->Model處理完數(shù)據(jù)-->代理或通知給Controller-->改變視圖樣式-->完成

可以看到Controller強(qiáng)引用View與Model,而View與Model是分離的限番,所以就可以保證Model和View的可測(cè)試性和復(fù)用性,但是Controller不行呀舔,因?yàn)镃ontroller是Model和View的中介弥虐,所以不能復(fù)用,或者說(shuō)很難復(fù)用媚赖。

iOS開(kāi)發(fā)實(shí)際使用的MVC架構(gòu)

實(shí)際MVC

在我們實(shí)際開(kāi)發(fā)中使用的MVC模式可以看到霜瘪,View與Controller耦合在一起了。這是由于每一個(gè)界面的創(chuàng)建都需要一個(gè)Controller惧磺,而每一個(gè)Controller里面必然會(huì)帶一個(gè)View颖对,這就導(dǎo)致了C和V的耦合。這種結(jié)構(gòu)確實(shí)可以提高開(kāi)發(fā)效率磨隘,但是一旦界面復(fù)雜就會(huì)造成Controller變得非常臃腫和難以維護(hù)缤底。

MVC代碼示例

我們要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的列表頁(yè)面,每行cell都一個(gè)按鈕番捂,點(diǎn)擊按鈕前面數(shù)字?1操作个唧。

mvcexamp

核心代碼:

// 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;
}
/*
* 用戶點(diǎn)擊事件通過(guò)Block傳遞過(guò)來(lái)后,在Controller層處理更新Mdoel以及更新視圖的邏輯
*/
- (void)changeNumWithModel:(DemoModel*)model{
    
    model.num++;
    NSIndexPath *path = [NSIndexPath indexPathForRow:model.Id inSection:0];
    [self.mainTabelView reloadRowsAtIndexPaths:@[path] withRowAnimation:UITableViewRowAnimationLeft];
}

  • 可以看到用戶點(diǎn)擊事件通過(guò)Block傳遞過(guò)來(lái)后设预,在Controller層處理更新Mdoel以及更新視圖的邏輯

2.MVP設(shè)計(jì)模式

MVP

M: Model 數(shù)據(jù)層徙歼,負(fù)責(zé)網(wǎng)絡(luò)數(shù)據(jù)的處理,數(shù)據(jù)持久化存儲(chǔ)和讀取等工作

V: View 視圖層鳖枕,負(fù)責(zé)呈現(xiàn)從數(shù)據(jù)層傳遞的數(shù)據(jù)渲染工作魄梯,以及與用戶的交互,這里把Controller層也合并到視圖層

P: Presenter層宾符,負(fù)責(zé)視圖需要數(shù)據(jù)的獲取酿秸,獲取到數(shù)據(jù)后刷新視圖。響應(yīng)View的事件和作為View的代理吸奴。

可以看到 MVP模式跟原始的MVC模式非常相似允扇,完全實(shí)現(xiàn)了View與Model層的分離缠局,而且把業(yè)務(wù)邏輯放在了Presenter層中,視圖需要的所有數(shù)據(jù)都從Presenter獲取考润,而View與 Presenter通過(guò)協(xié)議進(jìn)行事件的傳遞狭园。

用戶的交互邏輯

用戶點(diǎn)擊 View(視圖) --> 視圖響應(yīng)事件 -->通過(guò)代理傳遞事件到Presenter-->發(fā)起網(wǎng)絡(luò)請(qǐng)求更新Model-->Model處理完數(shù)據(jù)-->代理或通知給視圖(View或是Controller)-->改變視圖樣式-->完成

MVP代碼示例

項(xiàng)目結(jié)構(gòu)
//DemoProtocal
import <Foundation/Foundation.h>

@protocol DemoProtocal <NSObject>
@optional
//用戶點(diǎn)擊按鈕 觸發(fā)事件: UI改變傳值到model數(shù)據(jù)改變  UI --- > Model 點(diǎn)擊cell 按鈕
-(void)didClickCellAddBtnWithIndexPathRow:(NSInteger)index;
//model數(shù)據(jù)改變傳值到UI界面刷新 Model --- > UI
-(void)reloadUI;
@end
  • 我們把所有的代理抽象出來(lái),成為一個(gè)Protocal文件糊治。這兩個(gè)方法的作用:
  • -(void)didClickCellAddBtnWithIndexPathRow:(NSInteger)index;:Cell視圖調(diào)用它去Presenter層實(shí)現(xiàn)點(diǎn)擊邏輯的處理
  • -(void)reloadUI;: Presenter調(diào)用它去更新主視圖View或者Controller
//Presenter.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "DemoProtocal.h"

NS_ASSUME_NONNULL_BEGIN

@interface Presenter : NSObject
@property (nonatomic, strong,readonly) NSMutableArray *dataArray;
@property (nonatomic, weak) id<DemoProtocal>delegate;//協(xié)議唱矛,去更新主視圖UI
// 更新 TableView UI 根據(jù)需求
- (void)requestDataAndUpdateUI;
//更新 cell UI
- (void)updateCell:(UITableViewCell*)cell withIndex:(NSInteger)index;
@end
  • dataArray: 視圖需要的數(shù)據(jù)源
  • - (void)requestDataAndUpdateUI;:主視圖Controller調(diào)用,去更新自己的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 數(shù)據(jù)
    [self.presenter updateCell:cell withIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}

#pragma mark - DemoProtocal
//Presenter 的代理回調(diào) 數(shù)據(jù)更新了通知View去更新視圖
- (void)reloadUI{
    [self.mainTabelView reloadData];
}
  • Controller層初始化Presenter井辜,調(diào)用其方法更新自己的UI绎谦,可以看到網(wǎng)絡(luò)數(shù)據(jù)的獲取,處理都在Presenter中粥脚,處理完成后通過(guò)協(xié)議回調(diào)給Controller去reload數(shù)據(jù)
//Cell
- (void)addBtnDown:(UIButton*)btn{
    NSLog(@"%s",__func__);
    if([self.delegate respondsToSelector:@selector(didClickCellAddBtnWithIndexPathRow:)]){
        [self.delegate didClickCellAddBtnWithIndexPathRow:self.index];
    }
}
  • Cell層點(diǎn)擊事件通過(guò)協(xié)議調(diào)用窃肠,而這個(gè)協(xié)議方法的實(shí)現(xiàn)是在Presenter中實(shí)現(xiàn)的。

MVP模式也有自身的缺點(diǎn)刷允,所有的用戶操作和更新UI的回調(diào)需要定義冤留,隨著交互越來(lái)越復(fù)雜,這些定義都要有很大一坨代碼树灶。邏輯過(guò)于復(fù)雜的情況下纤怒,Present本身也會(huì)變得臃腫。所以衍生出了MVVM模式天通。

3.MVVM+RAC設(shè)計(jì)模式

MVVM


M: Model 數(shù)據(jù)層泊窘,負(fù)責(zé)網(wǎng)絡(luò)數(shù)據(jù)的處理,數(shù)據(jù)持久化存儲(chǔ)和讀取等工作

V: View 視圖層像寒,此時(shí)的視圖層包括Controller烘豹,負(fù)責(zé)呈現(xiàn)從數(shù)據(jù)層傳遞的數(shù)據(jù)渲染工作,以及與用戶的交互

VM:ViewModel層萝映,負(fù)責(zé)視圖需要數(shù)據(jù)的獲取吴叶,獲取到數(shù)據(jù)后刷新視圖。響應(yīng)View的事件和作為View的代理等工作序臂。

通過(guò)架構(gòu)圖可以看到蚌卤,MVVM模式跟MVP模式基本類似。主要區(qū)別是在MVP基礎(chǔ)上加入了雙向綁定機(jī)制奥秆。當(dāng)被綁定對(duì)象某個(gè)值的變化時(shí)逊彭,綁定對(duì)象會(huì)自動(dòng)感知,無(wú)需被綁定對(duì)象主動(dòng)通知綁定對(duì)象构订∥甓#可以使用KVO和RAC實(shí)現(xiàn)。我們這里采用了RAC的實(shí)現(xiàn)方式悼瘾。關(guān)于RAC如果不熟悉的小伙伴可以點(diǎn)這里,我們這篇文章不在涉及囊榜。

MVVM代碼示例

項(xiàng)目結(jié)構(gòu)

我們這里包括兩層視圖:主視圖Controller以及Cell审胸,分別對(duì)應(yīng)兩層ViewModel:ViewModel和CellViewModel

//ViewModel.h

@interface ViewModel : NSObject
//發(fā)送數(shù)據(jù)請(qǐng)求的Rac,可以去訂閱獲取 請(qǐng)求結(jié)果
@property (nonatomic,strong,readonly) RACCommand *requestCommand;
@property (nonatomic,strong) NSArray *dataArr;//返回子級(jí)對(duì)象的ViewModel
- (CellViewModel *)itemViewModelForIndex:(NSInteger)index;
@end
  • RACCommand *requestCommand:提供供主視圖調(diào)用的命令卸勺,調(diào)用它去獲取網(wǎng)絡(luò)數(shù)據(jù)
  • NSArray *dataArr: 提供供主視圖使用的數(shù)據(jù)源砂沛,注意這里不能用NSMutableArray,因?yàn)镹SMutableArray不支持KVO曙求,不能被RACObserve碍庵。
  • - (CellViewModel *)itemViewModelForIndex:(NSInteger)index; 根據(jù)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渲染需要的所有數(shù)據(jù)
  • RACCommand *addCommand;: 按鈕點(diǎn)擊事件的指令,觸發(fā)后需要在CellViewModel里面做處理悟狱。
//controller
- (void)iniData{
    self.viewModel = [[ViewModel alloc] init];
    // 發(fā)送請(qǐng)求
    RACSignal *signal = [self.viewModel.requestCommand execute:@{@"page":@"1"}];
    [signal subscribeNext:^(id x) {
        NSLog(@"x=======%@",x);
        if([x boolValue] == 1){//請(qǐng)求成功
            [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 數(shù)據(jù)
    cell.cellViewModel = [self.viewModel itemViewModelForIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
        
    return cell;
}
  • iniData:初始化ViewModel静浴,并發(fā)送請(qǐng)求命令。這里可以監(jiān)聽(tīng)這個(gè)完成信號(hào)挤渐,進(jìn)行刷新視圖操作
  • cell.cellViewModel = [self.viewModel itemViewModelForIndex:indexPath.row]; 根據(jù)主視圖的ViewModel去獲取Cell的ViewModel苹享,實(shí)現(xiàn)cell的數(shù)據(jù)綁定。
//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里面進(jìn)行與ViewModel的數(shù)據(jù)綁定挣菲,這邊有個(gè)注意Racobserve左邊只有self右邊才有viewModel.titleStr這樣就避Cell重用的問(wèn)題富稻。
  • [self.cellViewModel.addCommand execute:nil];:按鈕的點(diǎn)擊方法觸發(fā),事件的處理在CellViewModel中白胀。

總結(jié)

  • 經(jīng)過(guò)幾十年的發(fā)展和演變MVC模式出現(xiàn)了各種各樣的變種,并在不同的平臺(tái)上有著自己的實(shí)現(xiàn)抚岗。在實(shí)際項(xiàng)目開(kāi)發(fā)中或杠,根據(jù)具體的業(yè)務(wù)需求找到適合的架構(gòu)才是最好的,架構(gòu)本身并沒(méi)有好壞之分宣蔚。
  • 最后對(duì)文中的MVC向抢、MVP、MVVM架構(gòu)的描述也摻雜了作者的主觀意見(jiàn)胚委,如果對(duì)文中的內(nèi)容有疑問(wèn)挟鸠,歡迎提出不同的意見(jiàn)進(jìn)行討論。
  • 本文的Demo已上傳作者GitHub
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末亩冬,一起剝皮案震驚了整個(gè)濱河市艘希,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌硅急,老刑警劉巖覆享,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異营袜,居然都是意外死亡撒顿,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門荚板,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)凤壁,“玉大人吩屹,你說(shuō)我怎么就攤上這事∨《叮” “怎么了煤搜?”我有些...
    開(kāi)封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)徙鱼。 經(jīng)常有香客問(wèn)我宅楞,道長(zhǎng),這世上最難降的妖魔是什么袱吆? 我笑而不...
    開(kāi)封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任厌衙,我火速辦了婚禮,結(jié)果婚禮上绞绒,老公的妹妹穿的比我還像新娘婶希。我一直安慰自己,他們只是感情好蓬衡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布喻杈。 她就那樣靜靜地躺著,像睡著了一般狰晚。 火紅的嫁衣襯著肌膚如雪筒饰。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天壁晒,我揣著相機(jī)與錄音瓷们,去河邊找鬼。 笑死秒咐,一個(gè)胖子當(dāng)著我的面吹牛谬晕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播携取,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼攒钳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了雷滋?” 一聲冷哼從身側(cè)響起不撑,我...
    開(kāi)封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惊豺,沒(méi)想到半個(gè)月后燎孟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡尸昧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年揩页,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爆侣,死狀恐怖萍程,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兔仰,我是刑警寧澤茫负,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站乎赴,受9級(jí)特大地震影響忍法,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜榕吼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一饿序、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧羹蚣,春花似錦原探、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至胁出,卻和暖如春型型,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背全蝶。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工输莺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裸诽。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像型凳,于是被迫代替她去往敵國(guó)和親丈冬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容