杂谈: MVC/MVP/MVVM (一)

移动开发 iOS
本文为回答一位朋友关于MVC/MVP/MVVM架构方面的疑问所写, 旨在介绍iOS下MVC/MVP/MVVM三种架构的设计思路以及各自的优缺点。

前言

本文为回答一位朋友关于MVC/MVP/MVVM架构方面的疑问所写,旨在介绍iOS下MVC/MVP/MVVM三种架构的设计思路以及各自的优缺点。

MVC

  • MVC的相关概念

MVC最早存在于桌面程序中的, M是指业务数据, V是指用户界面, C则是控制器. 在具体的业务场景中, C作为M和V之间的连接, 负责获取输入的业务数据, 然后将处理后的数据输出到界面上做相应展示, 另外, 在数据有所更新时, C还需要及时提交相应更新到界面展示. 在上述过程中, 因为M和V之间是完全隔离的, 所以在业务场景切换时, 通常只需要替换相应的C, 复用已有的M和V便可快速搭建新的业务场景. MVC因其复用性, 大大提高了开发效率, 现已被广泛应用在各端开发中.

概念过完了, 下面来看看, 在具体的业务场景中MVC/MVP/MVVM都是如何表现的.

  • MVC之消失的C层

 

 

 

 

上图中的页面(业务场景)或者类似页面相信大家做过不少, 各个程序员的具体实现方式可能各不一样, 这里说说我所看到的部分程序员的写法:

  1. //UserVC 
  2.  
  3. - (void)viewDidLoad { 
  4.  
  5.     [super viewDidLoad]; 
  6.  
  7.   
  8.  
  9.     [[UserApi new] fetchUserInfoWithUserId:132 completionHandler:^(NSError *error, id result) { 
  10.  
  11.         if (error) { 
  12.  
  13.             [self showToastWithText:@"获取用户信息失败了~"]; 
  14.  
  15.         } else { 
  16.  
  17.   
  18.  
  19.             self.userIconIV.image = ... 
  20.  
  21.             self.userSummaryLabel.text = ... 
  22.  
  23.             ... 
  24.  
  25.         } 
  26.  
  27.     }]; 
  28.  
  29.   
  30.  
  31.     [[userApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) { 
  32.  
  33.         if (error) { 
  34.  
  35.             [self showErrorInView:self.tableView info:...]; 
  36.  
  37.         } else { 
  38.  
  39.   
  40.  
  41.             [self.blogs addObjectsFromArray:result]; 
  42.  
  43.             [self.tableView reloadData]; 
  44.  
  45.         } 
  46.  
  47.     }]; 
  48.  
  49.  
  50. //...略 
  51.  
  52. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
  53.  
  54.     BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"]; 
  55.  
  56.     cell.blog = self.blogs[indexPath.row]; 
  57.  
  58.     return cell; 
  59.  
  60.  
  61.   
  62.  
  63. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 
  64.  
  65.     [self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:self.blogs[indexPath.row]] animated:YES]; 
  66.  
  67.  
  68. //...略 
  69.  
  70.  
  71. //BlogCell 
  72.  
  73. - (void)setBlog:(Blog)blog { 
  74.  
  75.     _blog = blog; 
  76.  
  77.   
  78.  
  79.     self.authorLabel.text = blog.blogAuthor; 
  80.  
  81.     self.likeLebel.text = [NSString stringWithFormat:@"赞 %ld", blog.blogLikeCount]; 
  82.  
  83.     ... 
  84.  
  85.  

程序员很快写完了代码, Command+R一跑, 没有问题, 心满意足的做其他事情去了. 后来有一天, 产品要求这个业务需要改动, 用户在看他人信息时是上图中的页面, 看自己的信息时, 多一个草稿箱的展示, 像这样: 

 

 

 

于是小白将代码改成这样:

  1. //UserVC 
  2.  
  3. - (void)viewDidLoad { 
  4.  
  5.     [super viewDidLoad]; 
  6.  
  7.   
  8.  
  9.     if (self.userId != LoginUserId) { 
  10.  
  11.         self.switchButton.hidden = self.draftTableView.hidden = YES; 
  12.  
  13.         self.blogTableView.frame = ... 
  14.  
  15.     } 
  16.  
  17.   
  18.  
  19.     [[UserApi new] fetchUserI......略 
  20.  
  21.     [[UserApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) { 
  22.  
  23.         //if Error...略 
  24.  
  25.         [self.blogs addObjectsFromArray:result]; 
  26.  
  27.         [self.blogTableView reloadData]; 
  28.  
  29.   
  30.  
  31.     }]; 
  32.  
  33.   
  34.  
  35.     [[userApi new] fetchUserDraftsWithUserId:132 completionHandler:^(NSError *error, id result) { 
  36.  
  37.         //if Error...略 
  38.  
  39.         [self.drafts addObjectsFromArray:result]; 
  40.  
  41.         [self.draftTableView reloadData]; 
  42.  
  43.     }]; 
  44.  
  45.  
  46.   
  47.  
  48. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 
  49.  
  50.      return tableView == self.blogTableView ? self.blogs.count : self.drafts.count
  51.  
  52.  
  53.   
  54.  
  55. //...略 
  56.  
  57. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
  58.  
  59.   
  60.  
  61.     if (tableView == self.blogTableView) { 
  62.  
  63.         BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"]; 
  64.  
  65.         cell.blog = self.blogs[indexPath.row]; 
  66.  
  67.         return cell; 
  68.  
  69.     } else { 
  70.  
  71.         DraftCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DraftCell"]; 
  72.  
  73.         cell.draft = self.drafts[indexPath.row]; 
  74.  
  75.         return cell; 
  76.  
  77.     } 
  78.  
  79.  
  80.   
  81.  
  82. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 
  83.  
  84.     if (tableView == self.blogTableView) ... 
  85.  
  86.  
  87. //...略 
  88.  
  89.  
  90. //DraftCell 
  91.  
  92. - (void)setDraft:(draft)draft { 
  93.  
  94.     _draft = draft; 
  95.  
  96.     self.draftEditDate = ... 
  97.  
  98.  
  99.   
  100.  
  101. //BlogCell 
  102.  
  103. - (void)setBlog:(Blog)blog { 
  104.  
  105.     ...同上 
  106.  
  107.  

后来啊, 产品觉得用户看自己的页面再加个回收站什么的会很好, 于是程序员又加上一段代码逻辑 , 再后来…

随着需求的变更, UserVC变得越来越臃肿, 越来越难以维护, 拓展性和测试性也极差. 程序员也发现好像代码写得有些问题, 但是问题具体出在哪里? 难道这不是MVC吗?

我们将上面的过程用一张图来表示: 

 

 

 

通过这张图可以发现, 用户信息页面作为业务场景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应该是这个样子的: 

 

 

 

UserVC作为业务场景, 需要展示三种数据, 对应的就有三个MVC, 这三个MVC负责各自模块的数据获取, 数据处理和数据展示, 而UserVC需要做的就是配置好这三个MVC, 并在合适的时机通知各自的C层进行数据获取, 各个C层拿到数据后进行相应处理, 处理完成后渲染到各自的View上, UserVC***将已经渲染好的各个View进行布局即可, 具体到代码中如下:

  1. @interface BlogTableViewHelper : NSObject 
  2.  
  3.   
  4.  
  5. + (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId; 
  6.  
  7.   
  8.  
  9. - (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander; 
  10.  
  11. - (void)setVCGenerator:(ViewControllerGenerator)VCGenerator; 
  12.  
  13.   
  14.  
  15. @end 
  16.  
  17.  
  18. @interface BlogTableViewHelper() 
  19.  
  20.   
  21.  
  22. @property (weak, nonatomic) UITableView *tableView; 
  23.  
  24. @property (copy, nonatomic) ViewControllerGenerator VCGenerator; 
  25.  
  26.   
  27.  
  28. @property (assign, nonatomic) NSUInteger userId; 
  29.  
  30. @property (strong, nonatomic) NSMutableArray *blogs; 
  31.  
  32. @property (strong, nonatomic) UserAPIManager *apiManager; 
  33.  
  34.   
  35.  
  36. @end 
  37.  
  38. #define BlogCellReuseIdentifier @"BlogCell" 
  39.  
  40. @implementation BlogTableViewHelper 
  41.  
  42.   
  43.  
  44. + (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId { 
  45.  
  46.     return [[BlogTableViewHelper alloc] initWithTableView:tableView userId:userId]; 
  47.  
  48.  
  49.   
  50.  
  51. - (instancetype)initWithTableView:(UITableView *)tableView userId:(NSUInteger)userId { 
  52.  
  53.     if (self = [super init]) { 
  54.  
  55.   
  56.  
  57.         self.userId = userId; 
  58.  
  59.         tableView.delegate = self; 
  60.  
  61.         tableView.dataSource = self; 
  62.  
  63.         self.apiManager = [UserAPIManager new]; 
  64.  
  65.         self.tableView = tableView; 
  66.  
  67.   
  68.  
  69.         __weak typeof(self) weakSelf = self; 
  70.  
  71.         [tableView registerClass:[BlogCell class] forCellReuseIdentifier:BlogCellReuseIdentifier]; 
  72.  
  73.         tableView.header = [MJRefreshAnimationHeader headerWithRefreshingBlock:^{//下拉刷新 
  74.  
  75.                [weakSelf.apiManage refreshUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) { 
  76.  
  77.                     //...略 
  78.  
  79.            }]; 
  80.  
  81.         }]; 
  82.  
  83.         tableView.footer = [MJRefreshAnimationFooter headerWithRefreshingBlock:^{//上拉加载 
  84.  
  85.                 [weakSelf.apiManage loadMoreUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) { 
  86.  
  87.                     //...略 
  88.  
  89.            }]; 
  90.  
  91.         }]; 
  92.  
  93.     } 
  94.  
  95.     return self; 
  96.  
  97.  
  98.   
  99.  
  100. #pragma mark - UITableViewDataSource && Delegate 
  101.  
  102. //...略 
  103.  
  104. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 
  105.  
  106.     return self.blogs.count
  107.  
  108.  
  109.   
  110.  
  111. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
  112.  
  113.   
  114.  
  115.     BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogCellReuseIdentifier]; 
  116.  
  117.     BlogCellHelper *cellHelper = self.blogs[indexPath.row]; 
  118.  
  119.     if (!cell.didLikeHandler) { 
  120.  
  121.         __weak typeof(cell) weakCell = cell; 
  122.  
  123.         [cell setDidLikeHandler:^{ 
  124.  
  125.             cellHelper.likeCount += 1; 
  126.  
  127.             weakCell.likeCountText = cellHelper.likeCountText; 
  128.  
  129.         }]; 
  130.  
  131.     } 
  132.  
  133.     cell.authorText = cellHelper.authorText; 
  134.  
  135.     //...各种设置 
  136.  
  137.     return cell; 
  138.  
  139.  
  140.   
  141.  
  142. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 
  143.  
  144.     [self.navigationController pushViewController:self.VCGenerator(self.blogs[indexPath.row]) animated:YES]; 
  145.  
  146.  
  147.   
  148.  
  149. #pragma mark - Utils 
  150.  
  151.   
  152.  
  153. - (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander { 
  154.  
  155.   
  156.  
  157.     [[UserAPIManager new] refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) { 
  158.  
  159.         if (error) { 
  160.  
  161.             [self showErrorInView:self.tableView info:error.domain]; 
  162.  
  163.         } else { 
  164.  
  165.   
  166.  
  167.             for (Blog *blog in result) { 
  168.  
  169.                 [self.blogs addObject:[BlogCellHelper helperWithBlog:blog]]; 
  170.  
  171.             } 
  172.  
  173.             [self.tableView reloadData]; 
  174.  
  175.         } 
  176.  
  177.       completionHandler ? completionHandler(error, result) : nil; 
  178.  
  179.     }]; 
  180.  
  181.  
  182. //...略 
  183.  
  184. @end 
  185.  
  186.  
  187. @implementation BlogCell 
  188.  
  189. //...略 
  190.  
  191. - (void)onClickLikeButton:(UIButton *)sender { 
  192.  
  193.     [[UserAPIManager new] likeBlogWithBlogId:self.blogId userId:self.userId completionHandler:^(NSError *error, id result) { 
  194.  
  195.         if (error) { 
  196.  
  197.             //do error 
  198.  
  199.         } else { 
  200.  
  201.             //do success 
  202.  
  203.             self.didLikeHandler ? self.didLikeHandler() : nil; 
  204.  
  205.         } 
  206.  
  207.     }]; 
  208.  
  209.  
  210. @end 
  211.  
  212.  
  213. @implementation BlogCellHelper 
  214.  
  215.   
  216.  
  217. - (NSString *)likeCountText { 
  218.  
  219.     return [NSString stringWithFormat:@"赞 %ld", self.blog.likeCount]; 
  220.  
  221.  
  222. //...略 
  223.  
  224. - (NSString *)authorText { 
  225.  
  226.     return [NSString stringWithFormat:@"作者姓名: %@", self.blog.authorName]; 
  227.  
  228.  
  229. @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模块的逻辑:

  1. @implementation UserInfoViewController 
  2.  
  3.   
  4.  
  5. + (instancetype)instanceUserId:(NSUInteger)userId { 
  6.  
  7.     return [[UserInfoViewController alloc] initWithUserId:userId]; 
  8.  
  9.  
  10.   
  11.  
  12. - (instancetype)initWithUserId:(NSUInteger)userId { 
  13.  
  14.   //    ...略 
  15.  
  16.     [self addUI]; 
  17.  
  18.   //    ...略 
  19.  
  20.  
  21.   
  22.  
  23. #pragma mark - Action 
  24.  
  25.   
  26.  
  27. - (void)onClickIconButton:(UIButton *)sender { 
  28.  
  29.     [self.navigationController pushViewController:self.VCGenerator(self.user) animated:YES]; 
  30.  
  31.  
  32.   
  33.  
  34. #pragma mark - Utils 
  35.  
  36.   
  37.  
  38. - (void)addUI { 
  39.  
  40.   
  41.  
  42.     //各种UI初始化 各种布局 
  43.  
  44.     self.userIconIV = [[UIImageView alloc] initWithFrame:CGRectZero]; 
  45.  
  46.     self.friendCountLabel = ... 
  47.  
  48.     ... 
  49.  
  50.  
  51.   
  52.  
  53. - (void)fetchData { 
  54.  
  55.   
  56.  
  57.     [[UserAPIManager new] fetchUserInfoWithUserId:self.userId completionHandler:^(NSError *error, id result) { 
  58.  
  59.         if (error) { 
  60.  
  61.             [self showErrorInView:self.view info:error.domain]; 
  62.  
  63.         } else { 
  64.  
  65.   
  66.  
  67.             self.user = [User objectWithKeyValues:result]; 
  68.  
  69.             self.userIconIV.image = [UIImage imageWithURL:[NSURL URLWithString:self.user.url]];//数据格式化 
  70.  
  71.             self.friendCountLabel.text = [NSString stringWithFormat:@"赞 %ld", self.user.friendCount];//数据格式化 
  72.  
  73.             ... 
  74.  
  75.         } 
  76.  
  77.     }]; 
  78.  
  79.  
  80.   
  81.  
  82. @end  

UserInfoViewController除了比两个TableViewHelper多个addUI的子控件布局方法, 其他逻辑大同小异, 也是自己管理的MVC, 也是只需要初始化即可在任何一个Scene中使用.

现在三个自管理模块已经建立完成, UserVC需要的只是根据自己的情况做相应的拼装布局即可, 就和搭积木一样 

 

 

  

 

 

  

 

 

 

作为业务场景的的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.

 

责任编辑:庞桂玉 来源: iOS大全
相关推荐

2017-04-01 08:30:00

MVCMVPMVVM

2018-03-21 16:19:40

MVCMVPMVVM

2019-09-11 08:52:24

MVCMVPMVVM

2019-10-30 14:58:45

MVCAndroid表现层

2014-03-17 11:05:00

ScriptCode Blocks

2023-04-11 07:50:27

软件架构设计

2018-10-29 11:41:22

架构MVCAndroid

2023-10-20 13:21:55

软件设计模式架构

2017-05-12 16:50:14

GUI应用程序

2015-12-04 09:33:15

程序员前端演进史

2009-04-30 15:56:50

三层架构MVCMVP

2009-07-24 13:54:39

MVVM模式

2012-05-28 10:34:50

MVVM 数据绑定

2017-02-24 14:05:14

AndroidMVCMVP

2009-12-21 09:22:51

SilverlightMVVM模式

2018-07-10 10:00:15

Android架构MVC

2016-03-30 09:34:27

2021-01-21 05:50:28

MVVM模式Wpf

2010-01-27 08:44:56

ASP.NET MVC

2015-09-29 10:07:58

中文编码
点赞
收藏

51CTO技术栈公众号