[转]iOS架构模式——MV(X)的理解与实战
作为一个iOS程序员,MVC一定是我们耳熟能详的一种架构模式,而且当你的项目规模不大的时候,MVC也确实有它的优势,它的开发效率确实是足够高。但当你的项目发展的一定的规模,你会发现传统的MVC模式会导致C层代码量剧增,维护困难等一系列问题,这个时候我们就需要考虑一些其它模式了。
1-1-1. MV(X)的基本要素
常用的架构模式
- MVC
- MVVM
- MVP
- VIPER
前面三种模式都由三个模块组成:
- Models —— 数据层,负责数据的处理。
- Views —— 展示层,即所有的UI
- Controller/Presenter/ViewModele(控制器/展示器/视图模型)——它们负责View与Mode之间的调配
1-1. MVC
1-1-1-1-1. 传统的MVC
我们所熟知的MVC其实Apple给我们提供的Cocoa MVC,但其实MVC最先产生于Web,它原来的样子应该是这样的
1-1-1. 传统MVC
在这种架构下,View是无状态的,在Model变化的时候它只是简单的被Controller重绘,比如网页中你点击了一个新的链接,整个页面就重新加载。尽管这种MVC在iOS应该里面可以实现,但是由于MVC的三个模块都紧密耦合了,每一个模块都和其它两种模块有联系,所以即便是实现了也没有什么意义。这种耦合还降低了它们的可重用性,所以,传统的MVC在iOS中可以舍弃了。
1-1-2. Apple的MVC
1-1-3. Cocoa MVC
Apple提供的MVC中,View和Model之间是相互独立的,它们只通过Controller来相互联系。可惜的是Controller得重用性太差,因为我们一般都把冗杂的业务逻辑放在了Controller中。
现实中,我们的MVC一般是这样的
现实MVC
为什么会这样呢?主要还是因为我们的UIViewController它本身就拥有一个VIew,这个View是所有视图的根视图,而且View的生命周期也都由Controoler负责管理,所以View和Controller是很难做到相互独立的。虽然你可以把控制器里的一些业务逻辑和数据转换工作交给Model,但是你却没有办法将一些工作让View来分摊,因为View的主要职责只是将用户的操作行为交给Controller去处理而已。于是Controller最终就变成了所有东西的代理和数据源,甚至还有网络请求…..还有……所以我们写的Controller代码量一般都是非常大的,随着当业务需求的增加,Controller的代码量会一直增长,而相对来说View和Model的代码量就比较稳定,所以也有人把MVC叫做Massive View Controller,因为Controller确实显得有些臃肿。
在这里关于Model的划分,其实有一个胖Model和瘦Model之分,它们的差别主要就是把Controller的部分数据处理职责交给了胖Model。
胖Model(Fat Model):
胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用做额外的操作或者只做非常少的操作就能将数据应用在View上。
FatModel做了这些弱业务之后,Controller可以变得相对skinny一点,它只需要关注强业务代码。而强业务变动的可能性要比弱业务大得多,弱业务相对稳定,所以弱业务塞给Model不会有太大问题。另一方面,弱业务重复出现的频率要大于强业务,对复用性要求更高,如果这部分业务写在Controller,会造成代码冗余,类似的代码会洒得到处都是,而且一旦弱业务有修改,你就会需要修改所有地方。如果塞到了Model中,就只需要改Model就够了。
但是胖Mpdel也不是就是没有缺点的,它的缺点就在于胖Model相对比较难移植,虽然只是包含弱业务,但是它毕竟也是业务,迁移的时候很容易拔出罗布带出泥,也就是说它耦合了它的业务。而且软件是会成长的,FatModel也很有可能随着软件的成长越来越Fat,最后难以维护。
瘦Model(Slim Model):
瘦Model只负责业务数据的表达,所有业务无论强弱一律人给Controller。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或者方法来对弱业务做抽象,强业务依旧交给Controller。
由于Slim Model跟业务完全无关,它的数据可以交给任何一个能处理它数据的Helper或其他的对象,来完成业务。在代码迁移的时候独立性很强,很少会出现拔出萝卜带出泥的情况。另外,由于SlimModel只是数据表达,对它进行维护基本上是0成本,软件膨胀得再厉害,SlimModel也不会大到哪儿去。缺点就在于,Helper这种做法也不见得很好,由于Model的操作会出现在各种地方,SlimModel很容易出现代码重复,在一定程度上违背了DRY(Don’t Repeat Yourself)的思路,Controller仍然不可避免在一定程度上出现代码膨胀。
综上所述,Cocoa MVC在各方面的表现如下:
- 划分 – View 和 Model 确实是实现了分离,但是 View 和 Controller 耦合的太 厉害
- 可测性 – 因为划分的不够清楚,所以能测的基本就只有 Model 而已
- 易用 – 相较于其他模式,它的代码量最少。而且基本上每个人都很熟悉它,即便是没太多经验的开发者也能维护。
1-2. MVP
MVP
看起来和Cocoa MVC很像,也确实很像。但是,在MVC中View和COntroller是紧密耦合的,而在MVP中,Presenter完全不关注ViewController的生命周期,而且View也能被简单mock出来,所以在Presenter里面基本没有什么布局相关的代码,它的职责只是通过数据和状态更新View。
而且在MVP中,UIVIewController的那些子类其实是属于View的。这样就提供了更好的可测性,只是开发速度会更高,因为你必须手动去创建数据和绑定事件。
下面我写了个简单的Demo
MVPDemo
由于这里主要是学习架构模式思想,所以我的命名简单粗暴,希望大家理解。
界面1
界面也很简单,就是通过点击按钮修改两个label显示的内容
Model很简单,就是一个数据结构,但在实际应用中,你可以将网络请求等一些数据处理放在这里
1 2 3 4 5 |
<span class="">@interface Model : NSObject</span> <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) <span class="">NSString</span> *first; <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) <span class="">NSString</span> *second; <span class="">@end</span> |
要让Presenter和View通信,所以我们定义一个协议,以实现Presenter向View发送命令
1 2 3 4 5 |
<span class="">@protocol MyProtocol <NSObject></span> - (<span class="">void</span>)setFirst:(<span class="">NSString</span> *)first; - (<span class="">void</span>)setSecond:(<span class="">NSString</span> *)second; <span class="">@end</span> |
view/VIewController,实现该协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
.h <span class="">@interface ViewController : UIViewController</span> <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) <span class="">UILabel</span> *firstLabel; <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) <span class="">UILabel</span> *secondLabel; <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) <span class="">UIButton</span> *tapButton; <span class="">@end</span> .m主要代码 - (<span class="">void</span>)viewDidLoad { [<span class="">super</span> viewDidLoad]; [<span class="">self</span>.view addSubview:<span class="">self</span>.firstLabel]; [<span class="">self</span>.view addSubview:<span class="">self</span>.secondLabel]; [<span class="">self</span>.view addSubview:<span class="">self</span>.tapButton]; <span class="">self</span>.presenter = [Presenter new]; [<span class="">self</span>.presenter attachView:<span class="">self</span>]; } - (<span class="">void</span>)buttonClicked{ [<span class="">self</span>.presenter reloadView]; } - (<span class="">void</span>)setFirst:(<span class="">NSString</span> *)first{ <span class="">self</span>.firstLabel.text = first; } - (<span class="">void</span>)setSecond:(<span class="">NSString</span> *)second{ <span class="">self</span>.secondLabel.text = second; } |
Presenter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
.h <span class="">@interface Presenter : NSObject</span> - (<span class="">void</span>)attachView:(<span class="">id</span> <MyProtocol>)attachView; - (<span class="">void</span>)reloadView; <span class="">@end</span> .m <span class="">@interface Presenter()</span> <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">weak</span>) <span class="">id</span> <MyProtocol> view; <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) Model *model; <span class="">@end</span> <span class="">@implementation Presenter</span> - (<span class="">instancetype</span>)init { <span class="">self</span> = [<span class="">super</span> init]; <span class="">if</span> (<span class="">self</span>) { <span class="">self</span>.model = [Model new]; <span class="">self</span>.model.first = <span class="">@"first"</span>; <span class="">self</span>.model.second = <span class="">@"second"</span>; } <span class="">return</span> <span class="">self</span>; } - (<span class="">void</span>)attachView:(<span class="">id</span><MyProtocol>)attachView{ <span class="">self</span>.view = attachView; } - (<span class="">void</span>)reloadView{ <span class="">//可以在这里做一些数据处理</span> [<span class="">self</span>.view setFirst:<span class="">self</span>.model.first]; [<span class="">self</span>.view setSecond:<span class="">self</span>.model.second]; } <span class="">@end</span> |
这里只是一个简单的Demo,其实思想很简单,就是讲业务逻辑交给Presenter,而Presenter以命令的形式来控制View。
1-2-1-1-1. 一些说明:
MVP架构拥有三个真正独立的分层,所以在组装的时候会有一些问题,而MVP也成了第一个披露这种问题的架构,因为我们不想让View知道Model的信息,所以在当前的Controller去组装是不正确的,我们应该在另外的地方完成组装。比如我们可以创建一个应用层的Router服务,让它来负责组装和View-to-View的转场。这个问题下很多模式中都存在。
下面总结一下MVP的各方面表现:
- 划分——我们把大部分职责都分配到了Presenter和Model里面,而View基本不需要做什么
- 可测性——我们可以通过View来测试大部分业务逻辑
- 易用——代码量差不多是MVC架构的两倍,但是MVP的思路还是蛮清晰的
另外,MVP还有一个变体,它的不同主要就是添加了数据绑定。这个版本的MVP的View和Model直接绑定,而Presenter仍然继续处理View上的用户操作,控制View的显示变化。这种架构和传统的MVC类似,所以我们基本可以舍弃。
1. MVVM
MVVM可以说是MV(X)系列中最新兴起的也是最出色的一种架构,而它也广受我们iOS程序员喜爱。
MVVM和MVP很像:
- 把ViewController看成View
- View和Model之间没有紧耦合
另外它还让VIew和ViewModel做了数据绑定。ViewModel可以调用对Model做更改,也可以再Model更新的时候对自身进行调整,然后通过View和ViewModel之间的绑定,对View进行相应的更新。
1-1-1-1-1. 关于绑定
在iOS平台上面有KVO和通知,但是用起来总是觉得不太方便,所以有一些三方库供我们选择:
- 基于KVO的绑定库,如 RZDataBinding 或者 SwiftBond
- 使用全量级的 函数式响应编程 框架,比如ReactiveCocoaRxSwift 或者PromiseKit
实际上,我们在提到MVVM的时候就很容易想到ReactiveCocoa,它也是我们在iOS中使用MVVM的最好工具。但是相对来说它的学习成本和维护成本 也是比较高的,而且一旦你应用不当,很可能造成灾难性的问题。
下面我暂时不用RAC来简单展示一下MVVM:
界面很简单,就是点击一个button修改label里面的数据
界面
Model
1 2 3 4 5 6 7 8 9 |
<span class="">@interface MVVMModel : NSObject</span> <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">copy</span>) <span class="">NSString</span> *text; <span class="">@end</span> <span class="">@implementation MVVMModel</span> - (<span class="">NSString</span> *)text{ _text = [<span class="">NSString</span> stringWithFormat:<span class="">@"newText%d"</span>,rand()]; <span class="">return</span> _text; } |
ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<span class="">@interface MVVMViewModel : NSObject</span> - (<span class="">void</span>)changeText; <span class="">@end</span> <span class="">@interface MVVMViewModel()</span> <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) <span class="">NSString</span> *text; <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) MVVMModel *model; <span class="">@end</span> <span class="">@implementation MVVMViewModel</span> - (<span class="">instancetype</span>)init { <span class="">self</span> = [<span class="">super</span> init]; <span class="">if</span> (<span class="">self</span>) { <span class="">self</span>.model = [MVVMModel new]; } <span class="">return</span> <span class="">self</span>; } - (<span class="">void</span>)changeText{ <span class="">self</span>.text = <span class="">self</span>.model.text;; } |
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<span class="">@interface MVVMViewController ()</span> <span class="">@property</span> (<span class="">weak</span>, <span class="">nonatomic</span>) <span class="">IBOutlet</span> <span class="">UILabel</span> *textLabel; <span class="">@property</span> (<span class="">nonatomic</span>, <span class="">strong</span>) MVVMViewModel *viewModel; <span class="">@end</span> <span class="">@implementation MVVMViewController</span> - (<span class="">void</span>)viewDidLoad { [<span class="">super</span> viewDidLoad]; <span class="">self</span>.viewModel = [[MVVMViewModel alloc]init]; [<span class="">self</span>.viewModel addObserver:<span class="">self</span> forKeyPath:<span class="">@"text"</span> options:<span class="">NSKeyValueObservingOptionNew</span> context:<span class="">nil</span>]; } - (<span class="">IBAction</span>)buttonClicked:(<span class="">UIButton</span> *)sender { [<span class="">self</span>.viewModel changeText]; } - (<span class="">void</span>)observeValueForKeyPath:(<span class="">NSString</span> *)keyPath ofObject:(<span class="">id</span>)object change:(<span class="">NSDictionary</span><<span class="">NSKeyValueChangeKey</span>,<span class="">id</span>> *)change context:(<span class="">void</span> *)context{ <span class="">self</span>.textLabel.text = change[<span class="">@"new"</span>]; } |
MVVM的核心就是View和ViewModel的一个绑定,这里我只是简单的通过KVO实现,看起来并不是那么优雅,想要深度使用的话我觉得还是有必要学习一下RAC的,完整的Demo(https://github.com/RhettTamp/MVXDemo)
下面我们再来对MVVM的各方面表现做一个评价:
- 划分——MVVM 框架里面的 View 比 MVP 里面负责的事情要更多一些。因为前者是通过 ViewModel 的数据绑定来更新自身状态的,而后者只是把所有的事件统统交给 Presenter 去处理就完了,自己本身并不负责更新。
- 可测性—— 因为 ViewModel 对 View 是一无所知的,这样我们对它的测试就变得很简单。View 应该也是能够被测试的,但是可能因为它对 UIKit 的依赖,你会直接略过它。
- 易用——它比MVP会更加简洁,因为在 MVP 下你必须要把 View 的所有事件都交给 Presenter 去处理,而且需要手动的去更新 View 的状态;而在 MVVM 下,你只需要用绑定就可以解决。
综上:MVVM 真的很有魅力,因为它不仅结合了上述几种框架的优点,还不需要你为视图的更新去写额外的代码(因为在 View 上已经做了数据绑定),另外它在可测性上的表现也依然很棒。
为了简单易懂,以上的Demo都非常简洁,不知道看了这篇博客能否加深你对MV(X)的一些理解,这些理解也仅作为我个人的一些参考,有什么不对的地方希望大家指出。
[resource]iOS架构模式——MV(X)的理解与实战