本页内容
上下文
问题
影响因素
解决方案
示例
结果上下文
测试考虑事项
相关模式
致谢
上下文
您已经决定使用
Model-View-Controller (MVC) 模式来将动态 Web 应用程序的用户界面组件与业务逻辑分隔开来。要构建的应用程序将以动态方式构造网页,但网页间导航多为静态导航。
问题
如何以最佳方式为适度复杂的 Web 应用程序构建控制器,从而既能避免代码重复,又能实现重用性和灵活性?
影响因素
以下因素影响这种情况中的系统,在考虑上述问题解决方案时必须协调这些因素:
- MVC 模式通常主要关注模型与视图之间的分隔,而对于控制器的关注较少。在许多胖客户端方案中,控制器和视图之间的分隔相对次要,因此通常会被忽略
[Fowler03]。但在瘦客户端应用程序中,视图和控制器本来就是分隔的,这是因为显示是在客户端浏览器中进行的,而控制器是服务器端应用程序的一部分。因此有必要对控制器进行更为仔细的研究。
- 在动态 Web 应用程序中,多用户操作可以导致不同的控制器逻辑,然后显示相同页面。例如,在基于
Web 的简单电子邮件应用程序中,发送邮件和从收件箱中删除邮件这两个操作都可能将用户返回(刷新后的)收件箱页面。虽然这两种活动完成之后显示相同页面,但应用程序必须根据上一页面以及用户所单击的按钮来执行不同的操作。
- 显示大多数动态网页的代码都包括非常相似的步骤:验证用户身份、从查询字符串或表单域中提取页面参数、收集会话信息、从数据源检索数据、生成页面动态部分以及添加适用的页眉和页脚。这可能导致大量的代码重复。
- 脚本化服务器页面(例如 ASP.NET)可能很容易创建,但在应用程序不断增大时可能带来一些缺点。脚本化页面不能较好地分隔控制器和视图,因而降低了重用的可能性。例如,如果多个操作将导致相同页面,在多个控制器之间重用显示代码则会比较困难,这是因为显示代码与控制器代码混合在一起。对散布于业务逻辑和显示逻辑之间的脚本化服务器页面也更加难以进行测试和调试。最后,开发脚本化服务器页面要求同时精通开发业务逻辑和制作美观高效的
HTML 页面,而很少有人兼备这两项技能。基于上述考虑,因此有必要最大程度地减少脚本化服务器页面代码,而在实际类中开发业务逻辑。
- 正如 MVC 模式中的相关叙述,测试用户界面代码往往耗时而单调。如果可以分隔用户界面专用代码和实际业务逻辑,测试业务逻辑则会更为简单,且可重复性更强。对于显示部分和应用程序控制器部分都是如此。
- 通用外观和导航结构往往可以提高 Web 应用程序的可用性和品牌认知度。但通用外观可能会导致显示代码重复,特别是在脚本化服务器页面中嵌入代码时。因此,需要一种机制以提高页面间显示逻辑的重用性。
解决方案
使用 Page Controller 模式接受来自页面请求的输入、调用请求对模型执行的操作以及确定应用于结果页面的正确视图。分隔调度逻辑和所有视图相关代码。如果合适,创建用于所有页面控制器的公用基类,以避免代码重复并提高一致性和可测试性。图
1 显示了页面控制器与模型和视图的关系。
图 1:页面控制器结构
页面控制器可接收页面请求、提取所有相关数据、调用对模型的所有更新以及向视图转发请求。而视图又将根据该模型检索要显示的数据。定义独立页面控制器将分隔模型与
Web 请求细节(例如会话管理,或使用查询字符串或隐藏表单域向页面传递参数)。按照这种基本形式,为 Web
应用程序中的每个链接创建控制器。控制器因而将变得非常简单,因为每次仅须考虑一个操作。
为每个网页(或操作)创建独立控制器可能会导致大量代码重复。因此应该创建 BaseController
类以合并验证参数(请参阅图 2)等公用函数。每个独立页面控制器都可以从 BaseController
继承此公用功能。除了从公用基类继承之外,还可以定义一组帮助器类,控制器可以调用这些类来执行公用功能。
图 2:使用 BaseController 消除代码重复
如果多数页面相似,并且可以将公用功能放入一个基类,则此方法非常有效。页面变化越多,必须插入继承树的级别也就越多。比如,所有页面都分析参数,但只有显示列表的页面才从数据库检索数据,而需要输入数据的页面则会更新模型而不检索数据。现在可以引入两个新基类,即
ListController 和 DataEntryController,这两个类都是继承
BaseController 而得到的。然后列表页可以从 ListController
继承,而数据输入页则可以从 DataEntryController 继承。虽然这种方法在这个简单示例中非常有效,但在处理实际业务应用时,继承树可能很深且非常复杂。您可能希望向基类中添加条件逻辑,以适应某些变体,但如此操作将违反封装原则,基类也会因此在更改系统时造成较大麻烦。因此在应用程序变得更为复杂时,应当考虑使用帮助器类或者
Front Controller 模式。
因为很多时候都需要对 Web 应用程序使用页面控制器,因此多数 Web 应用程序框架都默认实现页面控制器。大多数框架以服务器页面的形式包含了页面控制器(例如
ASP、JSP 和 PHP)。服务器页面实际上组合了视图和控制器的功能,但没有提供显示代码与控制器代码之间的相应分隔。遗憾的是,对于有些框架,混合视图相关代码与控制器相关代码很轻松,但要正确分隔控制器逻辑却很困难。因此,Page
Controller 方式在很多开发人员中口碑不佳。现在很多开发人员都将 Page Controller
与较差设计联系在一起,而将 Front Controller 与较好设计联系在一起。实际上,这种感觉是由于具体的实现在不完善的情况下造成的;Page
Controller 和 Front Controller 都是可行性极佳的体系结构选择。
因此,最好将控制器逻辑单独放入可以从服务器页面调用的独立类。ASP.NET 页面框架提供了可以实现这种分隔的完善机制,这种机制称为"代码隐藏类"。(请参阅在
ASP.NET 中实现 Page Controller)。
变体
大多数情况下,页面控制器取决于基于 HTTP 的 Web 请求的具体细节。因此,页面控制器代码通常包含对
HTTP 头、查询字符串、表单域、多部分表单请求等的引用。因此在 Web 应用程序框架之外测试控制器代码非常困难。唯一方法是通过模拟
HTTP 请求和分析结果来测试控制器。这种类型的测试既费时且易出错。因此,要提高可测试性,可以将依赖 Web
的代码和不依赖 Web 的代码分别放入两个单独类中(请参阅图 3)。
图 3:分隔依赖 Web 的代码和不依赖 Web 的代码
在此示例中,AspNetController 封装了在应用程序框架 (ASP.NET)
上的所有依赖项。例如,它可以提取来自 Web 请求的所有传入参数,并使用独立于 Web 界面的方式(例如,使用集合)将其传递至
BaseController。此方法不仅可提高可测试性,而且允许通过其他用户界面重用该控制器代码,例如胖客户端界面或自定义脚本语言。
此方法的缺点在于增加了开销。现在新增了一个类,并且在处理每个请求前必须首先对其进行转换。因此,应尽可能控制器受环境影响的部分,并权衡选择降低依赖性与提高开发效率及执行效率。
示例
请参阅在
ASP.NET 中实现 Page Controller。
结果上下文
使用 Page Controller 模式存在下列优缺点。
优点
- 简单性。由于每个动态网页由特定控制器处理,所以这些控制器仅需进行有限范围的处理,从而可以保持简单性。由于每个页面控制器仅处理一个网页,所以
Page Controller 尤其适用于导航结构简单的 Web 应用程序。
- 内置框架功能。在其多数基本形式下,控制器已经置入大多数 Web 应用程序平台。例如,如果用户单击网页中指向由
ASP.NET 脚本生成的动态页面的链接,则 Web 服务器将分析与该链接关联的 URL,并执行相关
ASP.NET 页面。实际上,这些 ASP.NET 页面是用户所执行操作的控制器。ASP.NET 页面框架还提供了用于执行控制器代码的代码隐藏类。代码隐藏类提供了控制器和视图之间的更好分隔,并且允许创建合并所有控制器公用的功能控制器基类。有关示例,请参阅"在
ASP.NET 中实现 Page Controller"。
- 增强型重用性。创建控制器基类可以减少代码重复,并允许在不同的页面控制器重用公用代码。可以通过在基类中实现重复逻辑来重用代码。然后,所有具体的
Page Controller 对象将自动继承此逻辑。如果该逻辑的实现对于不同页面而有所不同,则仍可以在基类中使用
Template Method,并实现基本的执行结构;具体子步骤的实现可能因页面的不同而有所变化。
- 可扩展性。通过使用帮助器类,可以很简便地扩展页面控制器。如果控制器中的逻辑过于复杂,则可以向帮助器类委派部分逻辑。除了继承之外,帮助器类还提供了另一种重用机制。
- 开发人员责任的分隔。使用 Page Controller 类有助于分离开发队伍中各成员的责任。控制器开发人员必须熟悉由应用程序所实现的域模型和业务逻辑。另一方面,视图设计者可以专注于结果的显示风格。
缺点
由于其简单性,Page Controller 是大多数动态 Web 应用程序的默认实现方式。但是应该了解下列限制:
- 每个页面一个控制器。 Page Controller 的主要缺点是要为每个网页创建一个控制器。该特点非常适用于具有一组静态页面和简单导航路径的应用程序。有些较为复杂的应用程序要求对页面和页面间的导航映射进行动态映射。如果将此逻辑分散于众多页面控制器,则可能导致应用程序难以维护,即使某些逻辑可以放入基本控制器。另外,Web
框架的内置功能可能会降低对 URL 和资源路径命名时的灵活程度(虽然可以使用 ISAPI 筛选器等较低级别的机制进行一定程度的补偿)。在这些方案中,请考虑使用
Front Controller 截取所有 Web 请求,并根据可配置规则将这些请求转发至相应处理程序。
- 较深的继承树。继承似乎是基于对象编程方式的既最可爱又最讨厌的功能之一。如果仅通过使用继承来重用公用功能,则可能降低继承层次结构的灵活性。有关详细信息,请参阅"在
ASP.NET 中实现 Page Controller"。
- 对于 Web 框架的依赖。在基本形式中,页面控制器仍然依赖于 Web
应用程序环境,且不能单独进行测试。可以使用包装机制来分隔依赖 Web 的部分,但这样需要增加一个级别的间接性。
测试考虑事项
因为Page Controller 依赖于 Web 应用程序框架的具体细节(例如,查询字符串和
HTTP 头),因此不能在 Web 框架之外对控制器类进行实例化和测试操作。如果需要对控制器类运行一组自动单元测试,则每次测试时都必须启动
Web 应用程序。然后必须使用可执行所需功能的格式提交 HTTP 请求。此配置为测试带来许多依赖性和不良影响。要提高可测试性,请考虑将业务逻辑(包括变得更加复杂的控制器逻辑)与依赖
Web 的代码分隔开来。
相关模式
有关详细信息,请参阅以下相关模式:
-
Intercepting Filter。此模式是在 Web 应用程序内部实现重复功能的另一结构。该
Web 服务器框架可以使每个请求先通过可配置筛选器链,然后将其传递给控制器。筛选器通常处理解码、验证和会话管理等较低级别的功能,而
Page Controller 处理应用程序功能。而且,筛选器通常不是特定于页面的。
-
Front Controller。此模式更为复杂,但作为 Page Controller
的替代模式其功能也更为强大。Front Controller 为所有页面请求定义同一个控制器,因此能够作出跨页的导航决定。
-
Model-View-Controller。Page Controller 是
MVC 控制器部分的实现变体。
致谢
- [Fowler03] Fowler, Martin. Patterns of Enterprise
Application Architecture. Addison-Wesley,
2003.
- [Gamma95] Gamma, Helm, Johnson, and Vlissides. Design
Patterns: Element of Reusable Object-Oriented Software.
Addison-Wesley, 1995.
|