[简介]
本文对应于Web表示模式集群,文章的前半部分重笔墨的描述了MVC模式的架构、设计及其ASP.NET实现,而在更加复杂的系统中,随后提出了Page Controller(页面控制器)和Front Controller(前端控制器)作为MVC实现的补充,最后,简要介绍了Web表示模式集群的另外两个模式:Intercepting Filter(筛选器)和Page Cache(页面缓存)模式。
“体系结构设计者的第一个作品往往比较简练和干净。他知道自己并不了解正在进行的工作,因此他小心谨慎地设计它。在他设计第一个作品时,会进行多次修饰和润色。这些会留到“下一次”使用……这第二个系统是他曾经设计的最危险的系统……一般趋势是,在设计第二个系统时,将会使用在第一个作品中被小心搁置在一边的所有思路和修饰,从而导致设计过了头。”
—Frederick P. Brooks, Jr.发表于1972年的The Mythical Man Month(人月神话)。
Web上建立的第一个系统是简单地链接在一起的静态HTML页面,以便在分散的小组之间共享文档。随着用户的使用量增加,可响应用户输入的动态网页日益普遍。早期的动态页面一般是以通用网关接口(CGI)脚本的形式编写的。这些CGI脚本不仅包含用来确定在响应用户输入时应当显示什么内容的业务逻辑,而且还会生成表示HTML。随着对更复杂逻辑需求的增加,对更丰富、更生动的表示形式的需求也随之增加。这种增加了的复杂性给CGI编程模型带来了挑战。
不久,基于页面的开发手段(如ASP和JSP)出现了。这些新方法允许开发人员将脚本直接嵌入到HTML面中,从而简化了编程模型。当这些嵌入的脚本应用程序变得更复杂时,开发人员希望在页面级别将业务逻辑与表示逻辑分开。为适应这一要求,随之出现了具有帮助器对象和代码隐藏页面策略的标记库。然后,又出现了提供动态配置站点导航和命令调度程序的精细框架,但所有这一切都是以增加复杂性为代价的。假设现在有大量的Web表示可选方案,如何为您的应用程序选择适当的Web表示设计策略?
是否真的有一个设计策略能够适应所有的情况?很不幸,在软件设计中,消除过多的冗余和过度的复杂性是一个竞争性需求,很难能够真正做到两者之间的平衡。您可以从包含嵌入脚本的简单页面开始设计工作,但很快业务逻辑就会不断重复出现在各个文件中,从而导致系统难以维护和扩展。通过将该逻辑移到一组协作组件中,可以消除冗余,但是这样做会增加解决方案的复杂性。另一方面,您的设计工作可以从设计用来提供标记库、动态配置和命令调度程序的框架入手,可是这样虽然能够消除冗余代码,但它会大大增加系统的复杂性,而这通常是不必要的。
而如何考虑各个方面的需求,提出一个最合适我们应用的Web表示策略呢?这需要在复杂解决方案(支持将来可能发生变化的情形)和简单解决方案(满足目前的要求)之间做出抉择,原则上适当增加成本是可取的,而过多增加成本却是不可取的。那么废话少说,我们就从“简单”开始吧。
MVC(模型—视图—控制)
许多计算机系统的用途都是从数据存储检索数据并将其显示给用户。在用户更改数据之后,系统再将更新内容存储到数据存储中。因为关键的信息流发生在数据存储和用户界面之间,所以您可能倾向于将这两部分绑在一起,以减少编码量并提高应用程序性能。但是,这种看起来自然而然的方法有一些大问题。一个问题是,用户界面的更改往往比数据存储系统的更改频繁得多。将数据和用户界面这两部分耦合在一起带来的另一个问题是,业务应用程序往往会并入远不止数据传输功能的其他业务逻辑。如何让Web应用程序的用户界面功能实现模块化,以便您可以轻松地单独修改各个部分?
Model-View-Controller正是这样的模式,它实现功能模块和显示模块的分离,使得应用程序更加可维护,可扩展,可移植和可复用,它最初是Trygve Reenskaug在二十世纪七十年代末为Smalltalk平台开发的框架[Fowler03],而发展到目前为止,已经形成了一个非常成熟的模式。
MVC解决方案
Model-View-Controller (MVC)模式基于用户输入,将域的建模、显示和操作分为三个独立的类[Burbeck92]:
模型。模型用于管理应用程序域的行为和数据,并响应为获取其状态信息(通常来自视图)而发出的请求,还会响应更改状态的指令(通常来自控制器)。
视图。视图用于管理信息的显示。
控制器。控制器用于解释用户的鼠标和键盘输入,以通知模型和/或视图进行相应的更改。
图1、描述了这三个对象之间的结构关系
视图和控制器都依赖于模型。但是,模型既不依赖于视图,也不依赖于控制器。这是分离的主要优点之一。这样的分离允许模型在独立于可视表示功能的情况下建立和测试。在许多胖客户端应用程序中,视图与控制器的分离是次要的,实际上,许多用户界面框架将角色实现为一个对象。另一方面,在Web应用程序中,视图(浏览器)与控制器(处理HTTP请求的服务器端组件)的分离是很好定义的。
Model-View-Controller是一个用于将用户界面逻辑与业务逻辑分离开来的基础设计模式。遗憾的是,此模式的普及导致了许多错误的描述。特别是在不同的上下文中,术语“控制器”已经用于意指不同的事物。幸运的是,Web应用程序的出现已经帮助消除了一些不明确性,因为视图与控制器的分离是如此明显。
MVC的变型
在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、主动模型的行为
MVC的ASP.NET实现
为了解释如何在ASP.NET中实现Model-View-Controller模式,并说明在软件中分离模型、视图和控制器角色的好处,下面的示例将一个没有分离所有三个角色的单页面解决方案重构为分离这三个角色的解决方案。示例应用程序是一个带有下拉列表的网页(如栏下图所示),该页面显示了存储在数据库中的记录。
图5
利用Microsoft Visual Studio(r) .NET开发系统的代码隐藏功能,可以很容易地将表示(视图)代码与Model-Controller代码分离开来。每个ASP.NET页都有一种机制,这种机制允许在单独的类中实现从网页调用的方法。该机制是通过Visual Studio .NET提供的,它有许多优点,例如Microsoft IntelliSense(r)技术。当您使用代码隐藏功能来实现网页时,可以使用IntelliSense来显示网页后面的代码中所使用的对象的可用方法列表。IntelliSense不适用于.aspx页。与此同时,为了展现Model-Controller的分离,对于数据库操作提取了DatabaseGateway,这样就实现了三者的完整分离。
视图
<%@ Page language="c#" Codebehind="Solution.aspx.cs"
AutoEventWireup="false" Inherits="Solution" %>
<html>
<head>
<title>解决方案</title>
</head>
<body>
<form id="Solution" method="post" runat="server">
<h3>录音</h3>
选择录音:<br/>
<asp:dropdownlist id="recordingSelect" runat="server" />
<asp:button id="submit" runat="server" text="Submit"
enableviewstate="False" />
<p/>
<asp:datagrid id="MyDataGrid" runat="server" width="700"
backcolor="#ccccff" bordercolor="black" showfooter="false"
cellpadding="3" cellspacing="0" font-name="Verdana" font-size="8pt"
headerstyle-backcolor="#aaaadd" enableviewstate="false" />
</form>
</body>
</html>
模型
using System;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
public class DatabaseGateway
{
public static DataSet GetRecordings()
{
String selectCmd = "select * from Recording";
SqlConnection myConnection =
new SqlConnection(
"server=(local);database=recordings;Trusted_Connection=yes");
SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
DataSet ds = new DataSet();
myCommand.Fill(ds, "Recording");
return ds;
}
public static DataSet GetTracks(string recordingId)
{
String selectCmd =
String.Format(
"select * from Track where recordingId = {0} order by id",
recordingId);
SqlConnection myConnection =
new SqlConnection(
"server=(local);database=recordings;Trusted_Connection=yes");
SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
DataSet ds = new DataSet();
myCommand.Fill(ds, "Track");
return ds;
}
控制
using System;
using System.Data;
using System.Collections;
using System.Web.UI.WebControls;
public class Solution : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Button submit;
protected System.Web.UI.WebControls.DataGrid MyDataGrid;
protected System.Web.UI.WebControls.DropDownList recordingSelect;
private void Page_Load(object sender, System.EventArgs e)
{
if(!IsPostBack)
{
DataSet ds = DatabaseGateway.GetRecordings();
recordingSelect.DataSource = ds;
recordingSelect.DataTextField = "title";
recordingSelect.DataValueField = "id";
recordingSelect.DataBind();
}
}
void SubmitBtn_Click(Object sender, EventArgs e)
{
DataSet ds =
DatabaseGateway.GetTracks(
(string)recordingSelect.SelectedItem.Value);
MyDataGrid.DataSource = ds;
MyDataGrid.DataBind();
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: 此调用是 ASP.NET Web 窗体设计器所必需的。
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// 设计器支持所必需的方法 - 不要使用代码编辑器修改
/// 此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.submit.Click += new System.EventHandler(this.SubmitBtn_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
以上示例简单的说明了ASP.NET的MVC实现,在实际项目中,商务逻辑远远不止这样,但