雜談: MVC/MVP/MVVM

從簡書遷移到掘金...git

前言

本文爲回答一位朋友關於MVC/MVP/MVVM架構方面的疑問所寫, 旨在介紹iOS下MVC/MVP/MVVM三種架構的設計思路以及各自的優缺點. 全文約五千字, 預計花費閱讀時間20 - 30分鐘.程序員

MVC

  • MVC的相關概念 MVC最先存在於桌面程序中的, M是指業務數據, V是指用戶界面, C則是控制器. 在具體的業務場景中, C做爲M和V之間的鏈接, 負責獲取輸入的業務數據, 而後將處理後的數據輸出到界面上作相應展現, 另外, 在數據有所更新時, C還須要及時提交相應更新到界面展現. 在上述過程當中, 由於M和V之間是徹底隔離的, 因此在業務場景切換時, 一般只須要替換相應的C, 複用已有的M和V即可快速搭建新的業務場景. MVC因其複用性, 大大提升了開發效率, 現已被普遍應用在各端開發中.

概念過完了, 下面來看看, 在具體的業務場景中MVC/MVP/MVVM都是如何表現的.github

  • MVC之消失的C層

上圖中的頁面(業務場景)或者相似頁面相信你們作過很多, 各個程序員的具體實現方式可能各不同, 這裏說說我所看到的部分程序員的寫法:

//UserVC
- (void)viewDidLoad {
    [super viewDidLoad];

    [[UserApi new] fetchUserInfoWithUserId:132 completionHandler:^(NSError *error, id result) {
        if (error) {
            [self showToastWithText:@"獲取用戶信息失敗了~"];
        } else {
            
            self.userIconIV.image = ...
            self.userSummaryLabel.text = ...
            ...
        }
    }];
    
    [[userApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) {
        if (error) {
            [self showErrorInView:self.tableView info:...];
        } else {
            
            [self.blogs addObjectsFromArray:result];
            [self.tableView reloadData];
        }
    }];
}
//...略
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"];
    cell.blog = self.blogs[indexPath.row];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:self.blogs[indexPath.row]] animated:YES];
}
//...略
複製代碼
//BlogCell
- (void)setBlog:(Blog)blog {
    _blog = blog;
    
    self.authorLabel.text = blog.blogAuthor;
    self.likeLebel.text = [NSString stringWithFormat:@"贊 %ld", blog.blogLikeCount];
    ...
}
複製代碼

程序員很快寫完了代碼, Command+R一跑, 沒有問題, 心滿意足的作其餘事情去了. 後來有一天, 產品要求這個業務須要改動, 用戶在看他人信息時是上圖中的頁面, 看本身的信息時, 多一個草稿箱的展現, 像這樣:編程

屏幕快照 2017-03-04 下午3.46.40.png
因而小白將代碼改爲這樣:

//UserVC
- (void)viewDidLoad {
    [super viewDidLoad];

    if (self.userId != LoginUserId) {
        self.switchButton.hidden = self.draftTableView.hidden = YES;
        self.blogTableView.frame = ...
    }

    [[UserApi new] fetchUserI......略
    [[UserApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) {
        //if Error...略
        [self.blogs addObjectsFromArray:result];
        [self.blogTableView reloadData];
        
    }];
    
    [[userApi new] fetchUserDraftsWithUserId:132 completionHandler:^(NSError *error, id result) {
        //if Error...略
        [self.drafts addObjectsFromArray:result];
        [self.draftTableView reloadData];
    }];
}
     
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
     return tableView == self.blogTableView ? self.blogs.count : self.drafts.count;
}
     
//...略
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    if (tableView == self.blogTableView) {
        BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"];
        cell.blog = self.blogs[indexPath.row];
        return cell;
    } else {
        DraftCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DraftCell"];
        cell.draft = self.drafts[indexPath.row];
        return cell;
    }
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (tableView == self.blogTableView) ...
}
//...略
複製代碼
//DraftCell
- (void)setDraft:(draft)draft {
    _draft = draft;
    self.draftEditDate = ...
}

//BlogCell
- (void)setBlog:(Blog)blog {
    ...同上
}
複製代碼

後來啊, 產品以爲用戶看本身的頁面再加個回收站什麼的會很好, 因而程序員又加上一段代碼邏輯 , 再後來... 隨着需求的變動, UserVC變得愈來愈臃腫, 愈來愈難以維護, 拓展性和測試性也極差. 程序員也發現好像代碼寫得有些問題, 可是問題具體出在哪裏? 難道這不是MVC嗎? 咱們將上面的過程用一張圖來表示:api

屏幕快照 2017-03-04 下午4.35.35.png
經過這張圖能夠發現, 用戶信息頁面做爲業務場景Scene須要展現多種數據M(Blog/Draft/UserInfo), 因此對應的有多個View(blogTableView/draftTableView/image...), 可是, 每一個MV之間並無一個鏈接層C, 原本應該分散到各個C層處理的邏輯所有被打包丟到了Scene這一個地方處理, 也就是M-C-V變成了MM...-Scene-...VV, C層就這樣莫名其妙的消失了.

另外, 做爲V的兩個cell直接耦合了M(blog/draft), 這意味着這兩個V的輸入被綁死到了相應的M上, 複用無從談起. 最後, 針對這個業務場景的測試異常麻煩, 由於業務初始化和銷燬被綁定到了VC的生命週期上, 而相應的邏輯也關聯到了和View的點擊事件, 測試只能Command+R, 點點點...安全

  • 正確的MVC使用姿式

也許是UIViewController的類名給新人帶來了迷惑, 讓人誤覺得VC就必定是MVC中的C層, 又或許是Button, Label之類的View太過簡單徹底不須要一個C層來配合, 總之, 我工做以來經歷的項目中見過太多這樣的"MVC". 那麼, 什麼纔是正確的MVC使用姿式呢? 仍以上面的業務場景舉例, 正確的MVC應該是這個樣子的:bash

屏幕快照 2017-03-04 下午6.42.04.png
UserVC做爲業務場景, 須要展現三種數據, 對應的就有三個MVC, 這三個MVC負責各自模塊的數據獲取, 數據處理和數據展現, 而UserVC須要作的就是配置好這三個MVC, 並在合適的時機通知各自的C層進行數據獲取, 各個C層拿到數據後進行相應處理, 處理完成後渲染到各自的View上, UserVC最後將已經渲染好的各個View進行佈局便可, 具體到代碼中以下:

@interface BlogTableViewHelper : NSObject<UITableViewDelegate, UITableViewDataSource>

+ (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId;

- (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander;
- (void)setVCGenerator:(ViewControllerGenerator)VCGenerator;

@end
複製代碼
@interface BlogTableViewHelper()

@property (weak, nonatomic) UITableView *tableView;
@property (copy, nonatomic) ViewControllerGenerator VCGenerator;

@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) NSMutableArray *blogs;
@property (strong, nonatomic) UserAPIManager *apiManager;

@end
#define BlogCellReuseIdentifier @"BlogCell"
@implementation BlogTableViewHelper

+ (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId {
    return [[BlogTableViewHelper alloc] initWithTableView:tableView userId:userId];
}

- (instancetype)initWithTableView:(UITableView *)tableView userId:(NSUInteger)userId {
    if (self = [super init]) {
        
        self.userId = userId;
        tableView.delegate = self;
        tableView.dataSource = self;
        self.apiManager = [UserAPIManager new];
        self.tableView = tableView;

        __weak typeof(self) weakSelf = self;
        [tableView registerClass:[BlogCell class] forCellReuseIdentifier:BlogCellReuseIdentifier];
        tableView.header = [MJRefreshAnimationHeader headerWithRefreshingBlock:^{//下拉刷新
               [weakSelf.apiManage refreshUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) {
                    //...略
           }];
        }];
        tableView.footer = [MJRefreshAnimationFooter headerWithRefreshingBlock:^{//上拉加載
                [weakSelf.apiManage loadMoreUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) {
                    //...略
           }];
        }];
    }
    return self;
}

#pragma mark - UITableViewDataSource && Delegate
//...略
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.blogs.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogCellReuseIdentifier];
    BlogCellHelper *cellHelper = self.blogs[indexPath.row];
    if (!cell.didLikeHandler) {
        __weak typeof(cell) weakCell = cell;
        [cell setDidLikeHandler:^{
            cellHelper.likeCount += 1;
            weakCell.likeCountText = cellHelper.likeCountText;
        }];
    }
    cell.authorText = cellHelper.authorText;
    //...各類設置
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [self.navigationController pushViewController:self.VCGenerator(self.blogs[indexPath.row]) animated:YES];
}

#pragma mark - Utils

- (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander {
  
    [[UserAPIManager new] refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) {
        if (error) {
            [self showErrorInView:self.tableView info:error.domain];
        } else {
            
            for (Blog *blog in result) {
                [self.blogs addObject:[BlogCellHelper helperWithBlog:blog]];
            }
            [self.tableView reloadData];
        }
      completionHandler ? completionHandler(error, result) : nil;
    }];
}
//...略
@end
複製代碼
@implementation BlogCell
//...略
- (void)onClickLikeButton:(UIButton *)sender {
    [[UserAPIManager new] likeBlogWithBlogId:self.blogId userId:self.userId completionHandler:^(NSError *error, id result) {
        if (error) {
            //do error
        } else {
            //do success
            self.didLikeHandler ? self.didLikeHandler() : nil;
        }
    }];
}
@end
複製代碼
@implementation BlogCellHelper

- (NSString *)likeCountText {
    return [NSString stringWithFormat:@"贊 %ld", self.blog.likeCount];
}
//...略
- (NSString *)authorText {
    return [NSString stringWithFormat:@"做者姓名: %@", self.blog.authorName];
}
@end
複製代碼

Blog模塊由BlogTableViewHelper(C), BlogTableView(V), Blogs(C)構成, 這裏有點特殊, blogs裏面裝的不是M, 而是Cell的C層CellHelper, 這是由於Blog的MVC其實又是由多個更小的MVC組成的. M和V沒什麼好說的, 主要說一下做爲C的TableVIewHelper作了什麼.服務器

實際開發中, 各個模塊的View多是在Scene對應的Storyboard中新建並佈局的, 此時就不用各個模塊本身創建View了(好比這裏的BlogTableViewHelper), 讓Scene傳到C層進行管理就好了, 固然, 若是你是純代碼的方式, 那View就須要相應模塊自行創建了(好比下文的UserInfoViewController), 這個看本身的意願, 無傷大雅.微信

BlogTableViewHelper對外提供獲取數據和必要的構造方法接口, 內部根據自身狀況進行相應的初始化.架構

當外部調用fetchData的接口後, Helper就會啓動獲取數據邏輯, 由於數據獲取先後可能會涉及到一些頁面展現(HUD之類的), 而具體的展現又是和Scene直接相關的(有的Scene展現的是HUD有的可能展現的又是一種樣式或者根本不展現), 因此這部分會以CompletionHandler的形式交由Scene本身處理.

在Helper內部, 數據獲取失敗會展現相應的錯誤頁面, 成功則創建更小的MVC部分並通知其展現數據(也就是通知CellHelper驅動Cell), 另外, TableView的上拉刷新和下拉加載邏輯也是隸屬於Blog模塊的, 因此也在Helper中處理. 在頁面跳轉的邏輯中, 點擊跳轉的頁面是由Scene經過VCGeneratorBlock直接配置的, 因此也是解耦的(你也能夠經過didSelectRowHandler之類的方式傳遞數據到Scene層, 由Scene作跳轉, 是同樣的).

最後, V(Cell)如今只暴露了Set方法供外部進行設置, 因此和M(Blog)之間也是隔離的, 複用沒有問題.

這一系列過程都是自管理的, 未來若是Blog模塊會在另外一個SceneX展現, 那麼SceneX只須要新建一個BlogTableViewHelper, 而後調用一下helper.fetchData便可.

DraftTableViewHelper和BlogTableViewHelper邏輯相似, 就不貼了, 簡單貼一下UserInfo模塊的邏輯:

@implementation UserInfoViewController

+ (instancetype)instanceUserId:(NSUInteger)userId {
    return [[UserInfoViewController alloc] initWithUserId:userId];
}

- (instancetype)initWithUserId:(NSUInteger)userId {
  //    ...略
    [self addUI];
  //    ...略
}

#pragma mark - Action

- (void)onClickIconButton:(UIButton *)sender {
    [self.navigationController pushViewController:self.VCGenerator(self.user) animated:YES];
}

#pragma mark - Utils

- (void)addUI {
    
    //各類UI初始化 各類佈局
    self.userIconIV = [[UIImageView alloc] initWithFrame:CGRectZero];
    self.friendCountLabel = ...
    ...
}

- (void)fetchData {

    [[UserAPIManager new] fetchUserInfoWithUserId:self.userId completionHandler:^(NSError *error, id result) {
        if (error) {
            [self showErrorInView:self.view info:error.domain];
        } else {
            
            self.user = [User objectWithKeyValues:result];
            self.userIconIV.image = [UIImage imageWithURL:[NSURL URLWithString:self.user.url]];//數據格式化
            self.friendCountLabel.text = [NSString stringWithFormat:@"贊 %ld", self.user.friendCount];//數據格式化
            ...
        }
    }];
}

@end
複製代碼

UserInfoViewController除了比兩個TableViewHelper多個addUI的子控件佈局方法, 其餘邏輯大同小異, 也是本身管理的MVC, 也是隻須要初始化便可在任何一個Scene中使用.

如今三個自管理模塊已經創建完成, UserVC須要的只是根據本身的狀況作相應的拼裝佈局便可, 就和搭積木同樣:

@interface UserViewController ()

@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) UserInfoViewController *userInfoVC;

@property (strong, nonatomic) UITableView *blogTableView;
@property (strong, nonatomic) BlogTableViewHelper *blogTableViewHelper;

@end

@interface SelfViewController : UserViewController

@property (strong, nonatomic) UITableView *draftTableView;
@property (strong, nonatomic) DraftTableViewHelper *draftTableViewHelper;

@end

#pragma mark - UserViewController

@implementation UserViewController

+ (instancetype)instanceWithUserId:(NSUInteger)userId {
    if (userId == LoginUserId) {
        return [[SelfViewController alloc] initWithUserId:userId];
    } else {
        return [[UserViewController alloc] initWithUserId:userId];
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self addUI];
    
    [self configuration];
    
    [self fetchData];
}

#pragma mark - Utils(UserViewController)

- (void)addUI {
    
    //這裏只是表達一下意思 具體的layout邏輯確定不是這麼簡單的
    self.userInfoVC = [UserInfoViewController instanceWithUserId:self.userId];
    self.userInfoVC.view.frame = CGRectZero;
    [self.view addSubview:self.userInfoVC.view];
    [self.view addSubview:self.blogTableView = [[UITableView alloc] initWithFrame:CGRectZero style:0]];
}

- (void)configuration {
    
    self.title = @"用戶詳情";
//    ...其餘設置
    
    [self.userInfoVC setVCGenerator:^UIViewController *(id params) {
        return [UserDetailViewController instanceWithUser:params];
    }];
    
    self.blogTableViewHelper = [BlogTableViewHelper helperWithTableView:self.blogTableView userId:self.userId];
    [self.blogTableViewHelper setVCGenerator:^UIViewController *(id params) {
        return [BlogDetailViewController instanceWithBlog:params];
    }];
}

- (void)fetchData {
    
    [self.userInfoVC fetchData];//userInfo模塊不須要任何頁面加載提示
    [HUD show];//blog模塊可能就須要HUD
    [self.blogTableViewHelper fetchDataWithcompletionHandler:^(NSError *error, id result) {
      [HUD hide];
    }];
}

@end

#pragma mark - SelfViewController

@implementation SelfViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self addUI];
    
    [self configuration];
    
    [self fetchData];
}

#pragma mark - Utils(SelfViewController)

- (void)addUI {
    [super addUI];
    
    [self.view addSubview:switchButton];//特有部分...
    //...各類設置
    [self.view addSubview:self.draftTableView = [[UITableView alloc] initWithFrame:CGRectZero style:0]];
}

- (void)configuration {
    [super configuration];
    
    self.draftTableViewHelper = [DraftTableViewHelper helperWithTableView:self.draftTableView userId:self.userId];
    [self.draftTableViewHelper setVCGenerator:^UIViewController *(id params) {
        return [DraftDetailViewController instanceWithDraft:params];
    }];
}

- (void)fetchData {
    [super fetchData];

    [self.draftTableViewHelper fetchData];
}

@end
複製代碼

做爲業務場景的的Scene(UserVC)作的事情很簡單, 根據自身狀況對三個模塊進行配置(configuration), 佈局(addUI), 而後通知各個模塊啓動(fetchData)就能夠了, 由於每一個模塊的展現和交互是自管理的, 因此Scene只須要負責和自身業務強相關的部分便可. 另外, 針對自身訪問的狀況咱們創建一個UserVC子類SelfVC, SelfVC作的也是相似的事情.

MVC到這就說的差很少了, 對比上面錯誤的MVC方式, 咱們看看解決了哪些問題:

1.代碼複用: 三個小模塊的V(cell/userInfoView)對外只暴露Set方法, 對M甚至C都是隔離狀態, 複用徹底沒有問題.三個大模塊的MVC也能夠用於快速構建類似的業務場景(大模塊的複用比小模塊會差一些, 下文我會說明).

2.代碼臃腫: 由於Scene大部分的邏輯和佈局都轉移到了相應的MVC中, 咱們僅僅是拼裝MVC的便構建了兩個不一樣的業務場景, 每一個業務場景都能正常的進行相應的數據展現, 也有相應的邏輯交互, 而完成這些東西, 加空格也就100行代碼左右(固然, 這裏我忽略了一下Scene的佈局代碼).

3.易拓展性: 不管產品將來想加回收站仍是防護塔, 我須要的只是新建相應的MVC模塊, 加到對應的Scene便可.

4.可維護性: 各個模塊間職責分離, 哪裏出錯改哪裏, 徹底不影響其餘模塊. 另外, 各個模塊的代碼其實並不算多, 哪一天即便寫代碼的人離職了, 接手的人根據錯誤提示也能快速定位出錯模塊.

5.易測試性: 很遺憾, 業務的初始化依然綁定在Scene的生命週期中, 而有些邏輯也仍然須要UI的點擊事件觸發, 咱們依然只能Command+R, 點點點...

  • MVC的缺點

能夠看到, 即便是標準的MVC架構也並不是完美, 仍然有部分問題難以解決, 那麼MVC的缺點何在? 總結以下: 1.過分的注重隔離: 這個其實MV(x)系列都有這缺點, 爲了實現V層的徹底隔離, V對外只暴露Set方法, 通常狀況下沒什麼問題, 可是當須要設置的屬性不少時, 大量重複的Set方法寫起來仍是很累人的.

2.業務邏輯和業務展現強耦合: 能夠看到, 有些業務邏輯(頁面跳轉/點贊/分享...)是直接散落在V層的, 這意味着咱們在測試這些邏輯時, 必須首先生成對應的V, 而後才能進行測試. 顯然, 這是不合理的. 由於業務邏輯最終改變的是數據M, 咱們的關注點應該在M上, 而不是展現M的V.

  • MVP

MVC的缺點在於並無區分業務邏輯和業務展現, 這對單元測試很不友好. MVP針對以上缺點作了優化, 它將業務邏輯和業務展現也作了一層隔離, 對應的就變成了MVCP. M和V功能不變, 原來的C如今只負責佈局, 而全部的邏輯全都轉移到了P層.

對應關係如圖所示:

屏幕快照 2017-03-05 下午2.57.53.png

業務場景沒有變化, 依然是展現三種數據, 只是三個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
複製代碼
@interface BlogViewController ()<UITableViewDataSource, UITabBarDelegate, BlogView>

@property (weak, nonatomic) UITableView *tableView;
@property (strong, nonatomic) BlogPresenter presenter;
@property (copy, nonatomic) void(^didSelectRowHandler)(Blog *);

@end

@implementation BlogViewController

+ (instancetype)instanceWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter {
    return [[BlogViewController alloc] initWithTableView:tableView presenter:presenter];
}

- (instancetype)initWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter {
    if (self = [super init]) {
        
        self.presenter = presenter;
        self.tableView = tableView;
        tableView.delegate = self;
        tableView.dataSource = self;
        
        __weak typeof(self) weakSelf = self;
        [tableView registerClass:[BlogCell class] forCellReuseIdentifier:BlogCellReuseIdentifier];
        tableView.header = [MJRefreshAnimationHeader headerWithRefreshingBlock:^{//下拉刷新
            [weakSelf.presenter refreshUserBlogsWithCompletionHandler:^(NSError *error, id result) {
                [weakSelf.tableView.header endRefresh];
                if (!error) {
                    [weakSelf.tableView reloadData];
                }
                //...略
            }];
        }];
        tableView.footer = [MJRefreshAnimationFooter headerWithRefreshingBlock:^{//上拉加載
            [weakSelf.presenter loadMoreUserBlogsWithCompletionHandler:^(NSError *error, id result) {
                [weakSelf.tableView.footer endRefresh];
                if (!error) {
                    [weakSelf.tableView reloadData];
                }
                //...略
            }];
        }];
    }
    return self;
}

#pragma mark - Interface

- (void)fetchDataWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
    [self.presenter refreshUserBlogsWithCompletionHandler:^(NSError *error, id result) {
        if (error) {
            //show error info
        } else {
            [self.tableView reloadData];
        }
        completionHandler ? completionHandler(error, result) : nil;
    }];
}

#pragma mark - UITableViewDataSource && Delegate

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.presenter.allDatas.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogCellReuseIdentifier];
    BlogCellPresenter *cellPresenter = self.presenter.allDatas[indexPath.row];
    cell.present = cellPresenter;
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
     self.didSelectRowHandler ? self.didSelectRowHandler(self.presenter.allDatas[indexPath.row]) : nil;
}

@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 <NSObject>

- (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing;

@end

@interface UserCellPresenter : NSObject

@property (weak, nonatomic) id<UserCellPresenterCallBack> 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, 能夠嘗試, 但不要強制. 總之, 但願你們能作到: 設計時, 心中有數. 擼碼時, 開心就好.

=======================分割線=======================

這篇博客放出來之後, 陸陸續續收到一些小夥伴的簡信, 大部分都是一些提問, 可是每一個人的問題都比較雷同, 趁着清明放假有時間, 這裏將問得比較頻繁的問題擇出來, 這樣之後相似的問題就不用一一回復了, 算是解個懶.

Q: 爲何有的時候是MVP/MVVM有的時候是MVCP/MVCVM.

A: 其實這個問題在demo的MVP部分有作解釋, 估計有些朋友沒有看demo, 或者是我描述得太含糊了沒能讓人明白. 那麼這裏我分別給出MVVM和MVCVM的例子, 結合代碼解釋會方便一些, 順便也回答一下UserInfo模塊用MVVM怎麼寫.

@interface UserInfoViewModel : NSObject

+ (instancetype)viewModelWithUserId:(NSUInteger)userId;

- (User *)user;
- (RACCommand *)fetchUserInfoCommand;

- (UIImage *)icon;
- (NSString *)name;
- (NSString *)summary;
- (NSString *)blogCount;
- (NSString *)friendCount;

@end
複製代碼
@interface UserInfoViewModel ()

@property (strong, nonatomic) UIImage *icon;
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *summary;
@property (copy, nonatomic) NSString *blogCount;
@property (copy, nonatomic) NSString *friendCount;

@property (strong, nonatomic) User *user;
@property (assign, nonatomic) NSUInteger userId;

@end

@implementation UserInfoViewModel

+ (instancetype)viewModelWithUserId:(NSUInteger)userId {
    UserInfoViewModel *viewModel = [UserInfoViewModel new];
    viewModel.userId = userId;
    return viewModel;
}

- (RACCommand *)fetchUserInfoCommand {
    return [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        return [[self fetchUserInfoSignal] doNext:^(User *user) {
            
            self.user = user;
            self.icon = [UIImage imageNamed:user.icon ?: @"icon0"];
            self.name = user.name.length > 0 ? user.name : @"匿名";
            self.summary = [NSString stringWithFormat:@"我的簡介: %@", user.summary.length > 0 ? user.summary : @"這我的很懶, 什麼也沒有寫~"];
            self.blogCount = [NSString stringWithFormat:@"做品: %ld", user.blogCount];
            self.friendCount = [NSString stringWithFormat:@"好友: %ld", user.friendCount];
        }];
    }];
}

- (RACSignal *)fetchUserInfoSignal {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        
        [[UserAPIManager new] fetchUserInfoWithUserId:self.user.userId completionHandler:^(NSError *error, id result) {
            
            if (!error) {
                
                [subscriber sendNext:result];
                [subscriber sendCompleted];
            } else {
                [subscriber sendError:error];
            }
        }];
        return nil;
    }];
}
複製代碼

UserInfoViewModel作的事情很簡單, 從服務器拉取數據, 而後將數據格式化爲V層須要展現的樣子, 這部分MVVM和MVCVM都是同樣的, 接下來咱們看看不同的部分, 先看看MVVM中的V層代碼:

#import "UserInfoViewModel.h"
@interface UserInfoView : UIView

+ (instancetype)instanceWithViewModel:(UserInfoViewModel *)viewModel;
- (void)fetchData;
- (void)setOnClickIconCommand:(RACCommand *)onClickIconCommand;
@end
複製代碼
@interface UserInfoView ()

@property (weak, nonatomic) UIButton *iconButton;
@property (weak, nonatomic) UILabel *nameLabel;
@property (weak, nonatomic) UILabel *summaryLabel;
@property (weak, nonatomic) UILabel *blogCountLabel;
@property (weak, nonatomic) UILabel *friendCountLabel;

@property (strong, nonatomic) RACCommand *onClickIconCommand;
@property (strong, nonatomic) UserInfoViewModel *viewModel;
@end

@implementation UserInfoView

+ (instancetype)instanceWithViewModel:(UserInfoViewModel *)viewModel {
    UserInfoView *view = [UserInfoView new];
    view.viewModel = viewModel;
    return view;
}

- (instancetype)init {
    return [self initWithFrame:CGRectZero];
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self addUI];
        [self bind];
    }
    return self;
}

- (void)bind {
    RAC(self.nameLabel, text) = RACObserve(self, viewModel.name);
    RAC(self.summaryLabel, text) = RACObserve(self, viewModel.summary);
    RAC(self.blogCountLabel, text) = RACObserve(self, viewModel.blogCount);
    RAC(self.friendCountLabel, text) = RACObserve(self, viewModel.friendCount);
    @weakify(self);
    [RACObserve(self, viewModel.icon) subscribeNext:^(UIImage *icon) {
        @strongify(self);
        [self.iconButton setImage:icon forState:UIControlStateNormal];
    }];
    [[self.iconButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        @strongify(self);
        [self.onClickIconCommand execute:self.viewModel.user];
    }];
}

- (void)fetchData {
    
    [[[self.viewModel fetchUserInfoCommand] execute:nil] subscribeError:^(NSError *error) {
        //show error view
    } completed:^{
        //do completed
    }];
}

- (void)addUI {
//... 各類新建 各類佈局
}

@end
複製代碼

而後再看看MVCVM中V層代碼:

@interface UserInfoView : UIView

- (UIButton *)iconButton;
- (UILabel *)nameLabel;
- (UILabel *)summaryLabel;
- (UILabel *)blogCountLabel;
- (UILabel *)friendCountLabel;

@end
複製代碼
@interface UserInfoView ()

@property (weak, nonatomic) UIButton *iconButton;
@property (weak, nonatomic) UILabel *nameLabel;
@property (weak, nonatomic) UILabel *summaryLabel;
@property (weak, nonatomic) UILabel *blogCountLabel;
@property (weak, nonatomic) UILabel *friendCountLabel;

@end

@implementation UserInfoView

- (instancetype)init {
    return [self initWithFrame:CGRectZero];
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self addUI];
    }
    return self;
}

- (void)addUI {
//... 各類新建 各類佈局
}
@end
複製代碼

在MVVM中的UserInfoView一共作了三件事情: 1. UI佈局(addUI), 2. 數據綁定(bind) 3. 和上層交互(fetchData, onClickIconCommand) 相對而言, MVCVM中的UserInfoView作的事情就少多了, 只作了一件事情: UI佈局. 不過它不只布了局, 還將對應的View也暴露了出來. 這些暴露出來的東西給誰用呢? 還有, 數據綁定和上層交互如今由誰來作呢? 顯然只能是這個多出來的C層了, 看看這部分的代碼吧:

@interface UserInfoController : NSObject

+ (instancetype)instanceWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel;

- (UserInfoView *)view;

- (void)fetchData;
- (void)setOnClickIconCommand:(RACCommand *)onClickIconCommand;
@end
複製代碼
@interface UserInfoController ()

@property (strong, nonatomic) UserInfoView *view;
@property (strong, nonatomic) UserInfoViewModel *viewModel;

@property (strong, nonatomic) RACCommand *onClickIconCommand;
@end

@implementation UserInfoController


+ (instancetype)instanceWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel {
    if (view == nil || viewModel == nil) { return nil; }
    
    return [[UserInfoController alloc] initWithView:view viewModel:viewModel];
}

- (instancetype)initWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel {
    if (self = [super init]) {
        self.view = view;
        self.viewModel = viewModel;
        
        [self bind];
    }
    return self;
}

- (void)bind {
    
    RAC(self.view.nameLabel, text) = RACObserve(self, viewModel.name);
    RAC(self.view.summaryLabel, text) = RACObserve(self, viewModel.summary);
    RAC(self.view.blogCountLabel, text) = RACObserve(self, viewModel.blogCount);
    RAC(self.view.friendCountLabel, text) = RACObserve(self, viewModel.friendCount);
    @weakify(self);
    [RACObserve(self, viewModel.icon) subscribeNext:^(UIImage *icon) {
        @strongify(self);
        [self.view.iconButton setImage:icon forState:UIControlStateNormal];
    }];
    [[self.view.iconButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        @strongify(self);
        [self.onClickIconCommand execute:self.viewModel.user];
    }];
}

- (void)fetchData {
    
    [[[self.viewModel fetchUserInfoCommand] execute:nil] subscribeError:^(NSError *error) {
        //show error view
    } completed:^{
        //do completed
    }];
}

@end
複製代碼

代碼一亮出來, 相信各位應該很清楚MVP/MVVM和MVCP/MVCVM的區別何在了, 簡單描述一下就是是否拆分UI佈局和數據綁定(注意: 是數據綁定, 不是業務邏輯, 業務邏輯都在VM層).

毫無疑問, 拆分更加細緻的MVCVM比MVVM要好一些, 純佈局的V層優勢在MVC部分已經介紹過了, 複用性賊好, 另外, 佈局拆出來之後, 數據綁定層的代碼看起來會更加簡潔, 易讀性也很好. 然而, 最初的demo裏面並無包含這種寫法的例子, 這算是我本身的緣由. 由於實際開發一般沒有這麼細粒度的複用模塊(UI和產品不給機會), 另外我本人習慣用xib/sb作頁面佈局, 因此V層也不會有什麼佈局代碼, 長此以往, 本身寫的代碼都是MVVM而不是MVCVM, 習慣成天然了.

Q: V層直接聲明瞭P/VM的屬性, 數據綁定又是寫死的, 那不就是一對一了, 怎麼複用呢?

A: 注意到我描述P/VM層時都是說: xxxP/VM.h暴露了那些接口, 而不是xxxP/VM有那些屬性. 換句話說, P/VM其實只是定義了一套規範, 可是這套規範的實現倒是千差萬別的, 當只有一個實現時確實是一對一的, 當有多個實現時就是一對多了. 舉個我項目中的例子吧, 我有好友列表, 關注列表, 用戶列表三個不一樣數據源不一樣數據操做的列表, 但這三張表cell的佈局展現倒是如出一轍的, 只是展現的文字不同, 點擊按鈕有的是加/取消好友, 有的是加/取消關注, 這就是典型的佈局不變可是邏輯變化的例子, 因此我只寫了一個cell, 一個cellViewModel接口, 可是viewModel的接口實現倒是兩套, 對應到代碼中:

//HHUserCellViewModel.h
@interface HHUserCellViewModel : NSObject

+ (instancetype)friendCellViewModelWithUser:(HHUser *)user;
+ (instancetype)followCellViewModelWithUser:(HHFriend *)user;

- (id)user;
- (BOOL)isVip;

- (NSURL *)userAvatarURL;
- (NSString *)userName;
- (NSString *)userSignature;
- (NSString *)userFriendCount;

- (NSString *)rightButtonTitle;
- (NSString *)rightButtonEventName;
- (RACCommand *)rightButtonCommand;

- (BOOL)deleteButtonHidden;
- (UIImage *)deleteButtonImage;
- (RACCommand *)deleteButtonCommand;

- (CGFloat)contentHeight;

@end
複製代碼
//HHUserCellViewModel基類: 這裏定義了兩套實現都會用到的屬性和方法
@interface HHUserCellViewModel ()

@property (strong, nonatomic) HHUser *user;

@property (copy, nonatomic) NSString *rightButtonTitle;
@property (strong, nonatomic) RACCommand *rightButtonCommand;

@property (assign, nonatomic) BOOL deleteButtonHidden;
@property (strong, nonatomic) UIImage *deleteButtonImage;
@property (strong, nonatomic) RACCommand *deleteButtonCommand;

@end


#pragma mark - HHFollowCellViewModel

//HHFollowCellViewModel子類: 關注模式的viewModel的實現
@interface HHFollowCellViewModel : HHUserCellViewModel
@end

@implementation HHFollowCellViewModel

- (instancetype)initWithUser:(HHFriend *)user {
    if (self = [super initWithUser:user]) {
        
        [self switchRightButtonColor:user.followState];
        
        @weakify(self);
        self.rightButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            
            if ([self.rightButtonTitle isEqualToString:@"已關注"]) {
                
                self.deleteButtonHidden = !self.deleteButtonHidden;
                return [RACSignal empty];
            } else {
                
                [self switchRightButtonColor:YES];
                return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                    //點擊右側按鈕調用加關注接口
                    [[HHSocketFollowAPIManager new] followWithFollowUser:self.user completionHandler:^(NSError *error, id result) {
                        
                        if (error) {
                            [self switchRightButtonColor:NO];
                        }
                        
                        if ([USER_ID integerValue] != 0) {
                            [subscriber sendNext:@(error == nil)];
                        }
                        
                        [subscriber sendCompleted];
                    }];
                    return nil;
                }];
            }
        }];
        
        self.deleteButtonImage = [UIImage imageNamed:@"unfollow.png"];
        self.deleteButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                 //點擊刪除按鈕調用取消關注接口
                self.deleteButtonHidden = YES;
                [[HHSocketFollowAPIManager new] unfollowWithUnfollowUser:self.user completionHandler:^(NSError *error, id result) {
                    
                    if (error) {
                        [subscriber sendError:error];
                    } else {
                        [self switchRightButtonColor:NO];
                        [subscriber sendCompleted];
                    }
                }];
                return nil;
            }];
        }];
    }
    return self;
}

- (void)switchRightButtonColor:(BOOL)isSelected {
    [super switchRightButtonColor:isSelected];
    
    self.rightButtonTitle = isSelected ? @"已關注" : @"+關注";
    
}

- (CGFloat)contentHeight {
    return 68;
}

@end


#pragma mark - HHFriendCellViewModel

//HHFriendCellViewModel子類: 好友模式的viewModel的實現
@interface HHFriendCellViewModel : HHUserCellViewModel
@end

@implementation HHFriendCellViewModel

- (instancetype)initWithUser:(HHFriend *)user {
    if (self = [super initWithUser:user]) {
        
        [self switchRightButtonColorWithFriendState:self.user.friendState];
        
        @weakify(self);
        self.rightButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            
            if ([self.rightButtonTitle isEqualToString:@"加好友"]) {
                
                [self switchRightButtonColorWithFriendState:1];
                return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                    //點擊右側按鈕調用加好友接口
                    [[HHSocketFriendAPIManager new] addFriendWithUser:self.user msg:@"你好, 我是xxx" completionHandler:^(NSError *error, id result) {
                        if (error) {
                            [self switchRightButtonColorWithFriendState:0];
                        }
                        if ([USER_ID integerValue] != 0) {
                            [subscriber sendNext:@(error == nil)];
                        }
                        [subscriber sendCompleted];
                    }];
                    return nil;
                }];
            } else if([self.rightButtonTitle isEqualToString:@"好友"]) {
                
                self.deleteButtonHidden = !self.deleteButtonHidden;
            }
            
            return [RACSignal empty];
        }];
        
        self.deleteButtonImage = [UIImage imageNamed:@"deleteFriend.png"];
        self.deleteButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            
            self.deleteButtonHidden = !self.deleteButtonHidden;
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                
                //點擊刪除按鈕調用刪除好友接口
                [[HHSocketFriendAPIManager new] deleteFriendWithUser:self.user completionHandler:^(NSError *error, id result) {
                    
                    if (error) {
                        [subscriber sendError:error];
                    } else {
                        [self switchRightButtonColorWithFriendState:0];
                        [subscriber sendCompleted];
                    }
                }];
                return nil;
            }];
        }];
    }
    return self;
}

- (void)switchRightButtonColorWithFriendState:(NSInteger)state {
    self.user.friendState = state;
    
    switch (state) {
        case 0: {
            [super switchRightButtonColor:NO];
            self.rightButtonTitle = @"加好友";
        }   break;
            
        case 1: {
            
            self.rightButtonTitleColor = kColorGrayNine;
            self.rightButtonBorderColor = self.rightButtonBackgroundColor = [UIColor whiteColor];
            self.rightButtonTitle = @"驗證中";
        }   break;
            
        case 2: {
            [super switchRightButtonColor:YES];
            self.rightButtonTitle = @"好友";
        }   break;
    }
}

- (CGFloat)contentHeight {
    return self.user.userId != [USER_ID integerValue] && self.user.commonFriendCount > 0 ? 91 : 68;
}

@end


#pragma mark - HHUserCellViewModel

@implementation HHUserCellViewModel

+ (instancetype)friendCellViewModelWithUser:(HHFriend *)user {
    return [[HHFriendCellViewModel alloc] initWithUser:user];
}

+ (instancetype)followCellViewModelWithUser:(HHFriend *)user {
    return [[HHFollowCellViewModel alloc] initWithUser:user];
}

//HHUserCellViewModel基類: 一些實現相同的接口直接在此處實現 省得重複如出一轍的代碼
#pragma mark - PublicInterface

- (BOOL)isVip {
    return self.user.level > 0;
}

- (NSString *)userName {
    return self.user.nickname;
}

- (NSString *)userFriendCount {
    return self.user.commonFriendCount > 0 ? [NSString stringWithFormat:@"大家有%ld個共同好友", self.user.commonFriendCount] : @"";
}

- (NSString *)userSignature {
    return self.user.signature.length > 0 ? self.user.signature : @"TA很懶,什麼都沒寫";
}

- (NSURL *)userAvatarURL {
    return self.user.avatar.HHUrl;
}
複製代碼

對於Cell而言, 它只知道本身該怎麼樣佈局, 本身會有一個實現了HHUserCellViewModel接口的屬性, 而後會去綁定這些接口的數據進行展現, 點擊之後調用哪一個Command, 至於具體展現出來的是好友仍是關注, 點擊具體會執行什麼事件, 它徹底不關心, 它只管綁定, 其餘的事情上層會處理好的.

這裏也是出於我的習慣, 我本人特別喜歡用類簇或者說抽象工廠, 由於這樣能少建不少文件, 一個類就能作完全部事情. 如今想來, 若是一開始demo裏面寫的就是Protocol, 而後多個類實現這個Protocol可能就不會有人有疑問了.

額... 原本有好幾個問題的, 可是簡書有字數限制(代碼好像也算字?), 我又不太想另開一遍囉嗦囉嗦, 只能選擇把相對重要的MVVM這部分放出來了, 望海涵

本文附帶的demo地址

來來來 喝完這杯 還有一杯

再喝完這杯 還有三杯

相關文章
相關標籤/搜索