上下文
许多计算机系统的用途都是从数据存储检索数据并将其显示给用户。在用户更改数据之后,系统再将更新内容存储到数据存储中。因为关键的信息流发生在数据存储和用户界面之间,所以您可能倾向于将这两部分绑在一起,以减少编码量并提高应用程序性能。但是,这种看起来自然而然的方法有一些大问题。一个问题是,用户界面的更改往往比数据存储系统的更改频繁得多。将数据和用户界面这两部分耦合在一起带来的另一个问题是,业务应用程序往往会并入远不止数据传输功能的其他业务逻辑。
问题
如何让 Web 应用程序的用户界面功能实现模块化,以便您可以轻松地单独修改各个部分?
影响因素
下列影响因素作用于此上下文内的系统,在考虑问题的解决方案时必须协调这些因素:
用户界面逻辑的更改往往比业务逻辑频繁,尤其是在基于 Web 的应用程序中。例如,可能添加新的用户界面页,或者可能完全打乱现有的页面布局。毕竟,基于 Web 的瘦客户端应用程序的优点之一是可以随时更改用户界面,而不必重新分发应用程序。如果将显示代码和业务逻辑组合一起并放在单个对象中,则每次更改用户界面时,都必须修改包含业务逻辑的对象。这很可能引入错误,并需要在对用户界面进行每个极小更改之后都要重新测试所有业务逻辑。
在某些情况下,应用程序以不同的方式显示同一数据。例如,分析员喜欢用电子表格视图显示数据,而管理人员喜欢用饼图显示相同的数据。在一些胖客户端用户界面中,常常用多个视图同时显示相同数据。如果用户在一个视图中更改了数据,则系统必须自动更新该数据的其他所有视图。
设计令人赏心悦目而有效的 HTML 页通常要求采用一套与开发复杂业务逻辑不同的技能。很少有人同时具有这两种技能。因此,将这两部分的开发工作分隔开来是最理想的。
用户界面活动通常由以下两部分组成:显示和更新。显示部分负责从数据源检索数据,并格式化数据以便进行显示。当用户基于该数据执行操作时,更新部分将控制权返回给业务逻辑,以便更新数据。
在 Web 应用程序中,单个页面请求将这两方面的工作组合在一起:与用户所选链接相关联的操作进行的处理,以及目标页面的显示。在许多情况下,目标页可能不与操作直接相关。例如,假设有一个用于显示项目列表的简单 Web 应用程序。在将项目添加到列表或从列表中删除项目之后,用户将返回主列表页。因此,应用程序必须在执行两个有很大差异的命令(添加或删除)之后显示相同页面(列表),而所有这些操作均在同一个 HTTP 请求内进行。
与业务逻辑相比,用户界面代码对设备的依赖性往往更大。如果要将应用程序从基于浏览器的应用程序迁移到个人数字助理 (PDA) 或支持 Web 的手机上,则必须替换很多用户界面代码,而业务逻辑可能不受影响。这两部分的完全分离可以使迁移更快完成,并最大限度地降低将错误引入业务逻辑的风险。
通常,为用户界面创建自动测试比为业务逻辑更难、更耗时。因此,减少直接绑到用户界面中的代码量可提高应用程序的可测试性。
解决方案
Model-View-Controller (MVC) 模式基于用户输入将域的建模、显示和操作分为三个独立的类 [Burbeck92]:
- 模型。模型用于管理应用程序域的行为和数据,并响应为获取其状态信息(通常来自视图)而发出的请求,还会响应更改状态的指令(通常来自控制器)。
- 视图。视图用于管理信息的显示。
- 控制器。控制器用于解释用户的鼠标和键盘输入,以通知模型和/或视图进行相应的更改。
图 1 描述了这三个对象之间的结构关系。
图 1:MVC 类结构
请务必注意,视图和控制器都依赖于模型。但是,模型既不依赖于视图,也不依赖于控制器。这是分离的主要优点之一。这样的分离允许模型在独立于可视表示功能的情况下建立和测试。在许多胖客户端应用程序中,视图与控制器的分离是次要的,实际上,许多用户界面框架将角色实现为一个对象。另一方面,在 Web 应用程序中,视图(浏览器)与控制器(处理 HTTP 请求的服务器端组件)的分离是很好定义的。
Model-View-Controller 是一个用于将用户界面逻辑与业务逻辑分离开来的基础设计模式。遗憾的是,此模式的普及导致了许多错误的描述。特别是在不同的上下文中,术语"控制器"已经用于意指不同的事物。幸运的是,Web 应用程序的出现已经帮助消除了一些不明确性,因为视图与控制器的分离是如此明显。
变型
在 Application Programming in Smalltalk-80: How to use Model-View-Controller (MVC) [Burbeck92] 中,Steve Burbeck 描述了 MVC 的两个变型:被动模型和主动模型。
当一个控制器以独占方式操作模型时,将使用被动模型。控制器将修改模型,然后通知视图:模型已经更改,应该进行刷新(见图 2)。此情况下的模型完全独立于视图和控制器,这意味着模型无法报告其状态更改。HTTP 协议是此方案的示例。浏览器没有从服务器获取异步更新的简单方法。浏览器显示视图并对用户输入作出响应,但是它不会检测服务器上的数据更改。仅当用户显式请求刷新时,才会询问服务器是否发生了更改。
图 2:被动模型的行为
当模型更改状态而不涉及控制器时,将使用主动模型。当其他资源正在更改数据并且更改必须反映在视图中时,可能会发生这种情况。以股票报价机的显示为例。您从外部源接收股票数据,并希望当股票数据更改时更新视图(例如,报价机数据区和警告窗口)。因为只有模型检测对其内部状态的更改(在这些更改发生时),所以模型必须通知视图刷新显示。
但是,使用 MVC 模式的一个目的是使模型独立于视图。如果模型必须将更改通知视图,则会重新带来您希望避免的依赖性。幸运的是,Observer 模式 [Gamma95] 提供了这样的机制:提醒其他对象注意状态的更改,而不会导致对这些对象的依赖性。各个视图实现 Observer 接口,并向模型注册。模型将跟踪由订阅更改的所有观察器组成的列表。当模型发生改变时,模型将会遍历所有已注册的观察器,并将更改通知它们。此方法通常称为"发布-订阅"。模型从不需要有关任何视图的特定信息。实际上,在需要将模型更改通知控制器的情况下(例如,启用或禁用菜单选项),控制器必须做的全部工作是实现 Observer 接口并订阅模型更改。对于存在许多视图的情况,定义多个主体是有意义的,其中每个主体都描述了特定类型的模型更改。然后,每个视图都只能订阅与视图有关的更改类型。
图 3 显示了使用 Observer 的主动 MVC 的结构,以及观察器如何将模型与直接引用视图隔离开来。
图 3:在主动模型中使用观察器将模型与视图分离
图 4 说明当模型发生改变时 Observer 如何通知视图。可惜的是,在统一建模语言 (UML) 序列图中,没有好的方法来演示模型与视图的分离,因为该图表示的是对象的实例而不是类和接口。
图 4:主动模型的行为
示例
请参阅在 ASP.NET 中实现 Model-View-Controller.
测试考虑事项
在使用 Model-View-Controller 时,可测试性有了极大提高。当组件(尤其是用户界面组件)相互高度依赖时,测试组件变得很困难。这些类型的组件通常需要复杂的设置,只是为了测试简单的功能。更糟的是,在出现错误时,很难确定该问题发生在哪个特定组件上。这就是为什么将任务分离开来是重要的体系结构级别动机。MVC 将存储、显示和更新数据的任务分隔到三个可以分别进行测试的组件中。
除因互相依赖而产生的问题之外,用户界面框架本身是很难测试的。测试用户界面需要冗长乏味的(且易于出错)手动测试,或需要模拟用户操作的测试脚本。这些脚本的开发往往很耗时,而且容易被破坏。MVC 没有消除对用户界面测试的需要,但是,通过将模型与显示逻辑分离,可以允许模型在独立于显示的情况下进行测试,并减少了用户界面的测试步骤数。
结果上下文
围绕 MVC 模式构建显示层具有下列优缺点:
优点
支持多个视图。因为视图与模型分离,而且模型与视图之间没有直接依赖性,所以用户界面可以同时显示同一数据的多个视图。例如,Web 应用程序中的多个页面可以使用同一模型对象。另一个示例是允许用户对页面外观进行更改的 Web 应用程序。这些页面显示来自共享模型的同一数据,但以不同的方式进行显示。
适应更改。用户界面要求的更改往往比业务规则快。用户可能更喜欢新设备(如手机或 PDA)采用另一颜色、字体、屏幕布局和支持级别。因为模型不依赖于视图,所以将新类型的视图添加到系统中通常不会影响模型。因此,更改的作用范围仅限于视图。此模式为其进一步的专门化模式(如 Page Controller 和Front Controller)奠定了基础。
缺点
复杂性。MVC 模式引入了新的间接级别,因此稍微增加了解决方案的复杂性。它还增加了用户界面代码的事件驱动特性,调试用户界面代码会变得更加困难。
频繁更新的成本。将模型与视图分离并不意味着模型的开发人员可以忽略视图的特性。例如,如果模型发生频繁更改,则它可能向视图发出大量更新请求。一些视图(如图形显示)的显示可能需要一定时间。因此,视图可能滞后于更新请求。因此,在对模型进行编码时牢记视图是很重要的。例如,模型可以将多个更新作为单个通知发送到视图。
变体
Document-View 变体可以识别 Model-View-Controller 的所有三个角色,但它把控制器合并到视图中。文档对应于 MVC 中的模型角色。此变体存在于许多现有 GUI 平台中。Document-View 的一个极佳示例是 Microsoft Visual C++® 环境中的 Microsoft® 基础类库 (MFC)。使用此变体时,要考虑到视图与控制器更紧密耦合的影响。
相关模式
有关详细信息,请参阅下列相关模式:
Observer。此模式通常与 MVC 一起提及,因为需要使视图和关联模型保持同步。
Page Controller 和Front Controller 描述 MVC 模式的控制器部分的实现策略。
致谢
Model-View-Controller 最初是 Trygve Reenskaug 在二十世纪七十年代末为 Smalltalk 平台开发的框架 [Fowler03]。您刚才阅读的版本参考了以下著作:
[Burbeck92] Burbeck, Steve. "Application Programming in Smalltalk-80: How to use Model-View-Controller (MVC)."University of Illinois in Urbana-Champaign (UIUC) Smalltalk Archive. Available at: http://st-www.cs.uiuc.edu/users/smarch/st-docs/mvc.html.
[Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.
[Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.