MVP
MVC的缺點在于并沒有區分業務邏輯和業務展示, 這對單元測試很不友好. MVP針對以上缺點做了優化, 它將業務邏輯和業務展示也做了一層隔離, 對應的就變成了MVCP. M和V功能不變, 原來的C現在只負責布局, 而所有的邏輯全都轉移到了P層.
對應關系如圖所示:
業務場景沒有變化, 依然是展示三種數據, 只是三個MVC替換成了三個MVP(圖中我只畫了Blog模塊), UserVC負責配置三個MVP(新建各自的VP, 通過VP建立C, C會負責建立VP之間的綁定關系), 并在合適的時機通知各自的P層(之前是通知C層)進行數據獲取, 各個P層在獲取到數據后進行相應處理, 處理完成后會通知綁定的View數據有所更新, V收到更新通知后從P獲取格式化好的數據進行頁面渲染, UserVC最后將已經渲染好的各個View進行布局即可. 另外, V層C層不再處理任何業務邏輯, 所有事件觸發全部調用P層的相應命令, 具體到代碼中如下:
@interface BlogPresenter : NSObject
+ (instancetype)instanceWithUserId:(NSUInteger)userId;
- (NSArray *)allDatas;//業務邏輯移到了P層 和業務相關的M也跟著到了P層
- (void)refreshUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
- (void)loadMoreUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
@end
@interface BlogPresenter()
@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) NSMutableArray *blogs;
@property (strong, nonatomic) UserAPIManager *apiManager;
@end
@implementation BlogPresenter
+ (instancetype)instanceWithUserId:(NSUInteger)userId {
return [[BlogPresenter alloc] initWithUserId:userId];
}
- (instancetype)initWithUserId:(NSUInteger)userId {
if (self = [super init]) {
self.userId = userId;
self.apiManager = [UserAPIManager new];
//...略
}
}
#pragma mark - Interface
- (NSArray *)allDatas {
return self.blogs;
}
//提供給外層調用的命令
- (void)refreshUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[self.apiManager refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (!error) {
[self.blogs removeAllObjects];//清空之前的數據
for (Blog *blog in result) {
[self.blogs addObject:[BlogCellPresenter presenterWithBlog:blog]];
}
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
//提供給外層調用的命令
- (void)loadMoreUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[self.apiManager loadMoreUserBlogsWithUserId:self.userId completionHandler...]
}
@end
@interface BlogCellPresenter : NSObject
+ (instancetype)presenterWithBlog:(Blog *)blog;
- (NSString *)authorText;
- (NSString *)likeCountText;
- (void)likeBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
- (void)shareBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
@end
@implementation BlogCellPresenter
- (NSString *)likeCountText {
return [NSString stringWithFormat:@"贊 %ld", self.blog.likeCount];
}
- (NSString *)authorText {
return [NSString stringWithFormat:@"作者姓名: %@", self.blog.authorName];
}
//? ? ...略
- (void)likeBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[[UserAPIManager new] likeBlogWithBlogId:self.blogId userId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
//do fail
} else {
//do success
self.blog.likeCount += 1;
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
//? ? ...略
@end
BlogPresenter和BlogCellPresenter分別作為BlogViewController和BlogCell的P層, 其實就是一系列業務邏輯的集合. BlogPresenter負責獲取Blogs原始數據并通過這些原始數據構造BlogCellPresenter, 而BlogCellPresenter提供格式化好的各種數據以供Cell渲染, 另外, 點贊和分享的業務現在也轉移到了這里.
業務邏輯被轉移到了P層, 此時的V層只需要做兩件事:
1.監聽P層的數據更新通知, 刷新頁面展示.
2.在點擊事件觸發時, 調用P層的對應方法, 并對方法執行結果進行展示.
@interface BlogCell : UITableViewCell
@property (strong, nonatomic) BlogCellPresenter *presenter;
@end
@implementation BlogCell
- (void)setPresenter:(BlogCellPresenter *)presenter {
_presenter = presenter;
//從Presenter獲取格式化好的數據進行展示
self.authorLabel.text = presenter.authorText;
self.likeCountLebel.text = presenter.likeCountText;
//? ? ...略
}
#pragma mark - Action
- (void)onClickLikeButton:(UIButton *)sender {
[self.presenter likeBlogWithCompletionHandler:^(NSError *error, id result) {
if (!error) {//頁面刷新
self.likeCountLebel.text = self.presenter.likeCountText;
}
//? ? ? ? ...略
}];
}
@end
而C層做的事情就是布局和PV之間的綁定(這里可能不太明顯, 因為BlogVC里面的布局代碼是TableViewDataSource, PV綁定的話, 因為我偷懶用了Block做通知回調, 所以也不太明顯, 如果是Protocol回調就很明顯了), 代碼如下:
@interface BlogViewController : NSObject
+ (instancetype)instanceWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter;
- (void)setDidSelectRowHandler:(void (^)(Blog *))didSelectRowHandler;
- (void)fetchDataWithCompletionHandler:(NetworkCompletionHandler)completionHandler;
@end
BlogViewController現在不再負責實際的數據獲取邏輯, 數據獲取直接調用Presenter的相應接口, 另外, 因為業務邏輯也轉移到了Presenter, 所以TableView的布局用的也是Presenter.allDatas. 至于Cell的展示, 我們替換了原來大量的Set方法, 讓Cell自己根據綁定的CellPresenter做展示. 畢竟現在邏輯都移到了P層, V層要做相應的交互也必須依賴對應的P層命令, 好在V和M仍然是隔離的, 只是和P耦合了, P層是可以隨意替換的, M顯然不行, 這是一種折中.
最后是Scene, 它的變動不大, 只是替換配置MVC為配置MVP, 另外數據獲取也是走P層, 不走C層了(然而代碼里面并不是這樣的):
- (void)configuration {
//? ? ...其他設置
BlogPresenter *blogPresenter = [BlogPresenter instanceWithUserId:self.userId];
self.blogViewController = [BlogViewController instanceWithTableView:self.blogTableView presenter:blogPresenter];
[self.blogViewController setDidSelectRowHandler:^(Blog *blog) {
[self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:blog] animated:YES];
}];
//? ? ...略
}
- (void)fetchData {
//? ? ? ? ...略
[self.userInfoVC fetchData];
[HUD show];
[self.blogViewController fetchDataWithCompletionHandler:^(NSError *error, id result) {
[HUD hide];
}];
//還是因為懶, 用了Block走C層轉發會少寫一些代碼, 如果是Protocol或者KVO方式就會用self.blogViewController.presenter了
//不過沒有關系, 因為我們替換MVC為MVP是為了解決單元測試的問題, 現在的用法完全不影響單元測試, 只是和概念不符罷了.
//? ? ? ? ...略
}
上面的例子中其實有一個問題, 即我們假定: 所有的事件都是由V層主動發起且一次性的. 這其實是不成立的, 舉個簡單的例子: 類似微信語音聊天之類的頁面, 點擊語音Cell開始播放, Cell展示播放動畫, 播放完成動畫停止, 然后播放下一條語音.
在這個播放場景中, 如果CellPresenter還是像上面一樣僅僅提供一個playWithCompletionHandler的接口是行不通的. 因為播放完成后回調肯定是在C層, C層在播放完成后會發現此時執行播放命令的CellPresenter無法通知Cell停止動畫, 即事件的觸發不是一次性的. 另外, 在播放完成后, C層遍歷到下一個待播放CellPresenterX調用播放接口時, CellPresenterX因為并不知道它對應的Cell是誰, 當然也就無法通知Cell開始動畫, 即事件的發起者并不一定是V層.
針對這些非一次性或者其他層發起事件, 處理方法其實很簡單, 在CellPresenter加個Block屬性就行了, 因為是屬性, Block可以多次回調, 另外Block還可以捕獲Cell, 所以也不擔心找不到對應的Cell. 大概這樣:
@interface VoiceCellPresenter : NSObject
@property (copy, nonatomic) void(^didUpdatePlayStateHandler)(NSUInteger);
- (NSURL *)playURL;
@end
@implementation VoiceCell
- (void)setPresenter:(VoiceCellPresenter *)presenter {
_presenter = presenter;
if (!presenter.didUpdatePlayStateHandler) {
__weak typeof(self) weakSelf = self;
[presenter setDidUpdatePlayStateHandler:^(NSUInteger playState) {
switch (playState) {
case Buffering: weakSelf.playButton... break;
case Playing: weakSelf.playButton... break;
case Paused: weakSelf.playButton... break;
}
}];
}
}
播放的時候, VC只需要保持一下CellPresenter, 然后傳入相應的playState調用didUpdatePlayStateHandler就可以更新Cell的狀態了.
當然, 如果是Protocol的方式進行的VP綁定, 那么做這些事情就很平常了, 就不寫了.
MVP大概就是這個樣子了, 相對于MVC, 它其實只做了一件事情, 即分割業務展示和業務邏輯. 展示和邏輯分開后, 只要我們能保證V在收到P的數據更新通知后能正常刷新頁面, 那么整個業務就沒有問題. 因為V收到的通知其實都是來自于P層的數據獲取/更新操作, 所以我們只要保證P層的這些操作都是正常的就可以了. 即我們只用測試P層的邏輯, 不必關心V層的情況.
MVVM
MVP其實已經是一個很好的架構, 幾乎解決了所有已知的問題, 那么為什么還會有MVVM呢?
仍然是舉例說明, 假設現在有一個Cell, 點擊Cell上面的關注按鈕可以是加關注, 也可以是取消關注, 在取消關注時, SceneA要求先彈窗詢問, 而SceneB則不做彈窗, 那么此時的取消關注操作就和業務場景強關聯, 所以這個接口不可能是V層直接調用, 會上升到Scene層.具體到代碼中, 大概這個樣子:
@interface UserCellPresenter : NSObject
@property (copy, nonatomic) void(^followStateHander)(BOOL isFollowing);
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
@implementation UserCellPresenter
- (void)follow {
if (!self.isFollowing) {//未關注 去關注
//? ? ? ? follow user
} else {//已關注 則取消關注
self.followStateHander ? self.followStateHander(YES) : nil;//先通知Cell顯示follow狀態
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
self.followStateHander ? self.followStateHander(NO) : nil;//follow失敗 狀態回退
} eles {
self.isFollowing = YES;
}
//...略
}];
}
}
@end
@implementation UserCell
- (void)setPresenter:(UserCellPresenter *)presenter {
_presenter = presenter;
if (!_presenter.followStateHander) {
__weak typeof(self) weakSelf = self;
[_presenter setFollowStateHander:^(BOOL isFollowing) {
[weakSelf.followStateButton setImage:isFollowing ? : ...];
}];
}
}
- (void)onClickFollowButton:(UIButton *)button {//將關注按鈕點擊事件上傳
[self routeEvent:@"followEvent" userInfo:@{@"presenter" : self.presenter}];
}
@end
@implementation FollowListViewController
//攔截點擊事件 判斷后確認是否執行事件
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
if ([eventName isEqualToString:@"followEvent"]) {
UserCellPresenter *presenter = userInfo[@"presenter"];
[self showAlertWithTitle:@"提示" message:@"確認取消對他的關注嗎?" cancelHandler:nil confirmHandler: ^{
[presenter follow];
}];
}
}
@end
@implementation UIResponder (Router)
//沿著響應者鏈將事件上傳 事件最終被攔截處理 或者 無人處理直接丟棄
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
[self.nextResponder routeEvent:eventName userInfo:userInfo];
}
@end
Block方式看起來略顯繁瑣, 我們換到Protocol看看:
@protocol UserCellPresenterCallBack
- (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing;
@end
@interface UserCellPresenter : NSObject
@property (weak, nonatomic) id view;
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
@implementation UserCellPresenter
- (void)follow {
if (!self.isFollowing) {//未關注 去關注
//? ? ? ? follow user
} else {//已關注 則取消關注
BOOL isResponse = [self.view respondsToSelector:@selector(userCellPresenterDidUpdateFollowState)];
isResponse ? [self.view userCellPresenterDidUpdateFollowState:YES] : nil;
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
isResponse ? [self.view userCellPresenterDidUpdateFollowState:NO] : nil;
} eles {
self.isFollowing = YES;
}
//...略
}];
}
}
@end
@implementation UserCell
- (void)setPresenter:(UserCellPresenter *)presenter {
_presenter = presenter;
_presenter.view = self;
}
#pragma mark - UserCellPresenterCallBack
- (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing {
[self.followStateButton setImage:isFollowing ? : ...];
}
除去Route和VC中Alert之類的代碼, 可以發現無論是Block方式還是Protocol方式因為需要對頁面展示和業務邏輯進行隔離, 代碼上饒了一小圈, 無形中增添了不少的代碼量, 這里僅僅只是一個事件就這樣, 如果是多個呢? 那寫起來真是蠻傷的…
仔細看一下上面的代碼就會發現, 如果我們繼續添加事件, 那么大部分的代碼都是在做一件事情: P層將數據更新通知到V層. Block方式會在P層添加很多屬性, 在V層添加很多設置Block邏輯. 而Protocol方式雖然P層只添加了一個屬性, 但是Protocol里面的方法卻會一直增加, 對應的V層也就需要增加的方法實現.
問題既然找到了, 那就試著去解決一下吧, OC中能夠實現兩個對象間的低耦合通信, 除了Block和Protocol, 一般都會想到KVO. 我們看看KVO在上面的例子有何表現:
@interface UserCellViewModel : NSObject
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
@implementation UserCellViewModel
- (void)follow {
if (!self.isFollowing) {//未關注 去關注
//? ? ? ? follow user
} else {//已關注 則取消關注
self.isFollowing = YES;//先通知Cell顯示follow狀態
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) { self.isFollowing = NO; }//follow失敗 狀態回退
//...略
}];
}
}
@end
@implementation UserCell
- (void)awakeFromNib {
@weakify(self);
[RACObserve(self, viewModel.isFollowing) subscribeNext:^(NSNumber *isFollowing) {
@strongify(self);
[self.followStateButton setImage:[isFollowing boolValue] ? : ...];
};
}
代碼大概少了一半左右, 另外, 邏輯讀起來也清晰多了, Cell觀察綁定的ViewModel的isFollowing狀態, 并在狀態改變時, 更新自己的展示.
三種數據通知方式簡單一比對, 相信哪種方式對程序員更加友好, 大家都心里有數, 就不做贅述了.
現在大概一提到MVVM就會想到RAC, 但這兩者其實并沒有什么聯系, 對于MVVM而言RAC只是提供了優雅安全的數據綁定方式, 如果不想學RAC, 自己搞個KVOHelper之類的東西也是可以的. 另外 ,RAC的魅力其實在于函數式響應式編程, 我們不應該僅僅將它局限于MVVM的應用, 日常的開發中也應該多使用使用的.
關于MVVM, 我想說的就是這么多了, 因為MVVM其實只是MVP的綁定進化體, 除去數據綁定方式, 其他的和MVP如出一轍, 只是可能呈現方式是Command/Signal而不是CompletionHandler之類的, 故不做贅述.
最后做個簡單的總結吧:
1.MVC作為老牌架構, 優點在于將業務場景按展示數據類型劃分出多個模塊, 每個模塊中的C層負責業務邏輯和業務展示, 而M和V應該是互相隔離的以做重用, 另外每個模塊處理得當也可以作為重用單元. 拆分在于解耦, 順便做了減負, 隔離在于重用, 提升開發效率. 缺點是沒有區分業務邏輯和業務展示, 對單元測試不友好.
2.MVP作為MVC的進階版, 提出區分業務邏輯和業務展示, 將所有的業務邏輯轉移到P層, V層接受P層的數據更新通知進行頁面展示. 優點在于良好的分層帶來了友好的單元測試, 缺點在于分層會讓代碼邏輯優點繞, 同時也帶來了大量的代碼工作, 對程序員不夠友好.
3.MVVM作為集大成者, 通過數據綁定做數據更新, 減少了大量的代碼工作, 同時優化了代碼邏輯, 只是學習成本有點高, 對新手不夠友好.
4.MVP和MVVM因為分層所以會建立MVC兩倍以上的文件類, 需要良好的代碼管理方式.
5.在MVP和MVVM中, V和P或者VM之間理論上是多對多的關系, 不同的布局在相同的邏輯下只需要替換V層, 而相同的布局不同的邏輯只需要替換P或者VM層. 但實際開發中P或者VM往往因為耦合了V層的展示邏輯退化成了一對一關系(比如SceneA中需要顯示”xxx+Name”, VM就將Name格式化為”xxx + Name”. 某一天SceneB也用到這個模塊, 所有的點擊事件和頁面展示都一樣, 只是Name展示為”yyy + Name”, 此時的VM因為耦合SceneA的展示邏輯, 就顯得比較尷尬), 針對此類情況, 通常有兩種辦法, 一種是在VM層加狀態進而判斷輸出狀態, 一種是在VM層外再加一層FormatHelper. 前者可能因為狀態過多顯得代碼難看, 后者雖然比較優雅且拓展性高, 但是過多的分層在數據還原時就略顯笨拙, 大家應該按需選擇.
這里隨便瞎扯一句, 有些文章上來就說MVVM是為了解決C層臃腫, MVC難以測試的問題, 其實并不是這樣的. 按照架構演進順序來看, C層臃腫大部分是沒有拆分好MVC模塊, 好好拆分就行了, 用不著MVVM. 而MVC難以測試也可以用MVP來解決, 只是MVP也并非完美, 在VP之間的數據交互太繁瑣, 所以才引出了MVVM. 當MVVM這個完全體出現以后, 我們從結果看起源, 發現它做了好多事情, 其實并不是, 它的前輩們付出的努力也并不少!
架構那么多, 日常開發中到底該如何選擇?
不管是MVC, MVP, MVVM還是MVXXX, 最終的目的在于服務于人, 我們注重架構, 注重分層都是為了開發效率, 說到底還是為了開心. 所以, 在實際開發中不應該拘泥于某一種架構, 根據實際項目出發, 一般普通的MVC就能應對大部分的開發需求, 至于MVP和MVVM, 可以嘗試, 但不要強制.
總之, 希望大家能做到: 設計時, 心中有數. 擼碼時, 開心就好.