weixin_39664585
weixin_39664585
2021-01-09 01:04

界面之下:还原真实的MV*模式

UPDATE(2015-10-29):更新MV*关于业务逻辑的描述,此处感谢 指出错误

作者:戴嘉华

转载请注明出处并保留原文链接( https://github.com/livoras/blog/issues/11 )和作者信息。

目录:

  • 前言
  • MVC
  • MVC Model 2
  • MVP
  • Passive View
  • Supervising Controller
  • MVVM
  • 结语
  • References

前言

做客户端开发、前端开发对MVC、MVP、MVVM这些名词不了解也应该大致听过,都是为了解决图形界面应用程序复杂性管理问题而产生的应用架构模式。网上很多文章关于这方面的讨论比较杂乱,各种MV模式之间的区别分不清,甚至有些描述都是错误的。本文追根溯源,从最经典的Smalltalk-80 MVC模式开始逐步还原图形界面之下最真实的MV模式。

GUI程序所面临的问题

图形界面的应用程序提供给用户可视化的操作界面,这个界面提供给数据和信息。用户输入行为(键盘,鼠标等)会执行一些应用逻辑,应用逻辑(application logic)可能会触发一定的业务逻辑(business logic)对应用程序数据的变更,数据的变更自然需要用户界面的同步变更以提供最准确的信息。例如用户对一个电子表格重新排序的操作,应用程序需要响应用户操作,对数据进行排序,然后需要同步到界面上。

在开发应用程序的时候,以求更好的管理应用程序的复杂性,基于职责分离(Speration of Duties)的思想都会对应用程序进行分层。在开发图形界面应用程序的时候,会把管理用户界面的层次称为View,应用程序的数据为Model(注意这里的Model指的是Domain Model,这个应用程序对需要解决的问题的数据抽象,不包含应用的状态,可以简单理解为对象)。Model提供数据操作的接口,执行相应的业务逻辑。

gui

有了View和Model的分层,那么问题就来了:View如何同步Model的变更,View和Model之间如何粘合在一起。

带着这个问题开始探索MV模式,会发现这些模式之间的差异可以归纳为对这个问题处理的方式的不同。而几乎所有的MV模式都是经典的Smalltalk-80 MVC的修改版。

Smalltalk-80 MVC

历史背景

早在上个世纪70年代,美国的施乐公司(Xerox)的工程师研发了Smalltalk编程语言,并且开始用它编写图形界面的应用程序。而在Smalltalk-80这个版本的时候,一位叫Trygve Reenskaug的工程师设计了MVC图形应用程序的架构模式,极大地降低了图形应用程序的管理难度。而在四人帮(GoF)的设计模式当中并没有把MVC当做是设计模式,而仅仅是把它看成解决问题的一些类的集合。Smalltalk-80 MVC和GoF描述的MVC是最经典的MVC模式。

MVC的依赖关系

MVC出了把应用程序分成View、Model层,还额外的加了一个Controller层,它的职责为进行Model和View之间的协作(路由、输入预处理等)的应用逻辑(application logic);Model进行处理业务逻辑。Model、View、Controller三个层次的依赖关系如下:

mvc-dep

Controller和View都依赖Model层,Controller和View可以互相依赖。在一些网上的资料Controller和View之间的依赖关系可能不一样,有些是单向依赖,有些是双向依赖,这个其实关系不大,后面会看到它们的依赖关系都是为了把处理用户行为触发的事件处理权交给Controller。

MVC的调用关系

用户的对View操作以后,View捕获到这个操作,会把处理的权利交移给Controller(Pass calls);Controller会对来自View数据进行预处理、决定调用哪个Model的接口;然后由Model执行相关的业务逻辑;当Model变更了以后,会通过观察者模式(Observer Pattern)通知View;View通过观察者模式收到Model变更的消息以后,会向Model请求最新的数据,然后重新更新界面。如下图:

mvc-call

看似没有什么特别的地方,但是由几个需要特别关注的关键点: 1. View是把控制权交移给Controller,Controller执行应用程序相关的应用逻辑(对来自View数据进行预处理、决定调用哪个Model的接口等等)。 2. Controller操作Model,Model执行业务逻辑对数据进行处理。但不会直接操作View,可以说它是对View无知的。 3. 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

MVC的优缺点

优点: 1. 把业务逻辑和展示逻辑分离,模块化程度高。且当应用逻辑需要变更的时候,不需要变更业务逻辑和展示逻辑,只需要Controller换成另外一个Controller就行了(Swappable Controller)。 2. 观察者模式可以做到多视图同时更新。

缺点: 1. Controller测试困难。因为视图同步操作是由View自己执行,而View只能在有UI的环境下运行。在没有UI环境下对Controller进行单元测试的时候,应用逻辑正确性是无法验证的:Model更新的时候,无法对View的更新操作进行断言。 2. View无法组件化。View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model是不一样的

MVC Model 2

在Web服务端开发的时候也会接触到MVC模式,而这种MVC模式不能严格称为MVC模式。经典的MVC模式只是解决客户端图形界面应用程序的问题,而对服务端无效。服务端的MVC模式又自己特定的名字:MVC Model 2,或者叫JSP Model 2,或者直接就是Model 2 。Model 2客户端服务端的交互模式如下:

model2

服务端接收到来自客户端的请求,服务端通过路由规则把这个请求交由给特定的Controller进行处理,Controller执行相应的应用逻辑,对Model进行操作,Model执行业务逻辑以后;然后用数据去渲染特定的模版,返回给客户端。

因为HTTP协议是单工协议并且是无状态的,服务器无法直接给客户端推送数据。除非客户端再次发起请求,否则服务器端的Model的变更就无法告知客户端。所以可以看到经典的Smalltalk-80 MVC中Model通过观察者模式告知View更新这一环被无情地打破,不能称为严格的MVC。

Model 2模式最早在1998年应用在JSP应用程序当中,JSP Model 1应用管理的混乱诱发了JSP参考了客户端MVC模式,催生了Model 2。

jsp

后来这种模式几乎被应用在所有语言的Web开发框架当中。PHP的ThinkPHP,Python的Dijango、Flask,NodeJS的Express,Ruby的RoR,基本都采纳了这种模式。平常所讲的MVC基本是这种服务端的MVC。

MVP

MVP模式有两种: 1. Passive View 2. Supervising Controller

而大多数情况下讨论的都是Passive View模式。本文会对PV模式进行较为详细的介绍,而SC模式则简单提及。

历史背景

MVP模式是MVC模式的改良。在上个世纪90年代,IBM旗下的子公司Taligent在用C/C++开发一个叫CommonPoint的图形界面应用系统的时候提出来的。

MVP(Passive View)的依赖关系

MVP模式把MVC模式中的Controller换成了Presenter。MVP层次之间的依赖关系如下:

mvp-dep

MVP打破了View原来对于Model的依赖,其余的依赖关系和MVC模式一致。

MVP(Passive View)的调用关系

既然View对Model的依赖被打破了,那View如何同步Model的变更?看看MVP的调用关系:

mvp-call

和MVC模式一样,用户对View的操作都会从View交移给Presenter。Presenter会执行相应的应用程序逻辑,并且对Model进行相应的操作;而这时候Model执行完业务逻辑以后,也是通过观察者模式把自己变更的消息传递出去,但是是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面

关键点: 1. View不再负责同步的逻辑,而是由Presenter负责。Presenter中既有应用程序逻辑也有同步逻辑。 2. View需要提供操作界面的接口给Presenter进行调用。(关键)

对比在MVC中,Controller是不能操作View的,View也没有提供相应的接口;而在MVP当中,Presenter可以操作View,View需要提供一组对界面操作的接口给Presenter进行调用;Model仍然通过事件广播自己的变更,但由Presenter监听而不是View。

MVP模式,这里也提供一个用JavaScript编写的例子

MVP(Passive View)的优缺点

优点: 1. 便于测试。Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter应用逻辑的正确性。这里根据上面的例子给出了Presenter的单元测试样例。 2. View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的View组件。

缺点: 1. Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。

MVP(Supervising Controller)

上面讲的是MVP的Passive View模式,该模式下View非常Passive,它几乎什么都不知道,Presenter让它干什么它就干什么。而Supervising Controller模式中,Presenter会把一部分简单的同步逻辑交给View自己去做,Presenter只负责比较复杂的、高层次的UI操作,所以可以把它看成一个Supervising Controller。

Supervising Controller模式下的依赖和调用关系:

mvp-sc

因为Supervising Controller用得比较少,对它的讨论就到这里为止。

MVVM

MVVM可以看作是一种特殊的MVP(Passive View)模式,或者说是对MVP模式的一种改良。

历史背景

MVVM模式最早是微软公司提出,并且了大量使用在.NET的WPF和Sliverlight中。2005年微软工程师John Gossman在自己的博客上首次公布了MVVM模式。

ViewModel

MVVM代表的是Model-View-ViewModel,这里需要解释一下什么是ViewModel。ViewModel的含义就是 "Model of View",视图的模型。它的含义包含了领域模型(Domain Model)和视图的状态(State)。 在图形界面应用程序当中,界面所提供的信息可能不仅仅包含应用程序的领域模型。还可能包含一些领域模型不包含的视图状态,例如电子表格程序上需要显示当前排序的状态是顺序的还是逆序的,而这是Domain Model所不包含的,但也是需要显示的信息。

可以简单把ViewModel理解为页面上所显示内容的数据抽象,和Domain Model不一样,ViewModel更适合用来描述View。

MVVM的依赖

MVVM的依赖关系和MVP依赖,只不过是把P换成了VM。

mvvm-dep

MVVM的调用关系

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-call

也就是说,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交由框架所提供的Binder进行负责。只需要告诉Binder,View显示的数据对应的是Model哪一部分即可。

这里有一个JavaScript MVVM的例子,因为MVVM需要Binder引擎。所以例子中使用了一个MVVM的库:Vue.js

MVVM的优缺点

优点: 1. 提高可维护性。解决了MVP大量的手动View和Model同步的问题,提供双向绑定机制。提高了代码的可维护性。 2. 简化测试。因为同步逻辑是交由Binder做的,View跟着Model同时变更,所以只需要保证Model的正确性,View就正确。大大减少了对View同步更新的测试。

缺点: 1. 过于简单的图形界面不适用,或说牛刀杀鸡。 2. 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。 3. 数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。

结语

可以看到,从MVC->MVP->MVVM,就像一个打怪升级的过程。后者解决了前者遗留的问题,把前者的缺点优化成了优点。同样的Demo功能,代码从最开始的一堆文件,优化成了最后只需要20几行代码就完成。MV*模式之间的区分还是蛮清晰的,希望可以给对这些模式理解比较模糊的同学带来一些参考和思路。

References

小广告:欢迎follow关注个人github:https://github.com/livoras

该提问来源于开源项目:livoras/blog

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

56条回答

  • weixin_39776298 weixin_39776298 3月前

    赞👍

    点赞 评论 复制链接分享
  • weixin_39690963 weixin_39690963 3月前

    必须赞一个,向前辈学习和看齐~

    点赞 评论 复制链接分享
  • weixin_39831991 weixin_39831991 3月前

    受益匪浅!

    点赞 评论 复制链接分享
  • weixin_39792747 weixin_39792747 3月前

    点赞 评论 复制链接分享
  • weixin_39778582 weixin_39778582 3月前

    不错。其中 Model 2 的模式更多的是由C/S模式转变而来的吧,看起来象是把MVC硬套在C/S上

    +1

    点赞 评论 复制链接分享
  • weixin_39708854 weixin_39708854 3月前

    职责分离(Speration of Duties)单词写错了

    点赞 评论 复制链接分享
  • weixin_39800875 weixin_39800875 3月前

    请问 应用逻辑和业务逻辑与什么区别

    点赞 评论 复制链接分享
  • weixin_39894914 weixin_39894914 3月前

    膜大佬,向大佬学习

    点赞 评论 复制链接分享
  • weixin_39947521 weixin_39947521 3月前

    -Ma 缘妙不可言

    点赞 评论 复制链接分享
  • weixin_39685697 weixin_39685697 3月前

    点赞 评论 复制链接分享
  • weixin_39533795 weixin_39533795 3月前

    Dijango -> Django

    点赞 评论 复制链接分享
  • weixin_39927158 weixin_39927158 3月前

    值得多次阅读的文章,这次再看似乎又理解得深入了一点。

    点赞 评论 复制链接分享
  • weixin_39621794 weixin_39621794 3月前

    学习学习

    点赞 评论 复制链接分享
  • weixin_39758032 weixin_39758032 3月前

    好文, 看完之后终于明晰了一些,收藏了!

    点赞 评论 复制链接分享
  • weixin_39607090 weixin_39607090 3月前

    感谢大佬分享

    点赞 评论 复制链接分享
  • weixin_39620001 weixin_39620001 3月前

    学到了orz

    点赞 评论 复制链接分享
  • weixin_39678304 weixin_39678304 3月前

    大佬就是大佬4年前,就已经分析的这么透彻,其他文章各种原理分析,看了后还是相关概念还是比较模糊的,也分辨不出对与错,看了这篇后,能够具体的理解文章表达的深刻含义,舒服。orz

    点赞 评论 复制链接分享
  • weixin_39569112 weixin_39569112 3月前

    "View和Model的同步消息是通过观察者模式进行,而同步操作是由View自己请求Model的数据然后对视图进行更新" ---- 对这点不太理解,都使用观察者模式了,为何Model在广播消息的时候不直接把数据推送过去,而非要view自己去获取呢,这是不是有些多余了? 另外,“View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了”----个人理解view直接去订阅model的数据,对于接收到的数据,并不存在强依赖关系,有数据则展示,没有则不展示。 第二个问题其实是基于model会把数据广播给view产生的。 个人理解,欢迎斧正。

    点赞 评论 复制链接分享
  • weixin_39780260 weixin_39780260 3月前

    讲的真好,我还是喜欢MVC模式;

    点赞 评论 复制链接分享
  • weixin_39853590 weixin_39853590 3月前

    你画图看上去真不错,是什么工具呢?

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    PPT

    点赞 评论 复制链接分享
  • weixin_39905500 weixin_39905500 3月前

    赞! 好文章,感谢作者。。。

    点赞 评论 复制链接分享
  • weixin_39637370 weixin_39637370 3月前

    分析的不错

    点赞 评论 复制链接分享
  • weixin_39625258 weixin_39625258 3月前

    gulp运行出现错误:E:\jscode\test\MVW\MVW-demos-master\node_modules\gulp-browserify\node_modules\browserify\node_modules\browser-pack\node_modules\combine-source-map\node_modules\source-map\lib\source-map\array-set.js:83 throw new Error('No element indexed by ' + aIdx); ^ modules已经全部安装。我去看了那个array-set.js文件,83行如下: ArraySet.prototype.at = function ArraySet_at(aIdx) { if (aIdx >= 0 && aIdx < this._array.length) { return this._array[aIdx]; } throw new Error('No element indexed by ' + aIdx); }; 请告知该如何解决。

    点赞 评论 复制链接分享
  • weixin_39918690 weixin_39918690 3月前

    我个人认为,在形容MVC三者时,最后不要带例如“Model层”这样的说法,因为“层”是一种带有严格意义上的上下级关系的,例如J2EE中的Controller层、Service层、Dao层。MVC架构模式中的C也不单指的Controller层,指的是仅仅是一种结构的控制器。

    点赞 评论 复制链接分享
  • weixin_39621044 weixin_39621044 3月前

    赞!!!很清晰的讲述了MV*,谢谢~~

    点赞 评论 复制链接分享
  • weixin_39993322 weixin_39993322 3月前

    smalltalk-80 的 mvc 的表述似乎不太对,用户应该是通过 controller 交互的,而不是 view。这是 wikpedia 上的解释The third part, the controller, accepts input and converts it to commands for the model or view

    到了 mvp,截获用户交互的就变成 view 了,这是和 mvc 的区别。

    然后 mvc 移植到 web 上(model2)后,controller 的角色实际上变得非常像 mvp 中的 presenter(model 和 view 的中继者),但是我们并没有称为 web mvp,而是称为 web mvc,就是因为截获请求的是 controller,而不是 view。

    点赞 评论 复制链接分享
  • weixin_39993322 weixin_39993322 3月前

    不过仔细想下,在客户端中 view 和 controller 其实联系很紧密,说用户通过 view 交互,然后 view 将事件直接转发给 controller 处理应该也是对的。。

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    GUI的MVC,用户可以直接打交道的只有View了

    点赞 评论 复制链接分享
  • weixin_39807067 weixin_39807067 3月前

    这文章写的不错

    点赞 评论 复制链接分享
  • weixin_40006963 weixin_40006963 3月前

    和大神学习了

    点赞 评论 复制链接分享
  • weixin_39953673 weixin_39953673 3月前

    写的蛮不错的,思路清晰

    点赞 评论 复制链接分享
  • weixin_39814093 weixin_39814093 3月前

    谢谢大神分享,这是我看到过分析gui模式最好的文章

    点赞 评论 复制链接分享
  • weixin_39938312 weixin_39938312 3月前

    学习了,理解mv*的干货好文,谢谢大神!

    点赞 评论 复制链接分享
  • weixin_39922683 weixin_39922683 3月前

    好文

    点赞 评论 复制链接分享
  • weixin_39961943 weixin_39961943 3月前

    好文

    点赞 评论 复制链接分享
  • weixin_39630498 weixin_39630498 3月前

    mark

    点赞 评论 复制链接分享
  • weixin_39805924 weixin_39805924 3月前

    基于职责分离(Speration of Duties)的思想都会对应用程序进行分层。

    Speration of Duties-->Separation of Duties

    点赞 评论 复制链接分享
  • weixin_39847945 weixin_39847945 3月前

    不错。其中 Model 2 的模式更多的是由C/S模式转变而来的吧,看起来象是把MVC硬套在C/S上。

    点赞 评论 复制链接分享
  • weixin_39870155 weixin_39870155 3月前

    感谢嘉华大大分享干货 :)

    点赞 评论 复制链接分享
  • weixin_39700394 weixin_39700394 3月前

    点赞 评论 复制链接分享
  • weixin_39792803 weixin_39792803 3月前

    赞!

    点赞 评论 复制链接分享
  • weixin_39914243 weixin_39914243 3月前

    我看到的另一种关于MVC的说法是Controller并不执行业务逻辑,它只负责简单的参数验证和指定哪个Model进行业务逻辑的执行,Model则执行相应的业务逻辑

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    那完全没有必要分一个controller出来了,全部交给model做就好了

    点赞 评论 复制链接分享
  • weixin_39914243 weixin_39914243 3月前

    controller里面只是负责参数验证,安全检测,如果充斥着各种各样的业务逻辑,感觉不太好,Model负责和自身相关的业务逻辑,这样不是更清晰吗

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    问题是,你这个Model如果用在别的地方,就会把一堆无关的业务逻辑带过去

    点赞 评论 复制链接分享
  • weixin_39626237 weixin_39626237 3月前

    写的很清晰

    点赞 评论 复制链接分享
  • weixin_39759600 weixin_39759600 3月前

    赞!

    点赞 评论 复制链接分享
  • weixin_39754831 weixin_39754831 3月前

    awesome!

    点赞 评论 复制链接分享
  • weixin_39614874 weixin_39614874 3月前

    MVC主要的业务逻辑在V,MVP主要的业务逻辑在P

    点赞 评论 复制链接分享
  • weixin_39608680 weixin_39608680 3月前

    「Model层对应用程序的业务逻辑无知」如果这里的 Model 定义为 Domain Model,那他怎么会对应用程序的业务(Domain,领域)逻辑一无所知?这里的定义有出处吗?

    点赞 评论 复制链接分享
  • weixin_39608680 weixin_39608680 3月前

    把 Controller 的职责定义为协作(路由、输入预处理等)而不涉及到具体业务逻辑,感觉更清晰一些

    点赞 评论 复制链接分享
  • weixin_39664585 weixin_39664585 3月前

    你们是对的,感谢各位大神指正,文章持续修改更新中。

    点赞 评论 复制链接分享
  • weixin_39961943 weixin_39961943 3月前

    好文章,感谢作者,转载啦: https://linux.cn/article-6481-1.html

    点赞 评论 复制链接分享
  • weixin_39776787 weixin_39776787 3月前

    Model: 负责更新自己和给view提供数据, 当modle数据变化时通知给他的观察者们。

    View: 是Modle的观察者,当Modle数据变化时使用它的数据显示自己。View也封装了视图的细节(html/css/dom做操),通过Dom事件捕获界面上的用户行为,并且通知给他的观察者们。

    Controller:是View的观察者,View把需要Controller处理用户行为通知给Controller, Controller 调用modle上的方法更新数据。

    其中只有View了解视图的细节, Model和 Controller 对视图无感知。Model和View,Controller 和View 之间通过观察者模式交互。这样做的好处是,可以方便的对Conrtoller 和Model进行测试

    点赞 评论 复制链接分享
  • weixin_39764487 weixin_39764487 3月前

    赞!

    点赞 评论 复制链接分享

为你推荐