看了下上篇博客的发表时间到这篇博客,居然过了11个月,罪过,罪过。这一年时间也是够折腾的,年初离职跳槽到鹅厂,单独负责一个社区项目,忙的天昏地暗,忙的差很少了,转眼就到了7月。css
七月流火,心也跟着燥热起来了,眼瞅着移动端这发展趋势从05年开始就一直在走下坡路了,想着再这么下去不行,得找条后路备着。网上看了看,以为前端不错,最近炒的挺火热的,那就学学看吧,买了html,css,js的几本书,花了个把月的闲暇时间看完了,顺便作了几个demo,忽然以为好无聊。大概是iOS也是写界面,前端仍是写界面,写的有些麻木了。以前一直有学Python,写过一些爬虫、用Django也写事后台,感受还挺好玩的,Python在大数据和AI领域也大放异彩,想借此机会学学。html
虽然这两个领域进入门槛比较高,可是就目前发展势头来看,应该是一个发展趋势,互联网过去十年的浪潮是移动互联网,下一个十年的浪潮极可能就是AI了。因此早作准备,从零开始学吧。其实干程序员这行,焦虑是没法避免的,由于本身那点知识储备和突飞猛进的技术发展相比起来,简直沧海一粟,不禁让人感叹:吾生也有涯,而学无涯。前端
不少人都在追逐新技术的过程当中迷失了本身,越学越焦虑,由于发现本身不管怎么学,都赶不上技术发展的脚步。我倒以为如其去追逐那些还不知道能不能落地的新技术,还不如扎扎实实打好基本功,好比系统、数据结构、算法、网络,新技术层出不穷,乱花渐入迷人眼,可是归根到底也是在这些基础知识上面创建起来的。ios
关于如何学习,有时间我们单独开一篇聊聊。下面进入今天正题,聊一聊在iOS开发领域里面几大架构的应用,包括MVC、MVP、MVVM、VIPER,作iOS开发通常都是比较熟悉MVC的,由于Apple已经为咱们量身定制了适合iOS开发的MVC架构。git
可是在写代码的过程当中你们确定会有这些疑问:为何个人VC愈来愈大,为何感受apple的MVC怪怪的不像真正的MVC,网络请求逻辑到底放在哪层,网上很火热的MVVM是否值得学习,VIPER又是什么鬼?程序员
我但愿下面的文字能为你们解除这些疑惑,我会在多个维度对这几个框架进行对比分析,指出他们的优劣,而后结合一个具体的DEMO用不一样的架构去实现,让你们对这些架构有一个直观的了解。固然这些都只是作抛砖引玉之用,阐述的也是个人我的理解,若有错误,欢迎指出,你们一块儿探讨进步~~github
MVC的理想模型以下图所示:web
各层的职责以下所示:算法
如上图所示,M和View应该是彻底隔离的,由C做为中间人来负责两者的交互,同时三者是彻底独立分开的,这样能够保证M和V的可测试性和复用性,可是通常因为C都是为特别的应用场景下的M和V作中介者,因此很难复用。数据库
可是实际上在iOS里面MVC的实现方式很难作到如上所述的那样,由于因为Apple的规范,一个界面的呈现都须要构建一个viewcontroller,而每一个viewcontroller都带有一个根view,这就致使C和V紧密耦合在一块儿构成了iOS里面的C层,这明显违背了MVC的初衷。
apple里面的MVC真正写起来大概以下图所示:
这也是massive controller的由来,具体的下面再讲
那么apple为何要这么干呢?完整的能够参考下apple对于MVC的解释,下面的引用是我摘自其中一段。简单来讲就是iOS里面的viewcontroller实际上是view和controller的组合,目的就是为了提升开发效率,简化操做。
摘自上面的连接
One can merge the MVC roles played by an object, making an object, for example, fulfill both the controller and view roles—in which case, it would be called a view controller. In the same way, you can also have model-controller objects. For some applications, combining roles like this is an acceptable design.
A model controller is a controller that concerns itself mostly with the model layer. It “owns” the model; its primary responsibilities are to manage the model and communicate with view objects. Action methods that apply to the model as a whole are typically implemented in a model controller. The document architecture provides a number of these methods for you; for example, an NSDocument object (which is a central part of the document architecture) automatically handles action methods related to saving files.
A view controller is a controller that concerns itself mostly with the view layer. It “owns” the interface (the views); its primary responsibilities are to manage the interface and communicate with the model. Action methods concerned with data displayed in a view are typically implemented in a view controller. An NSWindowController object (also part of the document architecture) is an example of a view controller.
对于简单界面来讲,viewcontroller结构确实能够提升开发效率,可是一旦须要构建复杂界面,那么viewcontroller很容易就会出现代码膨胀,逻辑满天飞的问题。
另外我想说一句,apple搞出viewcontroller(VC)这么个玩意初衷多是好的,写起来方便,提升开发效率嘛。确实应付简单页面没啥问题,可是有一个很大的弊端就是容易把新手代入歧途,认为真正的MVC就是这么干的,致使不少新手都把原本view层的代码都堆到了VC,好比在VC里面构建view、view的显示逻辑,甚至在VC里面发起网络请求。
这也是我当初以为VC很怪异的一个地方,由于它没办法归类到MVC的任何一层,直到看到了apple文档的那段话,才知道VC原来是个组合体。
下面来谈谈现有iOS架构下MVC各层的职责,这里要注意下,下面的Controller层指的是iOS里面的VC组合体
controller层(VC):
model层:
view层:
PS:
model层的业务逻辑通常都是和后台数据交互的逻辑,还有一些抽象的业务逻辑,好比格式化日期字符串为NSDateFormatter类型等
从上面的MVC各层职责划分就能够看出来C干了多少事,这仍是作了明确的职责划分的状况下,更不用提新手把各类view和model层的功能都堆到C层后的惨不忍睹。
在复杂界面里面的VC代码轻松超过千行,我之间就见过超过5000行代码的VC,找个方法只能靠搜索,分分钟想死的节奏。
形成massive controller的缘由的罪魁祸首就是apple的把view和Cotroller组合在一块儿,让VC同时作view和C的事,致使代码量激增,也违背了MVC原则。
下面来举一个简单的例子,先声明下我下面列举的例子主要来着这篇博客:
这篇文章质量很高,对三种模式的讲解比较深刻,关键还有例子来作横向对比,这是其余文章没有的。你们能够先看看这篇文章,本文的demo来自这篇文章,可是我按照本身的理解在其基础上作了一些修改,你们能够本身对比下,作出本身的选择。
还有一些图也是借鉴该篇文字,在此感谢做者~
先看两张图:
这个界面分为三个部分,顶部的我的信息展现,下面有两张列表,分别展现博客和草稿内容。
咱们先来看看通常新手都是怎么实现的
//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];
}
}];
[[userApi new] fetchUserDraftsWithUserId:132 completionHandler:^(NSError *error, id result) {
//if Error...略
[self.drafts addObjectsFromArray:result];
[self.draftTableView reloadData];
}];
}
//...略
- (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){
[self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:self.blogs[indexPath.row]] animated:YES];
}else{
[self.navigationController pushViewController:[draftDetailViewController instanceWithdraft:self.drafts[indexPath.row]] animated:YES];
}复制代码
//DraftCell
- (void)setDraft:(draft)draft {
_draft = draft;
self.draftEditDate = ...
}
//BlogCell
- (void)setBlog:(Blog)blog {
...同上
}复制代码
model:
Blog.h
=========
#import <Foundation/Foundation.h>
@interface Blog : NSObject
- (instancetype)initWithBlogId:(NSUInteger)blogId;
@property (copy, nonatomic) NSString *blogTitle;
@property (copy, nonatomic) NSString *blogSummary;
@property (assign, nonatomic) BOOL isLiked;
@property (assign, nonatomic) NSUInteger blogId;
@property (assign, nonatomic) NSUInteger likeCount;
@property (assign, nonatomic) NSUInteger shareCount;
@end
~~~~~~~~~~~~~~~~~~~~~
blog.m
========
#import "Blog.h"
@implementation Blog
@end复制代码
若是后续再增长需求,那么userVC的代码就会愈来愈多,这就是咱们上面说的massive controller出现了。维护性和可测试性无从谈起,咱们是按照apple的MVC架构写的呀,为何会出现这种问题呢?
暂且按下不表,咱们先看另一个问题,先把这个问题搞清楚了,对于后续文章的理解大有裨益。
我看到不少所谓的MVC的M层实现就如上面所示,只有几个干巴巴的属性。我以前也是一直这么写的,可是我一直以为有疑惑,以为这样写的话,怎么可能算的上一个单独的层呢?说是数据模型还差很少。
那么实现正确的M层姿式应该是什么样的呢?
你们具体能够看下面这篇文章,对于M层讲解的很是不错,可是对于文中的MVVM的理解我不敢苟同,你们见仁见智吧
下面的引用也是摘自这篇文章:
理解Model层:
首先要正确的理解MVC中的M是什么?他是数据模型吗?答案是NO。他的正肯定义是业务模型。也就是你全部业务数据和业务实现逻辑都应该定义在M层里面,并且业务逻辑的实现和定义应该和具体的界面无关,也就是和视图以及控制之间没有任何的关系,它是能够独立存在的,您甚至能够将业务模型单独编译出一个静态库来提供给第三方或者其余系统使用。
在上面经典MVC图中也很清晰的描述了这一点: 控制负责调用模型,而模型则将处理结果发送通知给控制,控制再通知视图刷新。所以咱们不能将M简单的理解为一个个干巴巴的只有属性而没有方法的数据模型。
其实这里面涉及到一个最基本的设计原则,那就是面向对象的基本设计原则:就是什么是类?类应该是一个个具备不一样操做和不一样属性的对象的抽象(类是属性和方法的集合)。 我想如今任何一个系统里面都没有出现过一堆只有数据而没有方法的数据模型的集合被定义为一个单独而抽象的模型层来供你们使用吧。 咱们不能把一个保存数据模型的文件夹来当作一个层,这并不符合横向切分的规则。
Model层实现的正确姿式:
定义的M层中的代码应该和V层和C层彻底无关的,也就是M层的对象是不须要依赖任何C层和V层的对象而独立存在的。整个框架的设计最优结构是V层不依赖C层而独立存在,M层不依赖C层和V层独立存在,C层负责关联两者,V层只负责展现,M层持有数据和业务的具体实现,而C层则处理事件响应以及业务的调用以及通知界面更新。三者之间必定要明确的定义为单向依赖,而不该该出现双向依赖
M层要完成对业务逻辑实现的封装,通常业务逻辑最多的是涉及到客户端和服务器之间的业务交互。M层里面要完成对使用的网络协议(HTTP, TCP,其余)、和服务器之间交互的数据格式(XML, JSON,其余)、本地缓存和数据库存储(COREDATA, SQLITE,其余)等全部业务细节的封装,并且这些东西都不能暴露给C层。全部供C层调用的都是M层里面一个个业务类所提供的成员方法来实现。也就是说C层是不须要知道也不该该知道和客户端和服务器通讯所使用的任何协议,以及数据报文格式,以及存储方面的内容。这样的好处是客户端和服务器之间的通讯协议,数据格式,以及本地存储的变动都不会影响任何的应用总体框架,由于提供给C层的接口不变,只须要升级和更新M层的代码就能够了。好比说咱们想将网络请求库从ASI换成AFN就只要在M层变化就能够了,整个C层和V层的代码不变。
文章还给出了实现的例子,我就不粘贴过来了,你们本身过去看看
总结来讲:
M层不该该是数据模型,放几个属性就完事了。而应该是承载业务逻辑和数据存储获取的职责一层。
如今咱们来看看到底该如何在iOS下面构建一个正确的MVC呢?
首先先达成一个共识:viewcontroller不是C层,而是V和C两层的混合体。
咱们看到在标准的iOS下的MVC实现里面,C层作了大部分事情,大致分为五个部分(见上面MVC各层职责),由于他是两个层的混合,为了给VC减负,咱们如今把VC只当作一个view的容器来使用。
这里我要解释下什么叫作view的容器,咱们知道apple的VC有一个self.view,全部要显示在界面的上面的view都必须经过addsubview来添加到这个根view上面来。同时VC还控制着view的生命周期。那么咱们可不能够把VC当作一个管理各个View的容器?
你们能够看这篇文章加深理解下我上面说的view container的概念:
此时VC的职责简化为以下三条职责:
前面两点很好理解吧,上面已经讲过了。第三点咱们接着往下看
回到咱们上面说的第四点的例子,什么缘由形成VC的代码愈来愈臃肿呢?
由于咱们对于view和model层的职责都划分的比较清楚,前者负责数据展现,后者负责数据获取,那么那些模棱两可的代码,放在这两层感受都不合适,就都丢到了VC里面,致使VC日益膨胀。
此时的代码组织以下图所示:
经过这张图能够发现, 用户信息页面(userVC)做为业务场景Scene须要展现多种数据M(Blog/Draft/UserInfo), 因此对应的有多个View(blogTableView/draftTableView/image…), 可是, 每一个MV之间并无一个链接层C, 原本应该分散到各个C层处理的逻辑所有被打包丢到了Scene(userVC)这一个地方处理, 也就是M-C-V变成了MM…-Scene-…VV, C层就这样莫名其妙的消失了.
另外, 做为V的两个cell直接耦合了M(blog/draft), 这意味着这两个V的输入被绑死到了相应的M上, 复用无从谈起.
最后, 针对这个业务场景的测试异常麻烦, 由于业务初始化和销毁被绑定到了VC的生命周期上, 而相应的逻辑也关联到了和View的点击事件, 测试只能Command+R, 点点点…
那么怎么实现正确的MVC呢?
以下图所示,该界面的信息分为三部分:我的信息、博客列表信息、草稿列表信息。咱们应该也按照这三部分分红三个小的MVC,而后经过VC拼接组装这三个子MVC来完成整个界面。
具体代码组织架构以下:
UserVC做为业务场景, 须要展现三种数据, 对应的就有三个MVC, 这三个MVC负责各自模块的数据获取, 数据处理和数据展现, 而UserVC须要作的就是配置好这三个MVC, 并在合适的时机通知各自的C层进行数据获取, 各个C层拿到数据后进行相应处理, 处理完成后渲染到各自的View上, UserVC最后将已经渲染好的各个View进行布局便可
具体的代码见最后的demo里面MVC文件夹。
关于demo的代码,我想说明一点本身的见解:在demo里面网络数据的获取,做者放到了一个单独的文件UserAPIManager
里面。我以为最好是放在和业务相关的demo里面,由于接口一旦多起来,一个文件很容易膨胀,若是按照业务分为多个文件,那么还不如干脆放在model里面更加清晰。
PS:
图中的blogTableViewHelper对应代码中的blogTableViewController,其余几个helper一样的
此时做为VC的userVC只须要作三件事:
userVC的代码大大减小,并且此时逻辑更加清楚,并且由于每一个模块的展现和交互是自管理的, 因此userVC只须要负责和自身业务强相关的部分便可。
另外若是须要在另一个VC上面展现博客列表数据,那么只须要把博客列表的view添加到VC的view上面,而后经过博客列表的controller获取下数据就能够了,这样就达到了复用的目的。
咱们经过上面的方法,把userVC里面的代码分到了三个子MVC里面,架构更加清晰明了,对于更加复杂的页面,咱们能够作更细致的分解,同时每一个子MVC其实还能够拆分红更细的MVC。具体的拆分粒度你们视页面复杂度灵活变通,若是预计到一个页面的业务逻辑后续会持续增长,还不如刚开始就拆分红不一样的子MVC去实现。若是只是简单的页面,那么直接把全部的逻辑都写到VC里面也没事。
上面的MVC改造主要是把VC和C加以区分,让MVC成为真正的MVC,而不是让VC当成C来用,通过改造后的MVC对付通常场景应该绰绰有余了。无论界面多复杂,均可以拆分红更小的MVC而后再组装起来。
写代码就是一个不断重构的过程,当项目愈来愈大,单独功能能够抽离出来做为一个大模块,打包成pod库(这个是组件化相关的知识点,后面我也会写一篇博客)。同时在模块内部你又能够分层拆分。争取作到单一原则,不要在一个类里面啥都往里面堆
总结下MVC的优势有以下几点:
通过上面的改造,MVC架构已经足够清晰了,按照应用场景(通常都是单页面)进行大的拆分,而后在根据业务拆分红小的MVC。不行就接着拆,拆层,拆模块。
可是MVC的最大弊端就是C的代码无法复用,因此能把C层的代码拆出来就尽可能拆,咱们来看看如今C层的功能还有哪些了
这就致使一个问题:
业务逻辑和业务展现强耦合: 能够看到, 有些业务逻辑(页面跳转/点赞/分享…)是直接散落在V层的, 这意味着咱们在测试这些逻辑时, 必须首先生成对应的V, 而后才能进行测试. 显然, 这是不合理的. 由于业务逻辑最终改变的是数据M, 咱们的关注点应该在M上, 而不是展现M的V
举个例子吧,好比demo中的点赞功能代码以下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
BlogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.title = cellHelper.blogTitleText;
cell.summary = cellHelper.blogSummaryText;
cell.likeState = cellHelper.isLiked;
cell.likeCountText = cellHelper.blogLikeCountText;
cell.shareCountText = cellHelper.blogShareCountText;
//点赞的业务逻辑
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已经赞过它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//点赞的业务展现
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
return cell;
}复制代码
经过代码能够清晰的看到,必须生成cell,而后点击cell上面的点赞按钮,才能够触发点赞的业务逻辑。
可是业务逻辑通常改变的model数据,view只是拿到model的数据进行展现。如今却把这两个本来独立的事情合在一块儿了。致使业务逻辑无法单独测试了。
下面提到的MVP正是为了解决这一问题而诞生的,咱们接着往下看。
下面关于MVP文字,有部分文字和图片摘抄自该文章,在此感谢做者,以前忘记放上连接,向做者道歉:
浅谈 MVC、MVP 和 MVVM 架构模式
MVC的缺点在于并无区分业务逻辑和业务展现, 这对单元测试很不友好. MVP针对以上缺点作了优化, 它将业务逻辑和业务展现也作了一层隔离, 对应的就变成了MVCP.
M和V功能不变, 原来的C如今只负责布局, 而全部的业务逻辑全都转移到了P层。P层处理完了业务逻辑,若是要更改view的显示,那么能够经过回调来实现,这样能够减轻耦合,同时能够单独测试P层的业务逻辑
MVP的变种及定义比较多,可是最终广为人知的是Martin Fowler 的发表的关于Presentation Model描述,也就是下面将要介绍的MVP。具体看下面这篇文章:
Martin Fowler 发表的 Presentation Model 文章
MVP从视图层中分离了行为(事件响应)和状态(属性,用于数据展现),它建立了一个视图的抽象,也就是presenter层,而视图就是P层的『渲染』结果。P层中包含全部的视图渲染须要的动态信息,包括视图的内容(text、color)、组件是否启用(enable),除此以外还会将一些方法暴露给视图用于某些事件的响应。
MVP的架构图以下所示:
在 MVP 中,Presenter 能够理解为松散的控制器,其中包含了视图的 UI 业务逻辑,全部从视图发出的事件,都会经过代理给 Presenter 进行处理;同时,Presenter 也经过视图暴露的接口与其进行通讯。
各层职责以下
VC层
controller层
model层
view层
Presenter层职责
咱们来分析下View层的职责,其中三、4两点和MVC的view相似,可是一、2两点不一样,主要是由于业务逻辑从C转移到了P,那么view的事件响应和状态变化确定就依赖P来实现了。
这里又有两种不一样的实现方式:
第一种方式保持了view的纯粹,只是做为被动view来展现数据和更改状态,可是却致使了P耦合了V,这样业务逻辑和业务展现有糅合到了一块儿,和上面的MVC同样了。
第二种方式保证了P的纯粹,让P只作业务逻辑,至于业务逻辑引起的数据显示的变化,让view实现对应的代理事件来实现便可。这增长了view的复杂和view对于P的耦合。
Demo中采用了第二种方式,可是demo中的view依赖是具体的presenter,若是是一个view对应多个presenter,那么能够考虑把presenter暴露的方法和属性抽象成protocol。让view依赖抽象而不是具体实现。
目前常见的 MVP 架构模式其实都是它的变种:Passive View 和 Supervising Controller。咱们先来开下第一种,也是用的比较多的一种
MVP 的第一个主要变种就是被动视图(Passive View);顾名思义,在该变种的架构模式中,视图层是被动的,它自己不会改变本身的任何的状态,它只是定义控价的样式和布局,自己是没有任何逻辑的。
而后对外暴露接口,外界经过这些接口来渲染数据到view来显示,全部的状态都是经过 Presenter 来间接改变的(通常都是在view里面实现Presenter的代理来改变的)。这样view能够最大程度被复用,可测试性也大大提升
能够参考这篇文章Passive View
在监督控制器中,视图层接管了一部分视图逻辑,主要就是同步简单的视图和模型的状态;而监督控制器就须要负责响应用户的输入以及一部分更加复杂的视图、模型状态同步工做。
对于用户输入的处理,监督控制器的作法与标准 MVP 中的 Presenter 彻底相同。可是对于视图、模型的数据同步工做,使用相似于下面要讲到MVVM中的双向绑定机制来实现两者的相互映射。
以下图所示:
监督控制器中的视图和模型层之间增长了二者之间的耦合,也就增长了整个架构的复杂性。和被动式图的MVP不一样的是:视图和模型之间新增了的依赖,就是双向的数据绑定;视图经过声明式的语法与模型中的简单属性进行绑定,当模型发生改变时,会通知其观察者视图做出相应的更新。
经过这种方式可以减轻监督控制器的负担,减小其中简单的代码,将一部分逻辑交由视图进行处理;这样也就致使了视图同时能够被 Presenter 和数据绑定两种方式更新,相比于被动视图,监督控制器的方式也下降了视图的可测试性和封装性。
能够参考这篇文章Supervising Controller
MVC的缺点在于并无区分业务逻辑和业务展现, 这对单元测试很不友好。 MVP针对以上缺点作了优化, 它将业务逻辑和业务展现也作了一层隔离, 对应的就变成了MVCP。 M和V功能不变, 原来的C如今只负责view的生成和做为view的代理(view的布局依然由SceneVC来完成), 而全部的业务逻辑全都转移到了P层.
咱们用MVP把上面的界面重构一次,架构图以下所示:
业务场景没有变化, 依然是展现三种数据, 只是三个MVC替换成了三个MVP(图中我只画了Blog模块), UserVC负责配置三个MVP(新建各自的VP, 经过VP创建C, C会负责创建VP之间的绑定关系), 并在合适的时机通知各自的P层(以前是通知C层)进行数据获取。
各个P层在获取到数据后进行相应处理, 处理完成后会通知绑定的View数据有所更新, V收到更新通知后从P获取格式化好的数据进行页面渲染, UserVC最后将已经渲染好的各个View进行布局便可.
另外, V层C层再也不处理任何业务逻辑, 全部事件触发所有调用P层的相应命令。
具体代码你们看demo就好了,下面我抽出点赞功能来对比分析下MVC和MVP的实现有何不一样
blogViewController.m
//点赞事件
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.presenter = self.presenter.allDatas[indexPath.row];//PV绑定
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
[weakCell.presenter likeBlogWithCompletionHandler:^(NSError *error, id result) {
!error ?: [weakCell showToastWithText:error.domain];
}];
}];
return cell;
}
==========================================
BlogCellPresenter.m
- (void)likeBlogWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
if (self.blog.isLiked) {
!completionHandler ?: completionHandler([NSError errorWithDomain:@"你已经赞过了哦~" code:123 userInfo:nil], nil);
} else {
BOOL response = [self.view respondsToSelector:@selector(blogPresenterDidUpdateLikeState:)];
self.blog.isLiked = YES;
self.blog.likeCount += 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
[[UserAPIManager new] likeBlogWithBlogId:self.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
self.blog.isLiked = NO;
self.blog.likeCount -= 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
}
!completionHandler ?: completionHandler(error, result);
}];
}
}
==========================================
BlogViewCell.m
#pragma mark - BlogCellPresenterCallBack
- (void)blogPresenterDidUpdateLikeState:(BlogCellPresenter *)presenter {
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.likeButton setTitleColor:presenter.isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)blogPresenterDidUpdateShareState:(BlogCellPresenter *)presenter {
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}
#pragma mark - Action
- (IBAction)onClickLikeButton:(UIButton *)sender {
!self.didLikeHandler ?: self.didLikeHandler();
}
#pragma mark - Setter
- (void)setPresenter:(BlogCellPresenter *)presenter {
_presenter = presenter;
presenter.view = self;
self.titleLabel.text = presenter.blogTitleText;
self.summaryLabel.text = presenter.blogSummaryText;
self.likeButton.selected = presenter.isLiked;
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}复制代码
blogViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
BlogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.title = cellHelper.blogTitleText;
cell.summary = cellHelper.blogSummaryText;
cell.likeState = cellHelper.isLiked;
cell.likeCountText = cellHelper.blogLikeCountText;
cell.shareCountText = cellHelper.blogShareCountText;
//点赞的业务逻辑
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已经赞过它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//点赞的业务展现
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
return cell;
}
===========================================
BlogViewCell.m
- (IBAction)onClickLikeButton:(UIButton *)sender {
!self.didLikeHandler ?: self.didLikeHandler();
}
#pragma mark - Interface
- (void)setTitle:(NSString *)title {
self.titleLabel.text = title;
}
- (void)setSummary:(NSString *)summary {
self.summaryLabel.text = summary;
}
- (void)setLikeState:(BOOL)isLiked {
[self.likeButton setTitleColor:isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)setLikeCountText:(NSString *)likeCountText {
[self.likeButton setTitle:likeCountText forState:UIControlStateNormal];
}
- (void)setShareCountText:(NSString *)shareCountText {
[self.shareButton setTitle:shareCountText forState:UIControlStateNormal];
}复制代码
从上面的代码对比能够看出来,MVP的代码量比MVC多出来整整一倍,可是MVP在层次上更加清晰,业务逻辑和业务展现完全分离,让presenter和view能够单独测试,而MVC则把这二者混在一块儿,无法单独测试。实际项目中你们能够本身根据项目需求来选择。
下面是MVC下点赞的逻辑
//点赞的业务逻辑
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已经赞过它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//点赞的业务展现
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];复制代码
能够看到业务逻辑(改变model数据)和业务展现(改变cell的数据)糅杂在一块儿,若是我要测试点赞这个业务逻辑,那么就必须生成cell,而后点击cell的按钮,去触发点赞的业务逻辑才能够测试
再看看MVP下的点赞逻辑的实现
业务逻辑:
BlogCellPresenter.m
- (void)likeBlogWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
if (self.blog.isLiked) {
!completionHandler ?: completionHandler([NSError errorWithDomain:@"你已经赞过了哦~" code:123 userInfo:nil], nil);
} else {
BOOL response = [self.view respondsToSelector:@selector(blogPresenterDidUpdateLikeState:)];
self.blog.isLiked = YES;
self.blog.likeCount += 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
[[UserAPIManager new] likeBlogWithBlogId:self.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
self.blog.isLiked = NO;
self.blog.likeCount -= 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
}
!completionHandler ?: completionHandler(error, result);
}];
}
}复制代码
业务展现:
BlogViewCell.m
#pragma mark - BlogCellPresenterCallBack
- (void)blogPresenterDidUpdateLikeState:(BlogCellPresenter *)presenter {
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.likeButton setTitleColor:presenter.isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)blogPresenterDidUpdateShareState:(BlogCellPresenter *)presenter {
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}复制代码
能够看到在MVP里面业务逻辑和业务展现是分在不一样的地方实现,那么就能够分开测试两者了,而不想MVC那样想测试下业务逻辑,还必须生成一个view,这不合理,由于业务逻辑改变的model的数据,和view无关。
MVP相对于MVC, 它其实只作了一件事情, 即分割业务展现和业务逻辑. 展现和逻辑分开后, 只要咱们能保证V在收到P的数据更新通知后能正常刷新页面, 那么整个业务就没有问题. 由于V收到的通知其实都是来自于P层的数据获取/更新操做, 因此咱们只要保证P层的这些操做都是正常的就能够了. 即咱们只用测试P层的逻辑, 没必要关心V层的状况
MVVM是由微软提出来的,可是这个架构也是在下面这篇文章的基础上发展起来的:
Martin Fowler 发表的 Presentation Model 文章
这篇文章上面就提到过,就是MVP的原型,也就是说MVVM实际上是在MVP的基础上发展起来的。那么MVVM在MVP的基础上改良了啥呢?答案就是数据绑定,下面会慢慢铺开来说。网上关于MVVM的定义太多,没有一个统一的说法,有的甚至彻底相反。关于权威的MVVM解释,你们能够看下微软的官方文档:
里面关于MVVM提出的动机,解决的痛点,各层的职责都解释的比较清楚。要追本溯源看下MVVM的前世此生,那么上面的Martin Fowler发表的文章也能够看看
2005 年,John Gossman 在他的博客上公布了Introduction to Model/View/ViewModel pattern for building WPF apps 一文。MVVM 与 Martin Fowler 所说的 PM 模式实际上是彻底相同的,Fowler 提出的 PM 模式是一种与平台无关的建立视图抽象的方法,而 Gossman 的 MVVM 是专门用于 WPF 框架来简化用户界面的建立的模式;咱们能够认为 MVVM 是在 WPF 平台上对于 PM 模式的实现。
从 Model-View-ViewModel 这个名字来看,它由三个部分组成,也就是 Model、View 和 ViewModel;其中视图模型(ViewModel)其实就是 MVP 模式中的P,在 MVVM 中叫作VM。
除了咱们很是熟悉的 Model、View 和 ViewModel 这三个部分,在 MVVM 的实现中,还引入了隐式的一个 Binder层,这也是MVVM相对MVP的进步,而声明式的数据和命令的绑定在 MVVM 模式中就是经过binder层来完成的,RAC是iOS下binder的优雅实现,固然MVVM没有RAC也彻底能够运行。
下图展现了iOS下的MVC是如何拆分红MVVM的:
MVVM和MVP相对于MVC最大的改进在于:P或者VM建立了一个视图的抽象,将视图中的状态和行为抽离出来造成一个新的抽象。这能够把业务逻辑(P/VM)和业务展现(V)分离开单独测试,而且达到复用的目的,逻辑结构更加清晰
MVVM各层的职责和MVP的相似,VM对应P层,只是在MVVM的View层多了数据绑定的操做
上面提到过MVVM相对于MVC的改进是对VM/P和view作了双向的数据和命令绑定,那么这么作的好处是什么呢?仍是看上面MVP的点赞的例子
MVP的点赞逻辑以下:
点击cell按钮--->调用P的点赞逻辑---->点同意功后,P改变M的数据--->P回调Cell的代理方法改变cell的显示(点同意功,赞的个数加1,同时点赞数变红,不然不改变赞的个数也不变色)
上面就是一个事件完整过程,能够看到要经过四步来完成,并且每次都要把P的状态同步到view,当事件多起来的时候,这样写就很麻烦了。那有没有一种简单的机制,让view的行为和状态和P的行为状态同步呢?
答案就是MVVM的binder机制。
点赞的MVP的代码看上面MVP章节便可,咱们来看下在MVVM下的点赞如何实现的:
BlogCellViewModel.h
- (BOOL)isLiked;
- (NSString *)blogTitleText;
- (NSString *)blogSummaryText;
- (NSString *)blogLikeCount;
- (NSString *)blogShareCount;
- (RACCommand *)likeBlogCommand;
========================================
BlogCellViewModel.m
@weakify(self);
self.likeBlogCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
RACSubject *subject = [RACSubject subject];
if (self.isLiked) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.isLiked = NO;
self.blogLikeCount = self.blog.likeCount - 1;
[subject sendCompleted];
});
} else {
self.isLiked = YES;
self.blogLikeCount = self.blog.likeCount + 1;
[[UserAPIManager new] likeBlogWithBlogId:self.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
self.isLiked = NO;
self.blogLikeCount = self.blog.likeCount - 1;
}
error ? [subject sendError:error] : [subject sendCompleted];
}];
}
return subject;
}];复制代码
- (void)awakeFromNib {
[super awakeFromNib];
//数据绑定操做
@weakify(self);
RAC(self.titleLabel, text) = RACObserve(self, viewModel.blogTitleText);
RAC(self.summaryLabel, text) = RACObserve(self, viewModel.blogSummaryText);
RAC(self.likeButton, selected) = [RACObserve(self, viewModel.isLiked) ignore:nil];
[RACObserve(self, viewModel.blogLikeCount) subscribeNext:^(NSString *title) {
@strongify(self);
[self.likeButton setTitle:title forState:UIControlStateNormal];
}];
[RACObserve(self, viewModel.blogShareCount) subscribeNext:^(NSString *title) {
@strongify(self);
[self.shareButton setTitle:title forState:UIControlStateNormal];
}];
}
- (IBAction)onClickLikeButton:(UIButton *)sender {
//事件响应
if (!self.viewModel.isLiked) {
[[self.viewModel.likeBlogCommand execute:nil] subscribeError:^(NSError *error) {
[self showToastWithText:error.domain];
}];
} else {
[self showAlertWithTitle:@"提示" message:@"肯定取消点赞吗?" confirmHandler:^(UIAlertAction *confirmAction) {
[[self.viewModel.likeBlogCommand execute:nil] subscribeError:^(NSError *error) {
[self showToastWithText:error.domain];
}];
}];
}
}复制代码
能够看到相对MVP的view触发P的业务逻辑,而后P再回调改变View的显示的操做,使用MVVM的数据绑定来实现让逻辑更加清晰,代码也更少。这就是MVVM相对于MVP的改进之处
前面讲到的几个架构大多脱胎于MVC,可是VIPER和MVC没有啥关系,是一个全新的架构。从一点就能够看出来:前面几个MVX框架在iOS下是没法摆脱Apple的viewcontroller影响的,可是VIPER完全弱化了VC的概念,让VC变成了真正意义上的View。把VC的职责进行了完全的拆分,分散到各个子层里面了
下图就是VIPER的架构图
从上面能够看出VIPER应该是全部架构里面职责划分最为明确的,真正作到了SOLID原则。其余架构由于有VC的存在,或多或少都会致使各层的职责划分不明确。可是也因为VIPER的分层过多,而且是惟一一个把界面路由功能单独分离出来放到一个单独的类里面处理,全部的事件响应和界面跳转都须要本身处理,这致使代码复杂度大大增长。
Apple苦心孤诣的给咱们搞出一个VC,虽然会致使层次耦合,可是也确实简化了开发流程,而VIPER则是完全抛弃了VC,从新进行分层,作到了每一个模块均可以单独测试和复用,可是也致使了代码过多、逻辑比较绕的问题。
就我我的经验来讲,其实只要作好分层和规划,MVC架构足够应付大多数场景。有些文章上来就说MVVM是为了解决C层臃肿, MVC难以测试的问题, 其实并非这样的. 按照架构演进顺序来看, C层臃肿大部分是没有拆分好MVC模块, 好好拆分就好了, 用不着MVVM。 而MVC难以测试也能够用MVP来解决, 只是MVP也并不是完美, 在VP之间的数据交互太繁琐, 因此才引出了MVVM。 而VIPER则是跳出了MVX架构,本身开辟一条新的路。
VIPER是很是干净的架构。它将每一个模块与其余模块隔离开来。所以,更改或修复错误很是简单,由于您只须要更新特定的模块。此外,VIPER还为单元测试建立了一个很是好的环境。因为每一个模块独立于其余模块,所以保持了低耦合。在开发人员之间划分工做也很简单。
不该该在小项目中使用VIPER,由于MVP或MVC就足够了
关于究竟是否应该在项目中使用VIPER,你们能够看下Quora上面的讨论:
Should I use Viper architecture for my next iOS application, or it is still very new to use?
PS:
数据的获取应该单独放到一个层,而不该该放到Interactor里面
能够看到一个应用场景的全部功能点都被分离成功能彻底独立的层,每一个层的职责都是单一的。在VIPER架构中,每一个块对应于具备特定任务,输入和输出的对象。它与装配线中的工做人员很是类似:一旦工做人员完成其对象上的做业,该对象将传递给下一个工做人员,直到产品完成。
层之间的链接表示对象之间的关系,以及它们彼此传递的信息类型。经过协议给出从一个实体到另外一个实体的通讯。
这种架构模式背后的想法是隔离应用程序的依赖关系,平衡实体之间的责任分配。基本上,VIPER架构将您的应用程序逻辑分为较小的功能层,每一个功能都具备严格的预约责任。这使得更容易测试层之间边界的交互。它适用于单元测试,并使您的代码更可重用。
VIPER架构有不少好处,但重要的是要将其用于大型和复杂的项目。因为所涉及的元素数量,这种架构在启动新的小型项目时会致使开销,所以VIPER架构可能会对无心扩展的小型项目形成太高的影响。所以,对于这样的项目,最好使用别的东西,例如MVC。
咱们来构建一个小的VIPER应用,我不想把上面的demo用VIPER再重写一次了,由于太麻烦了,因此就写一个简单的demo给你们演示下VIPER,可是麻雀虽小五脏俱全,该有的功能都有了。
如上图所示,有两个界面contactlist和addcontact,在contactlist的右上角点击添加按钮,跳转到addcontact界面,输入firstname和secondname后点击done按钮,回到contactlist界面,新添加的用户就显示在该界面上了。
先看下项目的架构,以下所示:
能够看到每一个界面都有6个文件夹,还有两个界面公用的Entities文件夹,每一个文件夹对应一个分层,除了VIPER的五层以外,每一个界面还有两个文件夹:Protocols和DataManager层。
Protocols定义的VIPER的每层须要遵照的协议,每层对外暴露的操做都通过protocol抽象了,这样能够针对抽象编程。DataManager定义的是数据操做,包括从本地和网络获取、存储数据的操做。
下面先来看看Protocols类的实现:
import UIKit
/**********************PRESENTER OUTPUT***********************/
// PRESENTER -> VIEW
protocol ContactListViewProtocol: class {
var presenter: ContactListPresenterProtocol? { get set }
func didInsertContact(_ contact: ContactViewModel)
func reloadInterface(with contacts: [ContactViewModel])
}
// PRESENTER -> router
protocol ContactListRouterProtocol: class {
static func createContactListModule() -> UIViewController
func presentAddContactScreen(from view: ContactListViewProtocol)
}
//PRESENTER -> INTERACTOR
protocol ContactListInteractorInputProtocol: class {
var presenter: ContactListInteractorOutputProtocol? { get set }
var localDatamanager: ContactListLocalDataManagerInputProtocol? { get set }
func retrieveContacts()
}
/**********************INTERACTOR OUTPUT***********************/
// INTERACTOR -> PRESENTER
protocol ContactListInteractorOutputProtocol: class {
func didRetrieveContacts(_ contacts: [Contact])
}
//INTERACTOR -> LOCALDATAMANAGER
protocol ContactListLocalDataManagerInputProtocol: class {
func retrieveContactList() throws -> [Contact]
}
/**********************VIEW OUTPUT***********************/
// VIEW -> PRESENTER
protocol ContactListPresenterProtocol: class {
var view: ContactListViewProtocol? { get set }
var interactor: ContactListInteractorInputProtocol? { get set }
var wireFrame: ContactListRouterProtocol? { get set }
func viewDidLoad()
func addNewContact(from view: ContactListViewProtocol)
}复制代码
其实从该类中就能够清晰看到VIPER各层之间的数据流向,很是清晰。
而后就是各层去具体实现这些协议了,这里就不贴代码了,你们能够去demo里面看。下面主要讲一下路由层,这是VIPER所独有的,其余的MVX架构都是把路由放到了VC里面作,而VIPER架构由于完全摒弃了VC,因此把界面之间的路由单独作了一层。
下面来具体看看
import UIKit
class ContactListRouter: ContactListRouterProtocol {
//生成ContactList的View
class func createContactListModule() -> UIViewController {
let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController")
if let view = navController.childViewControllers.first as? ContactListView {
let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter()
let interactor: ContactListInteractorInputProtocol = ContactListInteractor()
let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager()
let router: ContactListRouterProtocol = ContactListRouter()
//绑定VIPER各层
view.presenter = presenter
presenter.view = view
presenter.wireFrame = router
presenter.interactor = interactor
interactor.presenter = presenter
interactor.localDatamanager = localDataManager
return navController
}
return UIViewController()
}
//导航到AddContact界面
func presentAddContactScreen(from view: ContactListViewProtocol) {
guard let delegate = view.presenter as? AddModuleDelegate else {
return
}
let addContactsView = AddContactRouter.createAddContactModule(with: delegate)
if let sourceView = view as? UIViewController {
sourceView.present(addContactsView, animated: true, completion: nil)
}
}
static var mainStoryboard: UIStoryboard {
return UIStoryboard(name: "Main", bundle: Bundle.main)
}
}复制代码
ContactListRouter有三个功能:
第一个功能被APPDelegate调用:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let contactsList = ContactListRouter.createContactListModule()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = contactsList
window?.makeKeyAndVisible()
return true
}复制代码
第二个功能点击ContactList的界面的右上角添加按钮调用:
class ContactListView: UIViewController {
var presenter: ContactListPresenterProtocol?
//点击添加按钮,调用presenter的对应业务逻辑
@IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) {
presenter?.addNewContact(from: self)
}
}
=================
//presenter实现添加按钮的业务逻辑,调用router的跳转逻辑,调到AddContact界面
class ContactListPresenter: ContactListPresenterProtocol {
weak var view: ContactListViewProtocol?
var interactor: ContactListInteractorInputProtocol?
var router: ContactListRouterProtocol?
func addNewContact(from view: ContactListViewProtocol) {
router?.presentAddContactScreen(from: view)
}
}复制代码
一样的AddContact的router层的功能也相似,你们能够本身去领会。从上面的代码能够看到VIPER架构的最大特色就是实现了SOLID原则,每层只作本身的事情,职责划分的很是清楚,本身的任务处理完后就交给下一个层处理。
看完上面的代码是否是以为这也太绕了吧,是的,我也这么以为,可是不得不说VIPER的优势也有不少,上面已经列举了。因此若是是中小型的项目,仍是用MVX架构吧,若是MVX架构依然hold不住你的每一个类都在膨胀,那么试试VIPER你可能会有新的发现。
其实我倒以为VIPER完全放弃Apple的VC有点得不偿失,我的仍是喜欢用VC来作界面路由,而不是单独搞一个router层去路由,这样既借鉴了VIPER的优势,有兼顾了VC的好处,具体的看最后的demo,我这里就不展开说了,你们作一个对比应该就有了解。
号称是惟一一本介绍VIPER的书籍,然而完整版只有俄语的,不过咱们有万能的谷歌翻译,只要不是火星文均可以看啦~
因为VIPER架构的类比较多,还要写一堆模块之间的协议,若是每次都要手写的话,太心累了~ 因此你们能够试试下面的代码生成器,一次生成VIPER的代码模板