设计框架(MVC、MVP、MVVM、VIPER)的演化说明总结

今天忽然想起来了软件的架构设计,看了好几篇文章,发现不同的文章里面有好多冲突,具体就是MVC框架和MVP框架的,包括阮一峰老师,寒冬winter大牛,唐巧大牛的文章,以及网友翻译的译文,最后找了多篇文章,再想想,终于算是知道了点其中的皮毛。

这其中我比较推崇的是寒冬winter的文章和王哼哼的译文,综合的总结一下,欢迎批评指正。

现在我们面对架构设计模式的时候有了很多选择:

  • MVC
  • MVP
  • MVVM
  • VIPER

首先前三种模式都是把所有的实体归类到了下面三种分类中的一种:

Models(模型)?—?数据层,或者负责处理数据的 数据接口层。比如 Person 和 PersonDataProvider 类

Views(视图) - 展示层(GUI)。对于 iOS 来说所有以 UI 开头的类基本都属于这层。

Controller/Presenter/ViewModel(控制器/展示器/视图模型)

它是 Model 和 View 之间的胶水或者说是中间人。一般来说,当用户对 View 有操作时它负责去修改相应 Model;当 Model 的值发生变化时它负责去更新对应 View。

将实体进行分类之后我们可以:

  • 更好的理解
  • 重用(主要是 View 和 Model)
  • 对它们独立的进行测试

一、MVC架构

1.1、由来

在1979年,经典MVC模式被提出。

在当时,人们一直试图将纯粹描述思维中的对象与跟计算机环境打交道的代码隔离开来,而Trygve Reenskaug在跟一些人的讨论中,逐渐剥离出一系列的概念,最初是Thing、Model、View、Editor。后来经过讨论定为Model、View和Controller。作者自言“最难搞的就是给这些架构组件起名字”。

因为当时的软件环境跟现在有很大不同,所以经典MVC中的概念很难被现在的工程师理解。比如经典MVC中说:“view永远不应该知道用户输入,比如鼠标操作和按键。”对一个现代的软件工程师来说,这听上去相当不可思议:难道监听事件不需要类似这样的代码吗?

view.onclick = ......
但是想想在70年代末,80年代初,我们并没有操作系统和消息循环,甚至鼠标的光标都需要我们的UI系统来自行绘制,所以我们面对的应该是类似下面的局面:

mouse.onclick = ......
mouse.onmove = ......
当鼠标点击事件发生后,我们需要通过view的信息将点击事件派发到正确的view来处理。假如我们面对的是鼠标、键盘驱动这样的底层环境,我们就需要一定的机制和系统来统一处理用户输入并且分配给正确的view或者model来处理。这样也就不难理解为什么经典MVC中称"controller是用户和系统之间的链接"。

因为现在的多数环境和UI系统设计思路已经跟1979年完全不同,所以现代一些喜好生搬硬套的"MVC"实现者常常会认为controller的输入来自view,以至于画出model、view、controller之间很奇葩的依赖关系:

1.png

我们来看看Trygve Reenskaug自己画的图(这恶趣味的骷髅啊……):

2.gif

值得一提的是,其实MVC的论文中,还提到了"editor"这个概念。因为没有出现在标题中,所以editor声名不著。MVC论文中推荐controller想要根据输入修改view时,从view中获取一个叫做editor的临时对象,它也是一种特殊的controller,它会完成对view和view相关的model的修改操作。

1.2、控件系统

MVC是一种非常有价值的架构思路,然而时代在变迁,随着以windows系为代表的WIMP(window、icon、menu、pointer)风格的应用逐渐成为主流,人们发现,view和controller某些部件之间的局部性实际上强于controller内部的局部性。于是一种叫做控件(control)的预制组件开始出现了。

控件本身带有一定的交互功能,从MVC的视角来看,它既包含view,又包含controller,并且它通过"属性",来把用户输入暴露给model。

controller的输入分配功能,则被操作系统提供的各种机制取代:

指针系统:少数DOS时代过来的程序员应该记得,20年前的程序中的“鼠标箭头”实际上是由各个应用自己绘制的,以MVC的视角来看,这应当属于一个"PointerView"的职责范畴。但是20世纪以后,这样的工作基本由操作系统的底层UI系统来实现了。

文本系统:今天我们几乎不需要再去关心文本编辑、选中、拖拽等逻辑,对web程序员可以尝试自己用canvas写一个文本编辑框来体验一下上个时代程序员编写程序的感受。你会发现,选中、插入/覆盖模式切换、换行、退格、双击、拖拽等逻辑异常复杂,经典MVC模式中通常使用TextView和TextEditor配合来完成这样的工作,但是今天几乎找不到需要我们自己处理这些逻辑的场景。

焦点系统:焦点系统通过响应鼠标、tab键等消息来使得控件获得操作系统级唯一的焦点状态,所有的键盘事件通常仅仅会由拥有焦点的控件来响应。在没有焦点系统的时代,操作系统通常是单任务的,但是即使是单一应用,仍然要自己管理多个controller之间的优先权和覆盖逻辑,焦点系统不但从技术上,也从交互设计的角度规范化了UI的输入响应,而最妙的是,焦点系统是对视觉障碍人士友好的,现在颇多盲人用读屏软件都是强依赖焦点系统的。

所以时至今日,MVC,尤其是其中controller的功能已经意义不大,若是在控件系统中,再令所有用户输入流经一个controller则可谓不伦不类、本末倒置。MVVM的提出者,微软架构师John Gossman曾言:“我倾向于认为它(指controller)只是隐藏到后台了,它仍然存在,但是我们不需要像是1979年那样考虑那么多事情了”

1.3、MVC - 它原来的样子

3.png

在这种架构下,View 是无状态的,在Model变化的时候它只是简单的被 Controller重绘,尽管这种架构可以在应用里面实现,但是由于 MVC 的三种实体被紧密耦合着,每一种实体都和其他两种有着联系,所以即便是实现了也没有什么意义。

1.4、理想化的MVC

4.png

View 和 Model 之间是相互独立的,它们只通过 Controller 来相互联系。有点恼人的是 Controller 是重用性最差的,因为我们一般不会把冗杂的业务逻辑放在 Model 里面,那就只能放在 Controller 里了。

这个理想的MVC模式也是最多人所说的MVC模式

理论上看这么做貌似挺简单的,但是你有没有觉得有点不对劲?你甚至听过有人把 MVC 叫做重控制器模式。另外关于 ViewController 瘦身 已经成为 iOS 开发者们热议的话题了。

1.5、现实中的MVC

5.png

Cocoa MVC 鼓励你去写重控制器是因为 View 的整个生命周期都需要它去管理,Controller 和 View 很难做到相互独立。虽然你可以把控制器里的一些业务逻辑和数据转换的工作交给 Model,但是你再想把负担往 View 里面分摊的时候就没办法了;

因为 View 的主要职责就只是讲用户的操作行为交给 Controller 去处理而已。于是 ViewController 最终就变成了所有东西的代理和数据源,甚至还负责网络请求的发起和取消。

比如cell的使用

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)

Cell 作为一个 View 直接用 Model 来完成了自身的配置,MVC 的原则被打破了,这种情况一直存在,而且还没人觉得有什么问题。如果你是严格遵循 MVC 的话,你应该是在 ViewController 里面去配置 Cell,而不是直接将 Model 丢给 Cell,当然这样会让你的 ViewController 更重。

Cocoa MVC 被戏称为重控制器模式还是有原因的。Controller 测试起来很困难,因为它和 View 耦合的太厉害,要测试它的话就需要频繁的去 mock View 和 View 的生命周期;而且按照这种架构去写控制器代码的话,业务逻辑的代码也会因为视图布局代码的原因而变得很散乱。

在下面这种情况下你可以选择 Cocoa MVC:你并不想在架构上花费太多的时间,而且你觉得对于你的小项目来说,花费更高的维护成本只是浪费而已。如果你最看重的是开发速度,那么 Cocoa MVC 就是你最好的选择。

二、MVP架构

2.1、由来

1996年,Taligent公司的CTO,Mike Potel在一篇论文中提出Model-View-Presenter的概念。

在这个时期,主流的view的概念跟经典MVC中的那个“永远不应该知道用户输入”的view有了很大的差别,它通常指本文中所述的控件,此时在Mike眼中,输入已经是由view获得的了:

6.jpeg

Model-View-Presenter是在MVC的基础上,进一步规定了Controller中的一些概念而成的:

7.jpeg

对,所以,不论你按照Mike还是Trygve的理解方式,MVP和MVC的依赖关系图应该是一!模!一!样!的!因为Mike的论文里说了“we refer to this kind(指应用程序全局且使用interactor, command以及selection概念的) of controller as a presenter”。presenter它就是一种controller啊!

2.2、MVP流程

8.png

就像寒冬大神说的依赖关系图流程是一样的,这个流程看起来确实很像 Apple 的 理想化的MVC,它的名字是 MVP(被动变化的 View)。Apple 的 MVC 实际上是 MVP 吗?不是的,区别就是IOS中,苹果的理想MVC中UIView相当于View,UIController是Controller,而在MVP中,UIView和UIController都相当于View,所以在 Presenter 里面基本没什么布局相关的代码,它的职责只是通过数据和状态更新 View。

在 MVP 架构里面,UIViewController 的那些子类其实是属于 View 的,而不是 Presenter。这种区别提供了极好的可测性,但是这是用开发速度的代价换来的,因为你必须要手动的去创建数据和绑定事件

划分 - 我们把大部分的职责都分配到了 Presenter 和 Model 里面,而 View 基本上不需要做什么(在上面的例子里面,Model 也什么都没做)。

可测性 - 简直棒,我们可以通过 View 来测试大部分的业务逻辑。

易用 - 就我们上面那个简单的例子来讲,代码量差不多是 MVC 架构的两倍,但是 MVP 的思路还是蛮清晰的。

2.3、MVP另外的版本架构

9.png

MVP 架构拥有三个真正独立的分层,所以在组装的时候会有一些问题,而 MVP 也成了第一个披露了这种问题的架构。因为我们不想让 View 知道 Model 的信息,但是另外一种架构就包括了 View 和 Model 的直接绑定,与此同时 Presenter(Supervising Controller)仍然继续处理 View 上的用户操作,控制 View 的显示变化。

三、MVVM架构

3.1、由来

随着20世纪初web的崛起,HTML跟JS这样标记语言+程序语言的组合模式开始变得令人注目。逐渐推出的Flex、Sliverlight、QT、WPF、JSF、Cocoa等UI系统不约而同地选择了标记语言来描述界面。

在这样的架构中,view(或者说叫控件,不但是从依赖关系上跟程序的其他部件解耦,而且从语言上跟其它部分隔离开来。

标记语言的好处是,它可以由非专业的程序员产生,通过工具或者经过简单培训,一些设计师可以直接产生用标记语言描述的UI。想要突破这个限制使得view跟其它部分异常耦合可能性也更低。

然而这样的系统架构中,MVC和MVP模式已经不能很好地适用了。微软架构师John Gossman在WPF的XAML模式推出的同时,提出了MVVM的概念。

在MVVM模式中,数据绑定是最重要的概念,在MVC和MVP中的view和model的互相通讯,被以双向绑定的方式替代,这进一步把逻辑代码变成了声明模式。

理论上来说,Model - View - ViewModel 看起来非常棒。View 和 Model 我们已经都熟悉了,中间人的角色我们也熟悉了,但是在这里中间人的角色变成了 ViewModel。

它跟 MVP 很像:

  • MVVM 架构把 ViewController 看做 View。
  • View 和 Model 之间没有紧耦合
  • 另外,它还像 Supervising 版的 MVP 那样做了数据绑定,不过这次不是绑定 View 和 Model,而是绑定 View 和 ViewModel。

那么,iOS 里面的 ViewModel 到底是个什么东西呢?本质上来讲,他是独立于 UIKit 的, View 和 View 的状态的一个呈现(representation)。ViewModel 能主动调用对 Model 做更改,也能在 Model 更新的时候对自身进行调整,然后通过 View 和 ViewModel 之间的绑定,对 View 也进行对应的更新。

10.png

在ios中,MVC编码可能会成这样

12.png

但它并没有做太多事情来解决 iOS 应用中日益增长的重量级视图控制器的问题。在典型的 MVC 应用里,许多逻辑被放在 View Controller 里。它们中的一些确实属于 View Controller,但更多的是所谓的“表示逻辑(presentation logic)”,以 MVVM 属术语来说,就是那些将 Model 数据转换为 View 可以呈现的东西的事情,例如将一个 NSDate 转换为一个格式化过的 NSString。

我们的图解里缺少某些东西,那些使我们可以把所有表示逻辑放进去的东西。我们打算将其称为 “View Model” —— 它位于 View/Controller 与 Model 之间:

可以这么看

11.png

看起好多了!这个图解准确地描述了什么是 MVVM:一个 MVC 的增强版,我们正式连接了视图和控制器,并将表示逻辑从 Controller 移出放到一个新的对象里,即 View Model。MVVM 听起来很复杂,但它本质上就是一个精心优化的 MVC 架构,而 MVC 你早已熟悉。

现在我们知道了什么是 MVVM,但为什么我们会想要去使用它呢?在 iOS 上使用 MVVM 的动机,对我来说,无论如何,就是它能减少 View Controller 的复杂性并使得表示逻辑更易于测试。

此处有三个重点是我希望你看完本文能带走的:

  • MVVM 可以兼容你当下使用的 MVC 架构。
  • MVVM 增加你的应用的可测试性。
  • MVVM 配合一个绑定机制效果最好。

对于可变 Model,我们还需要使用一些绑定机制,这样 View Model 就能在背后的 Model 改变时更新自身的属性。此外,一旦 View Model 上的 Model 发生改变,那 View 的属性也需要更新。Model 的改变应该级联向下通过 View Model 进入 View。

在 OS X 上,我们可以使用 Cocoa 绑定,但在 iOS 上我们并没有这样好的配置可用。我们想到了 KVO(Key-Value Observation),而且它确实做了很伟大的工作。然而,对于一个简单的绑定都需要很大的样板代码,更不用说有许多属性需要绑定了。作为替代,可以使用ReactiveCocoa,但 MVVM 并未强制我们使用 ReactiveCocoa。

相对于 MVC 的历史来说,MVVM 是一个相当新的架构,MVVM 最早于 2005 年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出,并且应用在微软的软件开发中。当时 MVC 已经被提出了 20 多年了,可见两者出现的年代差别有多大。

MVVM 在使用当中,通常还会利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。所以,MVVM 模式有些时候又被称作:model-view-binder 模式。

具体在 iOS 中,可以使用 KVO 或 Notification 技术达到这种效果。

四、VIPER架构

13.png

VIPER 是我们最后一个要介绍的框架,这个框架比较有趣的是它不属于任何一种 MV(X) 框架。

到目前为止,你可能觉得我们把职责划分成三层,这个颗粒度已经很不错了吧。现在 VIPER 从另一个角度对职责进行了划分,这次划分了五层。

  • Interactor(交互器) - 包括数据(Entities)或者网络相关的业务逻辑。比如创建新的 entities 或者从服务器上获取数据;要实现这些功能,你可能会用到一些服务和管理(Services and Managers):这些可能会被误以为成是外部依赖东西,但是它们就是 VIPER 的 Interactor 模块。
  • Presenter(展示器) - 包括 UI(but UIKit independent)相关的业务逻辑,可以调用 Interactor 中的方法。
  • Entities(实体) - 纯粹的数据对象。不包括数据访问层,因为这是 Interactor 的职责。
  • Router(路由) - 负责 VIPER 模块之间的转场

实际上 VIPER 模块可以只是一个页面(screen),也可以是你应用里整个的用户使用流程(the whole user story)- 比如说「验证」这个功能,它可以只是一个页面,也可以是连续相关的一组页面。你的每个「乐高积木」想要有多大,都是你自己来决定的。

如果我们把 VIPER 和 MV(X) 系列做一个对比的话,我们会发现它们在职责划分上面有下面的一些区别:

Model(数据交互)的逻辑被转移到了 Interactor 里面,Entities 只是一个什么都不用做的数据结构体。

Controller/Presenter/ViewModel 的职责里面,只有 UI 的展示功能被转移到了 Presenter 里面。Presenter 不具备直接更改数据的能力。

VIPER 是第一个把导航的职责单独划分出来的架构模式,负责导航的就是 Router 层。

如何正确的使用导航(doing routing)对于 iOS 应用开发来说是一个挑战,MV(X) 系列的架构完全就没有意识到(所以也不用处理)这个问题。

我们再来评价下它在各方面的表现:

划分 - 毫无疑问的,VIPER 在职责划分方面是做的最好的。

可测性 - 理所当然的,职责划分的越好,测试起来就越容易

易用 - 最后,你可能已经猜到了,上面两点好处都是用维护性的代价换来的。一个小小的任务,可能就需要你为各种类写大量的接口。

五、总结

我们简单了解了几种架构模式,对于那些让你困惑的问题,我希望你已经找到了答案。但是毫无疑问,你应该已经意识到了,在选择架构模式这件问题上面,不存在什么 银色子弹,你需要做的就是具体情况具体分析,权衡利弊而已。

因此在同一个应用里面,即便有几种混合的架构模式也是很正常的一件事情。比如:开始的时候,你用的是 MVC 架构,后来你意识到有一个特殊的页面用 MVC 做的的话维护起来会相当的麻烦;这个时候你可以只针对这一个页面用 MVVM 模式去开发,对于之前那些用 MVC 就能正常工作的页面,你完全没有必要去重构它们,因为两种架构是完全可以和睦共存的。

从经典MVC到MVVM,UI架构经过数次重大变迁,一些概念也在不断变化,架构和底层环境互相影响、适配,我认为时至今日,经典MVC已经不再是UI架构的正常选项。

更糟糕的是,今天无数经过演绎的MVC实现(如backbone)和科普文,要么是原本作者概念已经很混乱,掺杂私货,要么为了适配现代的标记语言和控件模式,自己修改了经典MVC中的一些概念和耦合关系。实际上今天MVC已经没法作为一种交流的标准词汇了。

写此文,希望大家能了解些历史上的发展历程,莫被不严谨的文章误导。其实本文的相当多观点也是经过演绎的,所以我附上所有原始文献链接,希望大家看了以后能有自己的判断:)也欢迎大家据此指出我理解的错误之处。

六、参考文章

Last modification:March 1st, 2018 at 06:12 pm
如果看了这个文章可以让你少加会班,可以请我喝杯可乐
已打赏名单
微信公众号

4 comments

  1. 马裕博

    实践了一下VIPER,感觉非常好,这才是iOS应该有的架构,完美解决了所有遇到的问题

    1. 东东
      @马裕博

      牛逼了我的哥

  2. 大袋鼠

    搞Web的话现在流行完全的前后端分离,MVC大概也就不需要V层了~

    1. 东东
      @大袋鼠

      web端可以和app端的设计不太一样,昨天主要就闲着看了下架构的发展,有的博客间内容之间有冲突,昨天终于找了几篇文章找了完整比较正确的,具体用什么还是看哪个方便,现在app的话貌似更加推崇的是这个mvvm,再看

Leave a Comment