原文:livoras/blog#11 作者: 戴嘉华
做客户端开发、前端开发对 MVC、MVP、MVVM 这些名词不了解也应该大致听过,都是为了解决图形界面应用程序复杂性管理问题而产生的应用架构模式。网上很多文章关于这方面的讨论比较杂乱,各种 MV模式之间的区别分不清,甚至有些描述都是错误的。本文追根溯源,从最经典的 Smalltalk-80 MVC 模式开始逐步还原图形界面之下最真实的 MV模式。
图形界面的应用程序提供给用户可视化的操作界面,这个界面提供给数据和信息。用户输入行为(键盘,鼠标等)会执行一些应用逻辑,应用逻辑(application logic)可能会触发一定的业务逻辑(business logic)对应用程序数据的变更,数据的变更自然需要用户界面的同步变更以提供最准确的信息。例如用户对一个电子表格重新排序的操作,应用程序需要响应用户操作,对数据进行排序,然后需要同步到界面上。
在开发应用程序的时候,以求更好的管理应用程序的复杂性,基于职责分离(Speration of Duties)的思想都会对应用程序进行分层。在开发图形界面应用程序的时候,会把管理用户界面的层次称为 View,应用程序的数据为 Model(注意这里的 Model 指的是 Domain Model,这个应用程序对需要解决的问题的数据抽象,不包含应用的状态,可以简单理解为对象)。Model 提供数据操作的接口,执行相应的业务逻辑。
有了 View 和 Model 的分层,那么问题就来了:View 如何同步 Model 的变更,View 和 Model 之间如何粘合在一起。
带着这个问题开始探索 MV模式,会发现这些模式之间的差异可以归纳为对这个问题处理的方式的不同。而几乎所有的 MV模式都是经典的 Smalltalk-80 MVC 的修改版。
早在上个世纪 70 年代,美国的施乐公司(Xerox)的工程师研发了 Smalltalk 编程语言,并且开始用它编写图形界面的应用程序。而在 Smalltalk-80 这个版本的时候,一位叫 Trygve Reenskaug 的工程师设计了 MVC 图形应用程序的架构模式,极大地降低了图形应用程序的管理难度。而在四人帮(GoF)的设计模式当中并没有把 MVC 当做是设计模式,而仅仅是把它看成解决问题的一些类的集合。Smalltalk-80 MVC 和 GoF 描述的 MVC 是最经典的 MVC 模式。
MVC 出了把应用程序分成 View、Model 层,还额外的加了一个 Controller 层,它的职责为进行 Model 和 View 之间的协作(路由、输入预处理等)的应用逻辑(application logic);Model 进行处理业务逻辑。Model、View、Controller 三个层次的依赖关系如下:
Controller 和 View 都依赖 Model 层,Controller 和 View 可以互相依赖。在一些网上的资料 Controller 和 View 之间的依赖关系可能不一样,有些是单向依赖,有些是双向依赖,这个其实关系不大,后面会看到它们的依赖关系都是为了把处理用户行为触发的事件处理权交给 Controller。
用户的对 View 操作以后,View 捕获到这个操作,会把处理的权利交移给 Controller(Pass calls);Controller 会对来自 View 数据进行预处理、决定调用哪个 Model 的接口;然后由 Model 执行相关的业务逻辑;当 Model 变更了以后,会通过观察者模式(Observer Pattern)通知 View;View 通过观察者模式收到 Model 变更的消息以后,会向 Model 请求最新的数据,然后重新更新界面。如下图:
看似没有什么特别的地方,但是由几个需要特别关注的关键点:
- View 是把控制权交移给 Controller,Controller 执行应用程序相关的应用逻辑(对来自 View 数据进行预处理、决定调用哪个 Model 的接口等等)。
- Controller 操作 Model,Model 执行业务逻辑对数据进行处理。但不会直接操作 View,可以说它是对 View 无知的。
- View 和 Model 的同步消息是通过观察者模式进行,而同步操作是由 View 自己请求 Model 的数据然后对视图进行更新。
需要特别注意的是 MVC 模式的精髓在于第三点:Model 的更新是通过观察者模式告知 View 的,具体表现形式可以是 Pub/Sub 或者是触发 Events。而网上很多对于 MVC 的描述都没有强调这一点。通过观察者模式的好处就是:不同的 MVC 三角关系可能会有共同的 Model,一个 MVC 三角中的 Controller 操作了 Model 以后,两个 MVC 三角的 View 都会接受到通知,然后更新自己。保持了依赖同一块 Model 的不同 View 显示数据的实时性和准确性。我们每天都在用的观察者模式,在几十年前就已经被大神们整合到 MVC 的架构当中。
这里有一个 MVC 模式的 JavaScript Demo,实现了一个小的 TodoList 应用程序。经典的 Smalltalk-80 MVC 不需要任何框架支持就可以实现。目前 Web 前端框架当中只有一个号称是严格遵循 Smalltalk-80 MVC 模式的:maria.js。
优点:
- 把业务逻辑和展示逻辑分离,模块化程度高。且当应用逻辑需要变更的时候,不需要变更业务逻辑和展示逻辑,只需要 Controller 换成另外一个 Controller 就行了(Swappable Controller)。
- 观察者模式可以做到多视图同时更新。
缺点:
- Controller 测试困难。因为视图同步操作是由 View 自己执行,而 View 只能在有 UI 的环境下运行。在没有 UI 环境下对 Controller 进行单元测试的时候,应用逻辑正确性是无法验证的:Model 更新的时候,无法对 View 的更新操作进行断言。
- View 无法组件化。View 是强依赖特定的 Model 的,如果需要把这个 View 抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的 Domain Model 是不一样的
在 Web 服务端开发的时候也会接触到 MVC 模式,而这种 MVC 模式不能严格称为 MVC 模式。经典的 MVC 模式只是解决客户端图形界面应用程序的问题,而对服务端无效。服务端的 MVC 模式又自己特定的名字:MVC Model 2,或者叫 JSP Model 2,或者直接就是 Model 2 。Model 2 客户端服务端的交互模式如下:
服务端接收到来自客户端的请求,服务端通过路由规则把这个请求交由给特定的 Controller 进行处理,Controller 执行相应的应用逻辑,对 Model 进行操作,Model 执行业务逻辑以后;然后用数据去渲染特定的模版,返回给客户端。
因为 HTTP 协议是单工协议并且是无状态的,服务器无法直接给客户端推送数据。除非客户端再次发起请求,否则服务器端的 Model 的变更就无法告知客户端。所以可以看到经典的 Smalltalk-80 MVC 中 Model 通过观察者模式告知 View 更新这一环被无情地打破,不能称为严格的 MVC。
Model 2 模式最早在 1998 年应用在 JSP 应用程序当中,JSP Model 1 应用管理的混乱诱发了 JSP 参考了客户端 MVC 模式,催生了 Model 2。
后来这种模式几乎被应用在所有语言的 Web 开发框架当中。PHP 的 ThinkPHP,Python 的 Dijango、Flask,NodeJS 的 Express,Ruby 的 RoR,基本都采纳了这种模式。平常所讲的 MVC 基本是这种服务端的 MVC。
MVP 模式有两种:
- Passive View
- Supervising Controller
而大多数情况下讨论的都是 Passive View 模式。本文会对 PV 模式进行较为详细的介绍,而 SC 模式则简单提及。
MVP 模式是 MVC 模式的改良。在上个世纪 90 年代,IBM 旗下的子公司 Taligent 在用 C/C++开发一个叫 CommonPoint 的图形界面应用系统的时候提出来的。
MVP 模式把 MVC 模式中的 Controller 换成了 Presenter。MVP 层次之间的依赖关系如下:
MVP 打破了 View 原来对于 Model 的依赖,其余的依赖关系和 MVC 模式一致。
既然 View 对 Model 的依赖被打破了,那 View 如何同步 Model 的变更?看看 MVP 的调用关系:
和 MVC 模式一样,用户对 View 的操作都会从 View 交移给 Presenter。Presenter 会执行相应的应用程序逻辑,并且对 Model 进行相应的操作;而这时候 Model 执行完业务逻辑以后,也是通过观察者模式把自己变更的消息传递出去,但是是传给 Presenter 而不是 View。Presenter 获取到 Model 变更的消息以后,通过 View 提供的接口更新界面。
关键点:
- View 不再负责同步的逻辑,而是由 Presenter 负责。Presenter 中既有应用程序逻辑也有同步逻辑。
- View 需要提供操作界面的接口给 Presenter 进行调用。(关键)
对比在 MVC 中,Controller 是不能操作 View 的,View 也没有提供相应的接口;而在 MVP 当中,Presenter 可以操作 View,View 需要提供一组对界面操作的接口给 Presenter 进行调用;Model 仍然通过事件广播自己的变更,但由 Presenter 监听而不是 View。
MVP 模式,这里也提供一个用 JavaScript 编写的例子。
优点:
- 便于测试。Presenter 对 View 是通过接口进行,在对 Presenter 进行不依赖 UI 环境的单元测试的时候。可以通过 Mock 一个 View 对象,这个对象只需要实现了 View 的接口即可。然后依赖注入到 Presenter 中,单元测试的时候就可以完整的测试 Presenter 应用逻辑的正确性。这里根据上面的例子给出了 Presenter 的单元测试样例。
- View 可以进行组件化。在 MVP 当中,View 不依赖 Model。这样就可以让 View 从特定的业务场景中脱离出来,可以说 View 可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的 View 组件。
缺点:
- Presenter 中除了应用逻辑以外,还有大量的 View->Model,Model->View 的手动同步逻辑,造成 Presenter 比较笨重,维护起来会比较困难。
上面讲的是 MVP 的 Passive View 模式,该模式下 View 非常 Passive,它几乎什么都不知道,Presenter 让它干什么它就干什么。而 Supervising Controller 模式中,Presenter 会把一部分简单的同步逻辑交给 View 自己去做,Presenter 只负责比较复杂的、高层次的 UI 操作,所以可以把它看成一个 Supervising Controller。
Supervising Controller 模式下的依赖和调用关系:
因为 Supervising Controller 用得比较少,对它的讨论就到这里为止。
MVVM 可以看作是一种特殊的 MVP(Passive View)模式,或者说是对 MVP 模式的一种改良。
MVVM 模式最早是微软公司提出,并且了大量使用在.NET 的 WPF 和 Sliverlight 中。2005 年微软工程师 John Gossman 在自己的博客上首次公布了 MVVM 模式。
MVVM 代表的是 Model-View-ViewModel,这里需要解释一下什么是 ViewModel。ViewModel 的含义就是 "Model of View",视图的模型。它的含义包含了领域模型(Domain Model)和视图的状态(State)。 在图形界面应用程序当中,界面所提供的信息可能不仅仅包含应用程序的领域模型。还可能包含一些领域模型不包含的视图状态,例如电子表格程序上需要显示当前排序的状态是顺序的还是逆序的,而这是 Domain Model 所不包含的,但也是需要显示的信息。
可以简单把 ViewModel 理解为页面上所显示内容的数据抽象,和 Domain Model 不一样,ViewModel 更适合用来描述 View。
MVVM 的依赖关系和 MVP 依赖,只不过是把 P 换成了 VM。
MVVM 的调用关系和 MVP 一样。但是,在 ViewModel 当中会有一个叫 Binder,或者是 Data-binding engine 的东西。以前全部由 Presenter 负责的 View 和 Model 之间数据同步操作交由给 Binder 处理。你只需要在 View 的模版语法当中,指令式地声明 View 上的显示的内容是和 Model 的哪一块数据绑定的。当 ViewModel 对进行 Model 更新的时候,Binder 会自动把数据更新到 View 上去,当用户对 View 进行操作(例如表单输入),Binder 也会自动把数据更新到 Model 上去。这种方式称为:Two-way data-binding,双向数据绑定。可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。
也就是说,MVVM 把 View 和 Model 的同步逻辑自动化了。以前 Presenter 负责的 View 和 Model 同步不再手动地进行操作,而是交由框架所提供的 Binder 进行负责。只需要告诉 Binder,View 显示的数据对应的是 Model 哪一部分即可。
这里有一个 JavaScript MVVM 的例子,因为 MVVM 需要 Binder 引擎。所以例子中使用了一个 MVVM 的库:Vue.js。
优点:
- 提高可维护性。解决了 MVP 大量的手动 View 和 Model 同步的问题,提供双向绑定机制。提高了代码的可维护性。
- 简化测试。因为同步逻辑是交由 Binder 做的,View 跟着 Model 同时变更,所以只需要保证 Model 的正确性,View 就正确。大大减少了对 View 同步更新的测试。
缺点:
- 过于简单的图形界面不适用,或说牛刀杀鸡。
- 对于大型的图形应用程序,视图状态较多,ViewModel 的构建和维护的成本都会比较高。
- 数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的。
可以看到,从 MVC->MVP->MVVM,就像一个打怪升级的过程。后者解决了前者遗留的问题,把前者的缺点优化成了优点。同样的 Demo 功能,代码从最开始的一堆文件,优化成了最后只需要 20 几行代码就完成。MV*模式之间的区分还是蛮清晰的,希望可以给对这些模式理解比较模糊的同学带来一些参考和思路。