JavaScript - CodeProject 上的 MVC、MVP 和 MVVM 模式圣地里的肚皮舞






4.95/5 (64投票s)
让我们与我们性感的伙伴:JavaScript 一起,前往 MVC、MVP 和 MVVM 模式的圣地进行一次激动人心的旅行吧。
目录
词汇表
MVC:模型-视图-控制器
MVVM:模型-视图-视图模型
MVP:模型-视图-呈现器
前言
近年来,涌现了大量基于 MV*(MVC、MVVM、MVP) 模式的 JavaScript 框架,并且相当一部分开发人员倾向于这些编程模式。然而,关于客户端脚本使用 MV* 模式的优势是什么,这个问题仍然存在。在本文中,我将使用具体而简单的 JavaScript 代码以及对每种模式的完整阐述来回答这个问题。换句话说,本文将对 MV* 模式进行准确的鉴别诊断。阅读本文后,读者将能够了解 JavaScript 如何处理 MV* 模式以及如何区分该类别中的每种模式。
背景
作为一个多年来使用 MVC 框架的 MVC 模式的开发人员,我心中仍然有一个未解之谜:MVVM、MVP 和 MVC 之间有什么主要区别?我还想知道如何以及何时选择一种模式而不是另一种。这个问题一直没有解决,直到我去了 IBM 做了一次面试。虽然我表现得很好,但我无法回答一个重要的问题。你能猜到那是关于什么的吗?是的,那是关于 MVC、MVP、MVVM 模式的鉴别诊断。同时,我还需要为我的大学面向对象编程课程准备一篇论文。因此,我考虑写一篇关于 MV* 模式的文章,这样就可以一举两得。
因此,我选择了一篇关于 MV 模式的文章。在我阅读上述论文期间,我还查阅了其他关于 MV* 模式的论文。最后,我完成了演示文稿,尽管大多数学生在我的演示文稿期间都在睡觉,但我确信将来他们会认识到这个问题的重要性。我建议您慢慢地、一步一步地、反复地阅读这篇文章。最后,我希望您在阅读本文时不会睡着,并从中获得乐趣!
MV 模式:仅仅是客户端的时尚,还是有用的东西?!
在大多数软件开发团队(包括专业团队)中,JavaScript 仍然被视为是“粉饰”项目“屎”(sh*ts)或使 Web 应用程序“更漂亮”的绝佳解决方案。他们通常要求后端开发人员编写 JavaScript。这时,潜在的灾难便悄然诞生了!由于大多数开发人员从未认真对待 JavaScript,在写了一些 JavaScript 代码(例如:“document.getElementById”)后,他们就会感到厌烦。因此,他们决定使用 JavaScript 框架来简化工作(只需看看,当您可以使用“$()”而不是编写完整的“document.getElementById”时,该有多么令人愉悦!)。这是上述灾难得到滋养和壮大的第二个关键点。最终,编写 JavaScript 代码的过程将转变为“屎”(sh*t)的孵化过程。
随着时间的推移,这些灾难会随着使用更多框架而变得更加复杂。最终,开发团队将花费大量时间用于
- 调试由不同框架引起的 JavaScript 错误,而这些错误通常是混淆的
- 由于缺乏单元测试而规避问题
- 在框架丛林中适应各种框架
- 编辑不同框架强制执行的不同编码风格的混乱代码
- 添加框架未满足的一些特定需求。结果,新的编码风格将被“强加”到上述混乱的代码丛林中
- 将新代码与预先编写的代码相结合,这本身就可能成为新的 bug 的温床
因此,当我说:“在大型项目中,使用熟练的 JavaScript 程序员比使用使用优秀 JavaScript 框架的糟糕程序员更好。”是有道理的。您可能会问:“如果我将优秀的程序员与 JavaScript 框架一起使用呢?”答案取决于项目,但通常,熟练的 JavaScript 程序员会避免在企业 Web 应用程序中使用 JS 框架,除非他们非常谨慎地使用框架,并在有限和受控的编码环境中进行。专业的 JavaScript 程序员试图使他们的代码**可测试**、**可重用**和**可维护**,这些代码在不断变化的客户端环境中具有相当大的灵活性。为此,他们也利用编程模式。既然本文旨在详细介绍 MV* 模式,那么让我们来看看 MV 模式如何在可测试性、可重用性和可维护性方面影响代码。
可重用性
一个类被编写和调试后,就可以交给其他程序员在他们自己的程序中使用。这称为可重用性。您拥有开箱即用的关注点分离。在每种 MV* 模式中,您都有模型和视图。它们都有自己的职责——模型负责封装您的领域特定模型,另一方面,视图负责显示模型(最终处理事件,这取决于框架)。这意味着您的模型与其表示形式没有耦合,最终可以轻松地在不同的项目中重用它。
可测试性
这是一个松耦合的开发框架,因为它分为三个层。这种松耦合有助于降低 Web 应用程序的复杂性,并且易于维护,并提供了更好的测试驱动开发,这是 MV* 模式提高应用程序可测试性目标之一。
可维护性
MV* 模式本质上是模块化的,这使得在不产生不利影响的情况下,更容易修改(甚至重写)应用程序的某一部分。它们使得实现**关注点分离**非常容易,这是编写易于维护和重构的代码的关键原则。
因此,通过 JavaScript 使用 MV* 模式可以使代码易于理解、可维护且可测试。清晰易懂的代码是使用 MV 模式的另一个结果。通过在客户端脚本中进行关注点分离,可以防止出现代码混乱。在本文的其余部分,我将深入探讨 MV* 模式的细节。在此之前,我将讨论两种重要的同步方法,称为“观察者同步”和“流程同步”,MV* 模式在某种程度上就是建立在这些方法之上的。
同步方法
流程同步
流程同步基于顺序命令执行:例如,从文本框 A 读取用户输入,用方法 B 处理,并将结果写入文本标签 C。流程同步使用用户界面组件和域组件之间的直接调用。“本质上,每次您更改共享跨多个屏幕的状态时,您都会告诉每个屏幕自行更新。问题在于,这通常意味着每个屏幕都与应用程序中的其他屏幕在某种程度上耦合”(Martin Fowler)。对于具有相对简单用户界面的小型应用程序,流程同步会产生清晰易懂的代码。对于更复杂的程序和更复杂的用户界面,由于缺乏域和用户界面关注点的分离,该方法可能导致难以维护的代码。
“流程同步”是用于同步屏幕与域数据的“观察者同步”的替代方案。在许多方面,这是一种更明确、更直接的做法。而不是隐式的“观察者”关系,这些关系可能难以看到和调试,您将拥有明确的同步调用,清晰地布局在代码中。
“流程同步”的问题在于,一旦屏幕数量不受限制且可能共享数据,事情就会变得非常混乱。任何屏幕上的更改都需要触发其他屏幕进行更新。通过对其他屏幕进行显式调用来实现这一点,会使屏幕之间高度相互依赖。这就是“观察者同步”如此容易的地方”(Martin Fowler)。在下面的示例中,您可以看到流程同步方法是如何工作的。
function Load() {
bookName.setText(model.getBookName());
price.setText(model.getPrice());
Author.setText(model.getAuthor());
}
根据 Martin Fowler 的说法,实现流程同步的一种著名的代码风格是有一个用于整个应用程序的根窗口(它是应用程序的主窗口)。每次您想打开应用程序的某个部分时,都会打开一个子窗口。在子窗口中更改任何数据后,将在关闭子窗口时更新根窗口。
观察者同步
与基于直接和顺序调用的流程同步不同,观察者同步基于**发布-订阅**风格,其中有一个中央发布者发布事件,然后有一组订阅者订阅事件并开始监听发布者。一旦发布者发出事件,所有预先订阅的成员都会收到通知。在这种方法中,发布者不知道订阅者及其内部结构,因此我们实现了良好的关注点分离。此外,这种方式也满足了封装原则。
“观察者同步”使用一个统一的面向域的数据区域,并让每个屏幕都成为该数据的观察者。一个屏幕上的任何更改都会传播到该面向域的数据,进而传播到其他屏幕。这种方法是模型视图控制器方法的重要组成部分。”(Martine Fowler)
“一如既往,权衡在于复杂性和性能之间。粗粒度方法设置起来要简单得多,而且不太可能产生 bug。然而,粗粒度方法会导致屏幕进行大量不必要的刷新,这可能会影响性能。我的建议是,像往常一样,从粗粒度开始,只有在测量到实际性能问题后,在需要时才引入适当的细粒度机制。”(Martine Fowler)
观察者模式
为了实现观察者同步,我们要么使用语言内置的委托和事件(例如 C#),要么使用**观察者模式**。根据 dofactory 的说法,**观察者模式**“定义了对象之间的一对多依赖关系,以便当一个对象的状态发生变化时,所有依赖它的对象都会自动得到通知和更新。”。正如您所见,此定义满足观察者同步的特征,因为它会通知订阅者。因此,让我们看一下观察者模式的 UML 图
参与的类和对象是
- 主题(发布者)
- 知道它的观察者。任意数量的观察者对象可以观察主题
- 提供用于添加和删除观察者对象的接口。
- 具体主题
- 存储具体观察者感兴趣的状态
- 当状态改变时,向其观察者发送通知
- 观察者(订阅者)
- 为应被通知主题变化的对象定义更新接口。
- 具体观察者
- 维护对具体主题对象的引用
- 存储应与主题保持一致的状态
- 实现观察者更新接口,以使其状态与主题保持一致
如上类图所示,新闻发布者有两个名为“attach”和“detach”的方法。在这里,发布者负责附加订阅者,但订阅者(观察者)也可以将自己附加到发布者。除了哪一方附加另一方之外,该方法将订阅者类型(如果发布者附加订阅者)或发布者(反之亦然)作为接口或抽象类,并将其存储在 ArrayList 或普通数组中(取决于编程语言)。最终,发布者(总是)通过 `for` 或 `foreach` 循环通知订阅者。在下一节中,我将讨论如何使用 JavaScript 实现观察者模式,因为我们需要它来实现一些 MV* 模式。
如何使用 JavaScript 实现观察者模式?
与其它面向对象编程语言(或脚本)类似,JavaScript 也可以以其特有的方式实现设计模式,尽管使用 JavaScript 有点棘手,有时也令人困惑。因此,我将详细阐述观察者模式如何通过 JavaScript 实现。
推送 vs. 拉取
设想一个报纸销售系统。通常,在现实世界中有两种销售报纸的方式。第一种称为“推送”,即发布者雇佣送报员每天将报纸送到客户手中。第二种风格是将报纸放在报刊亭,以便客户可以去那里获取报纸(“拉取”)。尽管第一种风格基于发布者-订阅者方法,但我认为拉取应该通过流程同步来实现,因为每个用户都应该直接调用报刊亭来获取最新新闻。考虑到我们在观察者模式中使用“推送”方法,我将谈论如何通过 JavaScript 使用这种方法,这一点值得考虑。
构建观察者 API
既然您已经了解了观察者模式的基础知识,我们将看到如何通过 JavaScript 实现每个部分。首先,我们创建一个名为“publisher”的类。此类在前面演示的类图中扮演“Subject”的角色。这里,Subject 将订阅者添加到 Array 数据结构中。那么,让我们看看代码
window.publisher = function () {}
window.publisher.prototype.subscribers = [];
首先创建了 `publisher` 类,并将 `subscribers` 字段添加到 `publisher` 对象的原型中。下一步,我们将编写订阅代码,供观察者订阅发布者。
window.Function.prototype.subscribe = function (publisher) {
var self = this;
//When you make a new instance of an object,
//the scope will change to the new created object
var _publisher = new window.publisher();
var alreadyExist = _publisher.subscribers.some(function (el) {
if (el == self)
return;
});
if (!alreadyExist) {
//In this part the subscriber addes itself as one of the subscribers of publisher
publisher.subscribers.push(self);
}
return this;
}
`subscribe` 方法获取一个发布者,并将其自身添加为该发布者的订阅者。这部分有点棘手,让我们看看它是如何工作的。首先,看看下面这行
Function.prototype.subscribe=function(publisher){//Some codes}
这行代码为 Window 区域中的所有函数添加了一个额外功能,称为 `subscribe`。那么,它是如何工作的呢?原生 JavaScript 对象在其原型中存储方法。每个 JavaScript 对象都有一个原型。原型也是一个对象。所有 JavaScript 对象都从其原型继承属性和方法。`Function` 也是一个对象,有自己的原型。当我们创建一个对象,如 `var object ={};`,它只是一个空对象,那么为什么它有例如“`toString()`”方法呢?因为我们实际上是在抽象默认行为,即 `var object=new Object();`。由 new Object 创建的对象自然会接收 `object.__proto__ == Object.prototype`,其中 `Object.prototype` 是一个具有 Object.prototype.toString 属性的原生对象。
对于 Function、Array 等其他对象,情况也同样适用。当我们向 Function 对象添加新方法或字段时,由于所有函数都由基本 Function 对象原型继承,因此它们也继承了新方法。例如
很有趣,不是吗?是的!!现在我们可以对所有对象做同样的事情,就像我们对 Array 或 Function 对象所做的一样。我们也可以修改内置函数(但这超出了本文的范围)。
正如您所见,我使用了 `some` 函数。`some` 是一个 `boolean` 函数,它测试数组的某些成员是否可以通过回调函数实现的测试,并返回 `True` 或 `False`。在上面的行中,您可以看到 `some` 函数用于检查订阅者是否已存在。显然,当得出不存在的结论时,订阅者将被添加到 `publisher` 类的 `subscribers` 数组中。
既然观察者可以订阅,它们也应该能够退订。
window.Function.prototype.unSubscribe = function (publisher) {
var _publisher = new window.publisher();
var alreadyExist = _publisher.subscribers.filter(function (el) {
var self = this;
if (el !== self)
return el;
});
return this;
`filter` 方法根据回调功能过滤数组的成员。然后,在此方法中,`filter` 方法返回除对象本身之外的所有成员。
最后一个函数是 `deliver`,负责将事件传递给事件监听器(观察者)。
window.publisher.prototype.deliver = function (data) { this.subscribers.forEach(function (fn) { fn(data); }); return this; }
此函数使用 Array 对象的 `forEach` 方法。`forEach()` 方法为每个数组元素执行一次提供的函数。
既然观察者模式已经实现,它就可以像下面的简单示例一样使用
//Publishers var NewYorkTimesDelivery = new Publisher; var AustinHeraldDelivery = new Publisher; //Observer 1 var reader1 = function (from) { console.log('Delivery from ' + from + ' to Joe'); }; //Observer 2 var reader2 = function (from) { console.log('Delivery from ' + from + ' to Lindsay'); }; //Observers subscribe for news reader1. subscribe(NewYorkTimesDelivery). subscribe(AustinHeraldDelivery); reader2. subscribe(AustinHeraldDelivery). subscribe(NewYorkTimesDelivery); // Publishers deliver the latest news to their subscribers NewYorkTimesDelivery. deliver('Here is your paper! Direct from the Big apple'); AustinHeraldDelivery. deliver('News');
正如您可能已经意识到的,我在这个类中使用了方法链。**方法链**也称为**级联**,是指在同一个代码行中,在一个对象上反复调用一个方法接一个方法。它允许我们像阅读句子一样阅读代码,流畅地跨页流动。它也使我们摆脱了通常构建的单调、僵化的结构。
既然您已经了解了一些重要的基础知识,您就可以开始前往 MV* 模式的圣地了。然后打包您的行李,准备出发。
代表性模式
在本节中,我们将详细介绍每种 MV* 模式。首先,我将讨论每种模式的历史,然后是结构,最后是层之间的关系。之后,我将展示如何通过 JavaScript 实现每种模式。本文讨论了现有的 MV* 模式,分为三个主要系列:模型-视图-控制器 (MVC)、模型-视图-视图模型 (MVVM) 和模型-视图-呈现器 (MVP)。它采取了实践者的观点,强调了每个系列的要点以及它们之间的区别。在讨论 MV* 模式之前,让我们看看没有模式的应用世界是什么样的。为此,我将讨论**基于控件的用户界面**,然后我将详细阐述 MV* 模式。
表单和控件
表单和控件(也称为**基于控件的用户界面**)是可视化编程的概念,就像我们在 Visual Basic 和其他可视化编程语言中那样,程序员将一组控件添加到窗体中,然后简单地开始处理与设计窗体相关的类中的所有域逻辑。在这种方法中,窗体类负责处理与域区域相关的所有内容,例如业务逻辑、用户界面、事件处理和数据模型处理。基于控件的方法非常简单,对于小型应用程序来说完全足够。如果我们被期望创建一个简单的应用程序用于大学任务,并且知道它不会扩展,那么为什么我们需要更高级的东西呢?
这种方法已经在 90 年代由 Delphy、VB 等公司进行过测试,而且它也很成功。由于没有关注点分离,因此该方法通常使用**流程同步**。如前所述,表单和控件对于小型简单应用程序完全可以接受,但存在一些缺点,这使得该方法在复杂应用程序中使用时容易出现问题
- 没有关注点分离,因此很难重用。而且以这种方式编码的应用程序也不灵活
- 我们拥有的新表单和功能越多,可维护性就会越低
- 很可能一段时间后,这些系统会变成我们称为**遗留系统**的软件系统
然而,如前所述,这种方法也有以下一些优点
简单性:基于控件的用户界面非常易于理解:窗体上的控件成为窗体类的字段,因此开发人员可以像访问任何其他字段一样访问这些控件。
一致性:该方法采用流程同步。同步是显式处理的:目标视图/控件通过直接调用进行修改,因此代码易于理解。
效率:为不需要丰富用户界面的应用程序创建复杂的 Tier 架构是多余的。在这种情况下保持设计简单可以减少开发时间并提高可维护性。
MV* 模式
欢迎来到 MV* 模式的圣地。在这个圣地,您将了解 MVC、MVVM 和 MVP,它们的结构以及它们的区别。此外,您还将了解各自的优缺点。
该领域的所有模式都有两个共享部分:视图和模型。然而,模型和视图在所有模式之间共享,但每种模式的职责和**权限范围**各不相同。另一方面,每种模式都有第三部分,我们称之为“**\***”,这意味着这一部分对每种模式都不同。**“\***”可以是 MVVM 中的**视图模型**,MVC 中的**控制器**,或者是 MVP 中的**呈现器**。根据每种模式而变化的另一个问题是我们之前讨论过的同步方法的类型。
上图显示了三个标有问号“?”的箭头。此外,您可以看到第三部分负责连接视图和模型。当我们谈论两个组件如何相互通信时,实际上我们是在谈论不同的**同步**方法。本文的其余部分将专门调查第三部分(“\*”)的变化以及各层之间的关系(“?”)如何影响我们处理每种模式的方式。
SmalTalk'80 MVC : *模型-视图-控制器*
MVC 于 70 年代在 Xerox Parc 发明,首次在其名为《Smalltalk -80 用户界面范式模型-视图-控制器使用手册》的论文中发布了基础知识,该论文由 Glenn Krasner 和 Stephen Pope 于 1988 年 8 月/9 月发表。SmallTalk 80 的 MVC 的出现甚至早于“表单和控件”方法。最初,MVC 用于设计和构建具有丰富图形用户界面的桌面应用程序。随着时间的推移,原始 MVC 模式不断演变,并因技术发展和新需求而出现各种变体。如今,MVC 被用于将界面逻辑与域逻辑集成到 Web 应用程序和移动系统等各种领域的开发中。
结构
SmallTalk80 的 MVC 由三部分组成:模型、视图和控制器。在 SmallTalk MVC 中,**模型**负责**域数据**和**逻辑**。这部分非常重要,与其他模式不同,其他模式的模型仅负责域数据。**视图**组件负责显示模型数据。MVC 中的第三部分称为**控制器**,它负责连接模型和视图,并处理用户输入。控制器负责处理用户输入。控制器的另一项职责是处理事件。让我们看一下下面的图
协作
正如我们所说,MV* 模式中各层之间的协作是不同的。在 MVC 中,**控制器**充当**视图**和**模型**之间的中间人,处理从视图到模型的所有请求。参见图。在此图中,您可以看到视图和控制器都在观察模型。这意味着 MVC 使用观察者同步。视图和控制器协同工作,允许用户与用户界面进行交互。例如,用户界面可能提供一个文本框,允许用户输入用户名。视图负责渲染文本框。用户可以更改文本并按键(例如,回车键)——这些事件由控制器处理。模型维护域数据。通常,应用程序有一个模型以及一组与其协同工作的视图-控制器对。
但是,这里有些令人困惑的地方。我第一次开始实现这种模式时,完全感到困惑。我给一些写过 MVC 论文的人发了邮件,询问控制器在这个图中的作用。不幸的是,他们也感到困惑,无法给我一个明确的答案。那么,令人困惑的问题是什么呢?视图观察模型是正常的(因为它需要在模型发布时尽快获取模型数据),但为什么控制器也要观察模型呢?我们为什么需要它?正如我们所说,控制器只负责处理用户输入和事件。为了了解这一点,让我们谈谈 MVC 中的模型。
MVC 中的模型,如前所述,承担域数据和域逻辑部分。这种行为被认为是 MVC 的缺点之一,因为它不遵循关注点分离。想象一下,我们在视图中有一个文本框,视图通过控制器向模型发送请求以获取与文本框相关的某些数据。假设这个文本框包含年龄,并且我们希望当年龄超过 80 岁时,将文本框值的颜色更改为红色。谁对此负责?文本颜色纯粹是用户界面属性,因此不应成为模型的一部分。在 SmallTalk80 的 MVC 中,模型负责做这件事,这是不对的。
Smalltalk 开发人员找到了处理此类情况的方法,例如,通过开发实现所需逻辑的自定义视图。然而,这些解决方案并未解决根本问题。在我读到的一篇文章中,有这样一句话:“Smalltalk’80 MVC 为显示模型数据本身提供了很好的解决方案;然而,它没有提供显式的方法来处理不属于模型但使用户界面更易于使用的状态的呈现。”
让我们回到我们的大问题,“为什么控制器观察模型?”如果控制器观察模型,那么控制器就会被通知模型的所有变化(同时视图也在更新)。因此,控制器可以观察到将要显示给视图的数据。因此,控制器可以参与一部分域逻辑。这样我们就不会再遇到之前的问题了,因为控制器现在可以检查年龄了。另一方面,视图通过直接调用连接到控制器。因此,控制器可以每次读取年龄,将其某个属性设置为视图状态(文本框颜色的状态),以便视图可以通过直接调用访问控制器的属性并根据视图状态设置其颜色。因此,我们可以将一部分域逻辑分配给控制器,并将模型从不属于其职责的事情中解放出来。现在我们可以划掉下面这句话,因为它是不正确的:“它没有提供显式的方法来处理不属于模型但使用户界面更易于使用的状态的呈现”。
JavaScript 在 MVC 模式圣地
现在您已经了解了 MVC 模式的结构,是时候在 MVC 圣地跳舞了。我们的舞蹈将通过 JavaScript 实现一个简单的例子,其中模型、控制器和视图根据其各自的职责进行分离。这个简单的例子是关于一个简单的页面,该页面获取一些书籍信息,并根据指定的视图状态(例如价格...)来呈现它们。
您已经学会了如何使用 JavaScript 实现观察者模式。好吧,我们这里需要它,因为在 MVC 中,同步是基于观察者同步的。这个类将在一些其他 MV* 模式中使用,在这些模式中我们需要观察者同步。
//----- Observer Synchronizer Class -------
window.publisher = function () {}
window.publisher.prototype.subscribers = [];
window.publisher.prototype.deliver = function (data) {
this.subscribers.forEach(function (fn) {
fn(data);
});
return this;
}
window.publisher.prototype.ready = function (fn) {
window.addEventListener('DOMContentLoaded', function () {
var params = Array.prototype.slice.call(arguments, 1);
fn.apply(this, params);
}, false);
}
window.Function.prototype.subscribe = function (publisher) {
var self = this;
//When you make a new instance of an object,
//the scope will change to the new created object
var _publisher = new window.publisher();
var alreadyExist = _publisher.subscribers.some(function (el) {
if (el == self)
return;
});
if (!alreadyExist) {
publisher.subscribers.push(self);
}
return this;
}
window.Function.prototype.unSubscribe = function (publisher) {
var _publisher = new window.publisher();
var alreadyExist = _publisher.subscribers.filter(function (el) {
var self = this;
if (el !== self)
return el;
});
return this;
}
下一步如下面的图所示
正如您所见,**模型**继承自 Observer Synchronizer 类。`getData` 方法将由控制器调用。此方法可以非常简单,就像下面的示例一样,或者它可以调用一些 Ajax 来从数据库获取信息。
window.ModelPublisher = function () {
}
//Inheritance using Object.create() method
window.ModelPublisher.prototype = Object.create(window.publisher.prototype);
//The method that will be called by Controller and the Book Information will be delivered
window.ModelPublisher.prototype.getData = function () {
this.deliver({ Name: "Javascript", Price: 200 });
}
让我们看看**视图**。正如我所说,视图只负责视图元素,如 HTML 表单、控件、CSS 样式等。视图根据视图状态创建书籍列表,并为相应文本添加颜色。
视图还观察模型,以便在模型引发任何事件时收到通知。
这是视图观察模型的地方
var _publisher = new window.ModelPublisher();
view.bookVendorObserver.subscribe(_publisher);
那么,这就是用 JavaScript 实现 MVC 模式的一个非常简单的例子。代码部分更多,但我只讨论了关键部分,因为展示更多代码会使这篇文章成为代码垃圾。不过,如果您愿意,可以查看完整的代码,了解具体发生了什么。
Microsoft MVVM
2005 年,John Gossman 在他的博客上公布了 Model-View-View Model 模式。他是 Microsoft WPF 和 Silverlight 的架构师之一,顺便说一句,MVVM 在 Silverlight 和 WPF 中得到了应用,并且非常成功。MVVM 与 Fowler 的 Presentation Model 相同,因为这两种模式都包含视图的抽象,其中包含视图的状态和行为。Fowler 提出了 Presentation Model 作为创建 UI 平台无关的视图抽象的手段,而 Gossman 则提出了 MVVM 作为利用 WPF 核心功能来简化用户界面创建的标准方法。
正如您之前读到的,我在 MVC 中遇到了关于控制器及其权限范围的重大困惑。当我想在本文中讨论 MVVM 时,我考虑最好多阅读一些资源,突然我看到了这个:“在 Martin Fowler 的‘GUI 架构’文档(bit.ly/11OH7Y)中,他关于 MVC 这样说:‘在不同地方阅读 MVC 的人会从中获得不同的想法,并将其描述为‘MVC’。如果这还不够令人困惑,那么您还会通过‘中国式传话’系统产生对 MVC 的误解。’“Whatsa Controller Anyway”文档(bit.ly/7ajVeS)很好地总结了他的观点,并说:‘计算机科学家通常有一个令人讨厌的倾向,即过度使用术语。也就是说,他们倾向于给同一个词分配多个(有时是矛盾的)含义。’”
在 Smalltalk MVC 中,“每个”视图都会有一个控制器,并且任何时候只有一个控制器可以激活!这有时被认为是一个很大的缺陷。想象一下,您想为每个控制器拥有几个视图。MVC 本身并不支持此功能,要实现它,我们可能需要使用一些编程技巧和窍门。
这些不足和困惑导致了 MVVM 等新架构的出现。接下来的几节将详细介绍 MVVM。
结构
乍一看,MVVM 中最重要的变化是视图和模型通信的方式。第二个是称为“视图模型”的第三部分。
该模式具有线性结构。**视图**负责渲染用户界面;它可以观察视图模型,在需要时调用其方法并修改其属性。视图对视图模型保持**单向**引用。当视图模型属性更改时,通过**观察者同步**通知视图。另一方面,当用户与视图交互时,视图模型属性会直接修改。**视图模型**负责处理视图状态和用户交互;它可以访问域模型,因此可以处理域数据并调用业务逻辑。视图模型不知道视图。**模型**负责处理域数据,并且不知道视图模型。**这种方法允许为同一数据创建多个不同的视图**,并且观察者同步使这些视图能够同时工作。
协作
在此模式下,每个“视图模型”都可以有多个视图。您猜为什么?是的,因为多个视图观察一个单独的视图模型。“视图模型”可以直接访问模型。它调用模型的方法并通过事件返回结果!当发生事件时,所有视图都会收到通知并更新自身。正如您所见,“视图模型”就像一个包装器,可以防止视图和模型之间的直接通信。“视图模型”负责域逻辑。如前所述,视图也可以直接调用“视图模型”以通过“视图模型”请求模型中的数据。“视图模型”也负责处理视图的事件。例如,当按钮被点击时,会调用视图模型中的相应事件处理程序,并在“视图模型”中执行一些命令。
Microsoft 使用数据绑定来处理视图和“视图模型”之间的所有通信,但通过 JavaScript 实现需要我们自己找到方法。
JavaScript 在 MVVM 模式圣地
在这里,我将讨论使用我们之前的示例在 JavaScript 中实现 MVVM。实现 MVVM 表明使用 MVVM 是完全合理的,因为我对每个层及其任务都没有任何疑问。这里有一些变化。模型不再发布事件,而是视图模型承担这项任务。因此,模型非常简单,它只负责域数据,如下所示
Window.Model = function () {
var dataFile = {
Books: [
{
"Name": "Javascript",
"Author": "J.Reisig",
"Publisher": "Wrox",
"Pages": 534,
"Price": 200
},
{
"Name": "Head First Java",
"Author": "Kathy Sierra",
"Publisher": "Head First",
"Pages": 426,
"Price": 100
},
],
};
this.retriveBookList = function () {
return dataFile;
}
}
视图模型可以直接访问模型,而模型对视图模型和视图一无所知。然后,视图模型调用模型的方法来检索数据,其余的工作将由视图模型完成。
另一方面,视图模型是一个发布者。它获取数据,进行检查,并根据域逻辑设置视图状态,然后通过观察者同步将结果发送给视图。
另一方面,视图负责界面元素。它观察视图模型,并在视图模型发布书籍列表后立即收到通知。这就是我们看到 MVVM 的一个强大之处。现在,多个视图可以观察视图模型并根据自己的样式生成界面元素。例如,想象一下我们想在几个图表中显示接收到的数据,例如折线图、面积图和条形图。现在每个视图都可以根据相同的数据生成自己的图表。
正如您所见,MVVM 是处理关注点分离的一个非常强大的模式,但它似乎是其弱点之一是过度使用观察者同步,这可能会影响性能。在下一节中,我们将看到另一种不会出现性能问题的模式。
Dolphine Smalltalk MVP
欢迎来到本文的最后一部分。“模型-视图-呈现器”软件模式起源于 20 世纪 90 年代初,当时由Taligent创建,Taligent 是Apple、HP和IBM的合资企业,并且是 Taligent 的C++类 CommonPoint 环境的应用程序开发底层编程模型”(Wikipedia)。Taligent 于 1997 年倒闭后,Dolphin Smalltalk采用了 MVP 模式,作为其 Smalltalk 用户界面框架的基础。
结构
MVP 由三部分组成。**视图**,与 MVC 类似,负责显示模型的数据。在一篇论文中提到,MVP 中的视图几乎不变,但根据Dolphine Smalltalk 论文,与 MVC 不同,在这里,视图负责处理一些事件,例如 `onclick`:“MVP 的一个显著区别是去掉了控制器。取而代之的是,视图需要处理操作系统生成的原始用户界面事件(在 Windows 中,这些事件以 WM_xxxx 消息的形式传入),而这种工作方式更自然地契合大多数现代操作系统的风格。”因此,我第一次犯了个错误,将事件处理部分编码到**呈现器**中,而根据上述论文,**视图**负责事件处理。
在这里,**呈现器**负责确认域逻辑。此外,**呈现器**负责视图如何操作模型数据或向模型发送请求。MVP 的核心就在这里,它与其他 MV* 模式有显著区别。我将在下一节讨论这一点。
在这里,**模型**扮演着**发布者**的角色,而一些充当**观察者**角色的视图。此外,模型对呈现器和视图一无所知。在 MVP 中,**模型**是纯粹的域对象,与 MVC 中的模型不同,它不能操作视图。
协作
**视图**观察模型,模型通知视图。视图和呈现器之间存在双向连接。这就是 MVP 的核心所在,**呈现器可以直接访问视图**。这意味着视图从模型获取数据,呈现器可以自发地施加域逻辑。视图触发呈现器来执行域逻辑。呈现器执行域逻辑并命令视图更新其视图状态。Dolphin Smalltalk MVP 模式的巨大灵活性来自于呈现器可以直接访问视图的决定。这可以被认为是呈现器的一大优势,但这种对呈现器的赋权很容易导致复杂性。
JavaScript 在 MVP 模式圣地
在本节中,**模型**的代码与我在 MVC 中编写的代码相同。那么,让我们专注于呈现器和视图,因为关键区别就在于此。
MVP 中的视图与 MVC 中的视图几乎相同,关键区别在于 MVP 中的视图还负责处理**控制器**事件,例如 `onclick`。
在这里,视图触发呈现器执行域逻辑。
结果,呈现器将执行域逻辑,并直接调用视图的 `setCss()` 方法。此外,呈现器可以根据视图发出的请求调用模型相应的方。
正如我所说,我避免在此处放置全部代码,因此请查看本文附带的代码以详细了解代码。
虽然我介绍了 Dolphine Smalltalk MVP,但讨论 MVP(Passive View)是完全必要的,因为如今当我们谈论 MVP 时,我们主要指的是**Passive View Pattern**。在后续更新中,我将详细阐述。
结论
编写本文对我来说是一次旅程,因为很少有文章描述实现 JavaScript 的 MV* 模式并描述架构细节。因此,我不得不阅读多篇论文。在这个学习过程中,我给一些作者发了邮件,以澄清我的疑虑。有趣的是,有时即使是论文作者在谈到 MV* 模式时也并非没有疑虑。因此,我也并非完全没有疑虑。我试图收集每种模式的所有论文、会议和讨论,并通过本文发布我自己的经验。
随着 JavaScript 在现代 Web 开发时代被视为一项严肃的客户端解决方案,我认为在不久的将来,许多开发人员将相信客户端编程中的关注点分离。这就是为什么我决定写这篇基础性文章,希望能对未来的开发者有所帮助。之所以取名为“肚皮舞”,是因为历史上有时用它来帮助孕妇为分娩做准备。那么,现在轮到您猜为什么我为这篇文章选择了这个名字了。最后,我可以称本文为一封未来的信,届时客户端的关注点分离将比今天受到更严肃的对待!感谢所有阅读本文的人。我期待着您的提问、疑虑以及您改进可能错误的建议。
参考文献
http://www.object-arts.com/downloads/papers/TwistingTheTriad.PDF
http://www.computer.org/csdl/proceedings/wicsa/2014/3412/00/3412a021.pdf[^]
https://www.lri.fr/~mbl/ENS/FONDIHM/2013/papers/Krasner-JOOP88.pdf[^]
http://blog.osteele.com/posts/2004/08/web-mvc/[^]
http://micsymposium.org/mics_2009_proceedings/mics2009_submission_55.pdf[^]
http://msdn.microsoft.com/en-us/magazine/hh580734.aspx[^]
http://www.amazon.com/Pro-JavaScript-Design-Patterns-Object-Oriented/dp/159059908X[^]
http://c2.com/cgi/wiki?WhatsaControllerAnyway
历史
- 2014年12月7日