杂谈: 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地址

来来来 喝完这杯 还有一杯

再喝完这杯 还有三杯

相关文章
相关标签/搜索