原文:https://addyosmani.com/resources/essentialjsdesignpatterns/book/#detailmvp

model-view-presenter(MVP)派生自 MVC 设计模式,它专注改善表示层逻辑。它起源于 1990 年代初一家名为 Taligent 公司,当时他们正在开发 C++ CommonPoint 环境的模型。尽管 MVC 和 MVP 的都是针对分离多个组件之间的关注点,但是它们之间有些本质上的区别。

因为本文是大概的介绍 MVP,我们将重点介绍最适合于基于 Web 架构的 MVP 版本。

Models, Views & Presenters

MVP 中的 P 代表 presenter。它是一个包含视图中的用户界面相关的业务逻辑的组件。与 MVC 不同的是,视图中的调用被委托给了 presenter,它们与视图是解耦的,并通过接口进行对话。这就使得我们可以做各种有用的事情,如能在单元测试中模拟视图。

MVP 的各个实现中最大的共同点就是使用了 Passive View(a view which is for all intents and purposes “dumb”),几乎没有逻辑。如果 MVC 与 MVP 不同,那是因为 C 和 P 做的事情不同。在 MVP 中,P 观察着模型,并在模型变更时更新视图。P 有效地将模型绑定到视图上,这个职责之前是由 MVC 中的控制器来承担。

由视图请求,presenters 处理任何与用户请求相关的工作,并把数据传递回视图。在这方面,它们检索并处理数据,并决定数据应该何时在视图中展示。在某些实现中,presenter 还要与服务层进行交互以保持数据持久化(模型)。

模型可能会触发事件,但是是由 presenters 来承担监听它们,以便它们对应的更新视图。在这种 passive 的架构中,我们没有直接数据绑定概念。视图开放 setter 给 presenter,以便它能用来更新视图的数据。

这个与 MVC 的区别带来的好处是提升我们应用的可测试性,并在视图和模型间建立一个清晰的隔离。然而这样做并非是没有成本的,因为这个模式不支持数据绑定,所以必须另外单独地来做这项工作。

尽管 Passive View 的一个常见实现是让视图来提供一个接口,但仍然有别的方式来做,包括使用事件,它略微减轻视图和 presenter 之间的耦合。因为在 JavaScript 不支持构造接口,我们在这里使用的更多的是协议而不是显示的接口。技术上讲它仍是一个 API,从这个角度来说,我们称它是接口是公平的。

MVP 还有一个 Supervising Controller 的变体,它更接近 MVC 和 MVVM 模式,因为它支持视图与模型直接的数据绑定。键-值观察(KVO)插件(比如 Derick Bailey 的 Backbone.ModelBinding 插件)倾向于将 Backbone 从 Passive View 转变成 Supervisor Controller 或者 MVVM 变体。

MVP 还是 MVC?

MVP 大多是用在企业级应用中,在这个级别的应用中通常会尽可能的复用表示层逻辑。在含有复杂视图和大量用户交互的应用中会发现使用 MVC 不太符合要求,因为要解决这个问题就意味着需要多个控制器。在 MVP 中,所有的复杂逻辑可以封装到 presenter 中,从而大大的简化了维护工作。

因为 MVP 的视图是通过接口来定义,技术上这个接口是系统和视图(除了 presenter 之外)之间唯一的联系点,这个模式还让开发者在不需要等待设计提供应用布局和界面图的情况下进行开发。

由于实现的差异,MVP 可能比 MVC 更容易进行自动化测试。原因是 presenter 可以当作用户界面的完整 mock 来使用,这样它就可以独立于其他组件进行单元测试。以我的经验,这实际取决于我们实现 MVP 的语言(选择 JavaScript 实现的 MVP 的项目与用 ASP.net 实现的项目差异很大)。

最后,考虑到它们主要是语义上的差异,MVC 中的潜在的问题也在 MVP 中也有。主要我们将关注点清晰的分离到模型、视图和控制器(或者 presenter),无论我们选择哪种变体,我们都应该获得几乎相同的收益。

MVC, MVP 和 Backbone.js

很少有(如果有的话) JavaScript 架构框架宣称自己是以经典的形式实现 MVC 或者 MVP 模式,因为 JavaScript 开发者并不认为 MVC 和 MVP 是互斥的(实际上,使用 ASP.net 或者 GWT 之类的 web 框架是严格的实现 MVP)。这是因为即使在我们的应用程序中可能有其他的 presenter/视图的逻辑,但它仍然被认为是 MVC 的一种。

Backbone 的开发者 Irene Ros (来自 Boston 的 Bocoup)赞成这种思维方式,因为当她将所有的视图分为各自不同的组件时,她需要一些东西才能将它们真正的组合起来。它可以是控制器路由(如 Backbone.Router ,本书随后将会讲到)或者响应与数据获取的回调。

也就是说,一些开发者确实觉得 Backbone.js 与 MVP 的描述更加匹配,而不是与 MVC。他们的观点是:

  • MVP 的 presenter 比控制器更好的描述了 Backbone.View (视图模板和绑定到它的数据之间的层)
  • 模型匹配 Backbone.Model (它与 MVC 中的模型没有太大的差异)
  • 视图最接近模板(如 HandleBars/Mustache 之类的标记模板)

对此的回应是视图可以单纯是一个视图(像 MVC 一样),因为 Backbone 足够灵活,可以用于多种用途。 MVC 中的 V 和 MVP 中的 P 可以同时通过 Backbone.View 来实现,因为它们是用来实现两个目标:渲染原子组件和组装由其他视图渲染的组件。

我们已经知道了 Backbone 中控制器的职责是由 Backbone.View 和 Backbone.Router 共同承担,在下面的例子中可以得到验证。

在我们 Backbone 应用 PhotoView 中,我们在 this.model.bind("change", ...) 这行代码中使用观察者模式来 ”订阅“ 视图对应模型的变更。它同时还处理 render() 方法中的模板化工作,但是与其他实现不同的是,用户交互同样也可以放在视图中处理的(见 events )。

  1. var PhotoView = Backbone.View.extend({
  2. //... 这里一个列表的标签。
  3. tagName: 'li',
  4. // 通过 template 方法传递照片模板的内容,
  5. // 并为单张照片缓存它
  6. template: _.template($('#photo-template').html()),
  7. // 具体到某个元素的 DOM 事件
  8. events: {
  9. 'click img': 'toggleViewed'
  10. },
  11. // PhotoView 监听它模型的变更,并重新渲染。
  12. // 因为在这个应用中,Photo 和 PhotoView 是一对一的通信,
  13. // 为了方便起见,我们直接引用这个模型。
  14. initialize: function() {
  15. this.model.on('change', this.render, this);
  16. this.model.on('destroy', this.remove, this);
  17. },
  18. // 重新渲染照片条目
  19. render: function() {
  20. $(this.el).html(this.template(this.model.toJSON()));
  21. return this;
  22. },
  23. // 切换模型中 "viewed" 的状态
  24. toggleViewed: function() {
  25. this.model.viewed();
  26. }
  27. });

另一个观点(差异有点大)是认为 Backbone 更接近 Smalltalk-80 MVC,我们前文已经介绍过它了。

正如 Backbone 博主 Derick Bailey 早先说的那样,最好不要强制将 Backbone 划归到某个具体的设计规范。设计模式应该是被当作如何架构应用的灵活的指南,在这方面上,Backbone 既不是 MVC 也不是 MVP。相反,它是吸取了多个架构模式的优点而创造出的一个新的好用的灵活的框架。

但是,了解这些概念的起源和成因是值得的,所以我希望我关于 MVC 和 MVP 的讲解对大家能有帮助。把它称为 Backbone 的方式 ,MV* 或者任何有助于参考其应用架构风格的东西。大多数 JavaScript 架构框架都会有意或者无意的采用它们自己的经典模式,但重要的是它们可以帮助我们开发有组织、整洁和易维护的应用程序。