浅谈 MVC、MVP 和 MVVM 架构模式

这是 MVX 系列的第四篇文章,在前面的文章中,我们先后介绍了 MVC 架构模式中的 Model、View 和 Controller 的现状,对比了其他平台中的设计,最后给出了作者理想中的结构。

而在这一篇文章中,作者会依次介绍 MVC、MVP 以及 MVVM 架构模式以及不同平台对它们的使用;虽然参考了诸多资料,不过文中观点难免掺入作者的主观意见,作者也希望文中的错误与不足之处能被各位读者指出。


前面的几篇文章中重点都是介绍 iOS 平台上的 Model、View 和 Controller 如何设计,而这篇文章会对目前 GUI 应用中的 MVC、MVP 和 MVVM 架构模式进行详细地介绍。

MVC

在整个 GUI 编程领域,MVC 已经拥有将近 50 年的历史了。早在几十年前,Smalltalk-76 就对 MVC 架构模式进行了实现,在随后的几十年历史中,MVC 产生了很多的变种,例如:HMVC、MVA、MVP、MVVM 和其它将 MVC 运用于其它不同领域的模式。

早期的 MVC

而本文的内容就是从 MVC 开始的,作为最出名并且应用最广泛的架构模式,MVC 并没有一个明确的定义,网上流传的 MVC 架构图也是形态各异,作者查阅了很多资料也没有办法确定到底什么样的架构图才是标准的 MVC 实现。

设计 MVC 的重要目的就是在人的心智模型与计算机的模型之间建立一个桥梁,而 MVC 能够解决这一问题并为用户提供直接看到信息和操作信息的功能

更早的概念类似 Model-View-Editor(MVE)这里就不再提及了,感兴趣的读者可以阅读这篇论文 Thing-Model-View-Editor 了解更多的信息。

混乱的 MVC 架构

作者相信,稍有编程经验的开发者就对 MVC 有所了解,至少也是听过 MVC 的名字。作者也一直都认为绝大多数人对于 MVC 理解的概念都一样,很多人对于 MVVM 的实现有很大争论,说遵循什么什么架构的是 MVVM,MVVM 有什么组件、没有什么组件,而对于 MVC 仿佛没有那么大的疑问,这其实却不然。

ASP.NET MVC

在最近的几个月,作者发现不同人对于 MVC 的理解有巨大的差异,这是 ASP.NET MVC Overview 一文中对于 MVC 模式描述的示意图。

图片中并没有对 Model、View 和 Controller 三者之间如何交互进行说明,有的也只是几个箭头。我们应该可以这么简单地理解:

  • 1、控制器负责管理视图和模型;
  • 2、视图负责展示模型中的内容;

由于文章没有明确对这三个箭头的含义进行解释,所以在这里也仅作推断,无法确认原作者的意思。

Spring MVC

与 ASP.NET 不同,Spring MVC 对于 MVC 架构模式的实现就更加复杂了,增加了一个用于分发请求、管理视图的 DispatchServlet:


在这里不再介绍 Spring MVC 对于 HTTP 请求的处理流程,我们对其中 Model、View 和 Controller 之间的关系进行简单的分析:

  • 1、通过 DispatchServlet 将控制器层和视图层完全解耦;
  • 2、视图层和模型层之间没有直接关系,只有间接关系,通过控制器对模型进行查询、返回给 DispatchServlet 后再传递至视图层;

虽然 Spring MVC 也声称自己遵循 MVC 架构模式,但是这里的 MVC 架构模式和 ASP.NET 中却有很大的不同。

iOS MVC

iOS 客户端中的 Cocoa Touch 自古以来就遵循 MVC 架构模式,不过 Cocoa Touch 中的 MVC 与 ASP.NET 和 Spring 中的 MVC 截然不同。

在 iOS 中,由于 UIViewController 类持有一个根视图 UIView,所以视图层与控制器层是紧密耦合在一起的,这也是 iOS 项目经常遇到视图控制器非常臃肿的重要原因之一。

Rails MVC

Rails 作为著名的 MVC 框架,视图层和模型层没有直接的耦合,而是通过控制器作为中间人对信息进行传递:

这种 MVC 的设计分离了视图层和模型层之间的耦合,作为承担数据存储功能的模型层,可以通过控制器同时为多种不同的视图提供数据:

MVC-in-Rails-with-different-view

控制器根据用户发出的 HTTP 请求,从模型中取出相同的数据,然后传给不同的视图以渲染出不同的结果。Rails 中的 MVC 架构模式能够很好地将用于展示的视图和用于存储数据的数据库进行分离,两者之间通过控制器解耦,能够实现同一数据库对应多种视图的架构。

维基百科中的 MVC

除了上述框架中的 MVC 架构模式,还有一些其它的书籍或者资料对于 MVC 也有着不同的解释,比如维基百科的 Model-view-controller 条目,该条目是我们在 Google 搜索 MVC 时能够出现的前几个条目,这也是维基百科中的架构图能够出现在这篇文章中的原因 —— 有着广泛的受众。

维基百科中对于 MVC 架构模式交互图的描述其实相比上面的图片还都是比较清晰的,这主要是因为它对架构图中的箭头进行了详细的说明,指出了这个关系到底表示什么。

  • 1、视图被用户看到;
  • 2、用户使用控制器;
  • 3、控制器操作模型;
  • 4、模型更新视图;

虽然说整个架构图的逻辑是可以说的通的,不过相比于前面的架构图总是感觉有一些奇怪,而在这幅图片中,视图和控制器之间是毫无关系的,这与前面见到的所有 MVC 架构模式都完全不同,作者也不清楚这幅图来源是什么、为什么这么画,放在这里也仅作参考。

『标准』的 MVC

到底什么才是标准的 MVC 这个问题,到现在作者也没有一个确切的答案;不过多个框架以及书籍对 MVC 的理解有一点是完全相同的,也就是它们都将整个应用分成 Model、View 和 Controller 三个部分,而这些组成部分其实也有着几乎相同的职责。

  • 视图:管理作为位图展示到屏幕上的图形和文字输出;
  • 控制器:翻译用户的输入并依照用户的输入操作模型和视图;
  • 模型:管理应用的行为和数据,响应数据请求(经常来自视图)和更新状态的指令(经常来自控制器);

上述内容出自 Applications Programming in Smalltalk-80: How to use Model-View-Controller (MVC) 一文。

作者所理解的真正 MVC 架构模式其实与 ASP.NET 中对于 MVC 的设计完全相同:

控制器负责对模型中的数据进行更新,而视图向模型中请求数据;当有用户的行为触发操作时,会有控制器更新模型,并通知视图进行更新,在这时视图向模型请求新的数据,而这就是作者所理解的标准 MVC 模式下,Model、View 和 Controller 之间的协作方式。

依赖关系

虽然我们对 MVC 中的各个模块的交互不是特别了解,但是三者之间的依赖关系却是非常明确的;在 MVC 中,模型层可以单独工作,而视图层和控制器层都依赖与模型层中的数据。

虽然如上图所示,视图和控制器之间没有相互依赖,不过因为视图和控制器之间的依赖并不常用,所以图中将视图和控制器之间的依赖省略了。

分离展示层

在 Martin Fowler 对于 Model-View-Controller 的描述中,MVC 最重要的概念就是分离展示层 Separated Presentation,如何在领域对象(Domain Object)和我们在屏幕上看到的 GUI 元素进行划分是 MVC 架构模式中最核心的问题。

GUI 应用程序由于其需要展示内容的特点,分为两个部分:一部分是用于展示内容的展示层(Presentation Layer),另一部分包含领域和数据逻辑的领域层(Domain Layer)。

展示层依赖于领域层中存储的数据,而领域层对于展示层一无所知,领域层其实也是 MVC 模式中的模型层,而展示层可以理解为 VC 部分。

MVC 最重要的目的并不是规定各个模块应该如何交互和联系,而是将原有的混乱的应用程序划分出合理的层级,把一团混乱的代码,按照展示层和领域层分成两个部分;在这时,领域层中的领域对象由于其自身特点不需要对展示层有任何了解,可以同时为不同的展示层工作。

观察者同步

除了分离展示层,MVC 还与观察者同步 Observer Synchronization 关系紧密。因为在 MVC 模式中,模型可以单独工作,同时它对使用模型中数据的视图和控制器一无所知,为了保持模型的独立性,我们需要一种机制,当模型发生改变时,能够同时更新多个视图和控制器的内容;在这时,就需要以观察者同步的方式解决这个问题。

我们将所有需要实时更新的组件注册成为模型的观察者,在模型的属性发生变化时,通过观察者模式推送给所有注册的观察者(视图和控制器)。

当多个视图共享相同的数据时,观察者同步是一个非常关键的模式,它能够在对这些视图不知情的前提下,同时通知多个视图;通过观察者模式,我们可以非常容易地创建一个依赖于同一模型的视图。

观察者同步或者说观察者模式的主要缺点就是:由于事件触发的隐式行为可能导致很难查找问题的来源并影响其解决,不过虽然它有着这样的缺点,但是观察者同步这一机制仍然成为 MVC 以及其衍生架构模式中非常重要的一部分。

占主导地位的控制器

MVC 架构模式的三个组成部分:Model、View 和 Controller 中最重要的就是控制器,它承担了整个架构中的大部分业务逻辑,同时在用户请求到达或者事件发生时都会首先通知控制器并由它来决定如何响应这次请求或者事件。

在 MVC 中,所有的用户请求都会首先交给控制器,再由控制器来决定如何响应用户的输入,无论是更新模型中的信息还是渲染相应的视图,都是通过控制器来决定的;也就是说,在 MVC 中,控制器占据主导地位,它决定用户的输入是如何被处理的。

被动的模型

在绝大多数的 MVC 架构模式中,模型都不会主动向视图或者控制器推送消息;模型都是被动的,它只存储整个应用中的数据,而信息的获取和更新都是由控制器来驱动的。

但是当模型中的数据发生变化时,却需要通过一些方式通知对应的视图进行更新,在这种情况下其实也不需要模型主动将数据变化的消息推送给视图;因为所有对于模型层的改变都是由用户的操作导致的,而用户的操作都是通过控制器来处理的,所以只需要在控制器改变模型时,将更新的信息发送给视图就可以了;当然,我们也可以通过观察者模式向未知的观察者发送通知,以保证状态在不同模块之间能够保持同步。
作为被动的模型层,它对于视图和控制器的存在并不知情,只是向外部提供接口并响应视图和控制器对于数据的请求和更新操作。

MVC + MVC

目前的大多数应用程序都非常复杂并且同时包含客户端和服务端,两者分开部署但同时又都遵循 MVC 或者衍生的架构模式;过去的 Web 应用由于并不复杂,前端和服务端往往都部署在同一台服务器上,比如说使用 erb 模板引擎的 Rails 或者使用 jsp 的 Java 等等;这时的 Web 应用都遵循 MVC 架构模式:

上图的 MVC 架构模式的通信方式与标准的 MVC 中不同,上图以 Rails 为例展示其中的 MVC 是如何工作的,其中的 HTML、CSS 和 Javascript 代码就是视图层,控制器负责视图的渲染并且操作模型,模型中包含部分业务逻辑并负责管理数据库。

过去的 Web 应用的非常简单,而现在的应用程序都非常复杂,而整个应用程序无论是 Web 还是客户端其实都包含两个部分,也就是前端/客户端和后端;先抛开后端不谈,无论是 Web 前端、iOS 还是 Android 都遵循 MVC 架构模式或者它的变种。

在实际情况下,单独的 iOS、Android 和 Web 应用往往不能单独工作,这些客户端应用需要与服务端一起工作;当前端/客户端与后端一同工作时,其实分别『部署』了两个不同的应用,这两个应用都遵循 MVC 架构模式:

客户端和服务器通过网络进行连接,并组成了一个更大的 MVC 架构;从这个角度来看,服务端的模型层才存储了真正的数据,而客户端的模型层只不过是一个存储在客户端设备中的本地缓存和临时数据的集合;同理,服务端的视图层也不是整个应用的视图层,用于为用户展示数据的视图层位于客户端,也就是整个架构的最顶部;中间的五个部分,也就是从低端的模型层到最上面的视图共同组成了整个应用的控制器,将模型中的数据以合理的方式传递给最上层的视图层用于展示。

MVP

MVP 架构模式是 MVC 的一个变种,很多框架都自称遵循 MVC 架构模式,但是它们实际上却实现了 MVP 模式;MVC 与 MVP 之间的区别其实并不明显,作者认为两者之间最大的区别就是 MVP 中使用 Presenter 对视图和模型进行了解耦,它们彼此都对对方一无所知,沟通都通过 Presenter 进行。

MVP 作为一个比较有争议的架构模式,在维基百科的 Model-view-presenter 词条中被描述为 MVC 设计模式的变种(derivation),自上个世纪 90 年代出现在 IBM 之后,随着不断的演化,虽然有着很多分支,不过 Martin Fowler 对 MVP 架构模式的定义最终被广泛接受和讨论。

在 MVP 中,Presenter 可以理解为松散的控制器,其中包含了视图的 UI 业务逻辑,所有从视图发出的事件,都会通过代理给 Presenter 进行处理;同时,Presenter 也通过视图暴露的接口与其进行通信。

目前常见的 MVP 架构模式其实都是它的变种:Passive ViewSupervising Controller,接下来的内容也是围绕这两种变种进行展开的。

被动视图

MVP 的第一个主要变种就是被动视图(Passive View);顾名思义,在该变种的架构模式中,视图层是被动的,它本身不会改变自己的任何的状态,所有的状态都是通过 Presenter 来间接改变的。

被动的视图层就像前端中的 HTML 和 CSS 代码,只负责展示视图的结构和内容,本身不具有任何的逻辑:
```

top Created with Sketch.