概述
做了這么多年的客戶端研發(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ì)模式
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í)際開(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操作个唧。
核心代碼:
// 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ì)模式
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代碼示例
//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ì)模式
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代碼示例
我們這里包括兩層視圖:主視圖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