雜談: MVC/MVP/MVVM

黑花白花發表於2018-01-09

從簡書遷移到掘金...

前言

本文為回答一位朋友關於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都是如何表現的.

  • MVC之消失的C層

雜談: MVC/MVP/MVVM
上圖中的頁面(業務場景)或者類似頁面相信大家做過不少, 各個程式設計師的具體實現方式可能各不一樣, 這裡說說我所看到的部分程式設計師的寫法:

//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嗎? 我們將上面的過程用一張圖來表示:

螢幕快照 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應該是這個樣子的:

螢幕快照 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地址

來來來 喝完這杯 還有一杯

再喝完這杯 還有三杯

相關文章