浅析 MVC, MVP 与 MVVM之间的异同

创建于2015.02.03
编辑于2015.06.15


背景

之前在重构组内项目的js代码时,看了许多MVC、MVP、MVVM相关的文章,和各种“MVC”框架写成的todo程序,发现大家对MVC的理解存在分歧,许多框架(包括BackoneAngular等)其实只能算是MV*,而并非MVC,MVP或者MVVM中的任何一种。

在读了Martin Fowler(最先提出MVVM的人)写的一系列有关Presentation Patterns的文章以后,我已经不再去纠结那些框架到底是MVC、MVP还是MVVM了。这些框架只是工具而已,具体的模式在我们的心中。

由于大家对MVC、MVP甚至MVVM有广泛的误解,所以我们并没有这三个模式的完整详细定义,只能尽量去找权威的文章来学习。MVC在很早之前就出现了,但MVP和MVVM的思想应该是Martin Flow提出的,可以通过这篇综述性的文章去详细了解各种GUI架构。

本文主要从程序逻辑的组织以及程序数据流这两个角度来分析这三个模式的异同。

三者之中,我觉得MVP是最简单的模式,所以我会首先分析MVP,然后才是MVC和MVVM。

MVP

MVP被分为两类:Supervising ControllerPassive View。其中Passive View最简单,而Supervising Controller引入了数据绑定略显复杂。这里我们主要对Passive View进行分析。

Passive View的结构图

图片

模块依赖关系

如上图所示,程序被分为View,Presenter和Model三个模块:

  • Model,对外暴露函数调用接口和事件监听接口,对Presenter和View的存在一无所知,所以不依赖Presenter和View。
  • View,对外暴露函数调用接口和事件监听接口,对Presenter和Model的存在一无所知,所以不依赖Presenter和View。
  • Presenter,不对外暴露任何接口,监听View和model的事件,并对它们的接口了如指掌,所以依赖于Model和View。

数据流回路

View、Presenter和Model三个模块间的数据流形成了一个回路:

  1. 用户与View交互,触发用户事件;
  2. 事件被Presenter模块监听到;
  3. 然后Presenter根据不同的用户事件,调用Model层相应的接口;
  4. 接口的调用执行,导致Model层数据的变化,进而触发相应的数据改变事件;
  5. 事件又被Presenter模块监听到;
  6. 最后Presenter调用View层接口,改变View的状态,完成了这次对用户事件的响应。

    逻辑分类

    根据数据流动,可以将程序逻辑分为四类:
  • UI逻辑:存在于View中,各种UI控件的动态效果等都是由UI逻辑控制。
  • 响应逻辑:存在于Presenter中,当用户事件到来时,由响应逻辑来决定程序接下来如何做,比如转换View层传来的数据为Model需要的格式,然后再调用Model层对应的业务逻辑。响应逻辑需要涉及到Model层,这是其与UI逻辑的根本区别。
  • 业务逻辑:存在于Model中,往往与某一个领域的知识相关,或者直白点说,业务逻辑指的是一个应用中与展示无关的逻辑,这是它与其他三种逻辑的根本区别。Model模块向Presenter暴露业务逻辑的调用接口。
  • 展示逻辑:存在于Presenter中,当Model中的数据或状态发生改变时,由展示逻辑来决定如何在View中将这些变化表现出来(通过调用View的接口)。

    逻辑分层

    根据Separated Presentation的思想,我们可以知道Model属于领域层(Domain Layer),而View和Presenter属于展示层(Presentation Layer)。因此,相应逻辑有如下的归属关系:

  • Presentation Layer:UI逻辑,响应逻辑,展示逻辑

  • Domain Layer:业务逻辑

    小结

    我们可以看到,Passive View模式的优点有:

  • 极大地增强了可测性,除了View层的UI逻辑比较难测外,处于Presenter模块和Model模块的逻辑都可以方便地写单测。

  • 程序划分简单,逻辑清晰,可读性强,方便调试。

    MVC

    这里的MVC指的是经典的MVC模式,可以到这里了解。

    MVC的结构图

    图片

经典MVC模式最早出现在桌面端,Controller可以直接获取到用户事件。但Web前端应用却有所不同,如图所示,我们的Controller一般是从View那里监听用户事件。

模块依赖关系

  • Model,对外暴露函数调用接口和事件监听接口,不依赖Controller和View。
  • View,对外暴露函用户事件监听的接口,并监听Model的数据改变事件,依赖于Model。
  • Controller,不对外暴露任何接口,监听View的用户事件,并对Model的接口了如指掌,依赖于Model和View。

    数据流回路

    有了MVP的经验,我们可以看到View、Controller和Model三个模块间也有一个类似的的数据流回路:
  1. 用户与View交互,触发用户事件;
  2. 事件被Controller模块监听到;
  3. 然后Controller根据不同的用户事件,调用Model层相应的接口;
  4. 接口的调用执行,导致Model层数据的变化,进而触发相应的数据改变事件;
  5. 事件又被View模块监听到;
  6. 最后View根据新的数据改变自己的状态,完成了这次对用户事件的响应。

小结

我们可以看到,与MVP比起来,MVC的不同之处主要在于:展示逻辑被写到了View中,所以View需要直接去监听Model中的事件。
这样做的优点就是简单直接,开发速度快。但这样做的缺点也是明显的:展示逻辑与UI逻辑混杂在一起,使View太臃肿,不便于测试。

MVVM

Martin Flow提出的模式Presentation Model,就是后来广为流传的MVVM

Presentation Model这个模式是在MVP之后提出来的,主要解决了MVP模式中的一个问题:当应用变复杂以后,View需要向Presenter提供大量的接口,供展示逻辑调用,导致开发效率比较低

主要思想其实也很简单:在ViewModel中构建一组状态数据(state data),作为View状态的抽象。然后通过双向数据绑定(data binding)使ViewModel中的状态数据(state data)与View中的显示状态(screen state)保持一致。这样,ViewModel中的展示逻辑只需要修改对应的状态数据,就可以控制View的状态,从而避免在View上开发大量的接口。

MVVM的结构图

图片

模块依赖关系

  • Model,对外暴露函数调用接口和事件监听接口,不依赖ViewModel和View。
  • ViewModel,监听Model的事件,并对Model的接口了如指掌,依赖于Model。同时向View暴露响应逻辑的调用接口,以及所有的状态数据,并不依赖于View。
  • View,监听用户交互事件,然后调用ViewModel的响应逻辑,同时将自己的显示状态与ViewModel的状态数据绑定在一起,所以依赖于ViewModel。

数据流回路

  1. 用户与View交互,触发用户事件;
  2. View根据事件类型调用ViewModel中对应的响应逻辑;
  3. 然后ViewModel中的响应逻辑在做完适当处理后,会去调用Model层的接口;
  4. 接口的调用执行,导致Model层数据的变化,进而触发相应的数据改变事件;
  5. 事件又被ViewModel模块监听到;
  6. 拿到新的Model数据,ViewModel中的展示逻辑会去修改ViewModel中的状态数据;
  7. ViewModel中状态数据的改变,最终引起View的状态变换,完成了这次对用户事件的响应。

另一条特殊数据流回路

因为在ViewModel和View模块之间采用了双向数据绑定,所以在MVVM模式中还有一条特殊的数据流回路,见下图:

图片

  1. 用户与View交互,导致View的显示状态发生改变(如某个Input的值被修改);
  2. View的显示状态发生变化,由于双向数据绑定,导致ViewModel中的状态数据发生变化;
  3. 如上图中的name属性发生变化,导致另一个复合属性(值为”Hello, “ + name + “!”)的值也发生了变化;
  4. 然后这个复合属性的变化再次被传递到View,更新View的状态,从而完成这次用户事件响应。

小结

通过在ViewModel中引入状态数据,并通过双向数据绑定将其与View关联起来,带来的优点有:

  • 减少View层接口的开发量,使ViewModel中的展示逻辑的开发更简单;
  • 去掉ViewModel对View的依赖,使ViewModel的测试更简单,不用再去创建View的Mock对象。

ViewModel模式的缺点有:

  • 进一步复杂化了程序的数据流;
  • 当引入双向数据绑定以后,我们发现某些用户事件可以通过监听状态数据的变化去发现。这样容易诱导我们直接去监听状态数据的变化,而写一些响应逻辑。
    • 这样的做法是有问题的:不要忘记状态数据的改变还有另一种情况,Model层数据变化触发了相应的展示逻辑而导致状态数据的更新。
    • 所以通过状态数据的改变监听到的不仅仅是用户事件,还可能是Model层的数据变化,所以在这里写响应逻辑做法会带来不可预知的bug。

因此,当页面的逻辑不是特别复杂时,建议还是采用MVP模式。当逻辑变复杂以后,我们可以轻松地将MVP模式重构到MVVM模式,只需要引入一个双向数据绑定的机制即可。

总结

MVC,MVP和MVVM的相同点:

  • 都采用了Separated Presentation,将程序分成了Domain Layer和Presentation Layer。
  • 都是富客户端应用程序的解决方案,都包含UI逻辑、响应逻辑、业务逻辑和展示逻辑这四类逻辑。

不同点:

  • 逻辑的组织方式略有不同,MVC将展示逻辑放到了View中。
  • 模块之间的依赖关系不同,但作为下层模块的Model,一定不会依赖上面展示层的任何模块。

逻辑的组织方式直接影响了逻辑的可测性,而一个模块对其他模块的依赖程度则决定了对这个模块测试的容易程度(显然对Model的测试是最容易的)。

相关文档

  1. Presentation Patterns
  2. Supervising Controller
  3. Passive View
  4. Presentation Model
  5. WPF Apps With The Model-View-ViewModel Design Pattern
  6. Organizing Presentations
  7. Separated Presentation
  8. GUI Architecture
  9. Model View Controller