使用Micrisoft.net设计方案 第三章Web表示模式 Web模式集群详细介绍 Front Controller(前端控制器)

已经决定使用Model-View-Controller (MVC) 模式将动态 Web 应用程序的用户界面逻辑与业务逻辑分隔开来。您已经考察了Page Controller模式,但您的页面控制器类具有复杂的逻辑,并且是较深的继承层次结构的一部分,或者,您的应用程序是基于可配置的规则来动态确定页面导航的。

如何为非常复杂的 Web 应用程序构建最佳的控制器结构,以便在避免代码重复的同时实现重用性和灵活性?

下面是适用于 Front Controller 模式的、由 Model-View-Controller 带来的各种具体的影响因素:

1、如果在系统的不同视图内复制公共逻辑,则需要集中此逻辑才能减少代码重复量。删除重复的代码是改进系统的总体可维护性的关键。

2、数据检索最好也集中在一个位置进行处理。一个好的示例是,让一系列视图使用数据库中的相同数据。与让每个视图检索数据并重复数据库访问代码相比,在一个位置实现对此数据的检索是更好的做法。

如 MVC 中所述,测试用户界面代码往往是耗时而乏味的。通过区分单各自的角色,可以提高总体可测试性。这不仅适用于模型代码(在 MVC 中已说明),而且适用于控制器代码。

以下影响因素可能使您决定使用 Front Controller,而不是 Page Controller。

1、Page Controller 的一般实现方法涉及为各个页面所共享的行为创建一个基类。但是,随着时间的推移,由于要增加非所有页面公用的代码,这些基类就会不断增大。若需要定期重构 此基类以确保其只包括公共行为,则需要制定规则。例如,您不希望由页面检查请求并决定(基于请求参数)是否将控制权转移给另一个页面,因为这种类型的决定 对于特定功能来说更具体,而不是所有页面共有的。

2、为了避免在基类中添加过多的条件逻辑,您会创建更深的继承层次结构以删除条件逻辑。例如,在具有三个功能区域的应用程序中,只使用一个包含应用 程序公共功能的基类可能是很有用的。每个功能区域可能还有另一个类,该类 继承总体应用程序的基类。乍一看,这种类型的结构是简单的,但它通常会导致非常脆弱的设计和实现,并给代码带来问题。

3、Page Controller 解决方案描述了每个逻辑页面使用一个对象。当需要跨多个页面对处理过程进行控制或协调时,此解决方案将不可行。例如,假定在 Web 应用程序中具有复杂的可配置导航(以 XML 格式存储)。当收到请求时,应用程序必须根据其当前状态查找下一步要前进到哪个位置。

4、由于Page Controller 是通过每个逻辑页面使用一个对象来实现的,因此,很难在 Web 应用程序的所有页面中一致地应用特定操作。例如,安全性最好以协调方式实现。让每个视图或页面控制器对象分别处理安全性是有问题的,因为它可以被不一致地 应用,并导致安全问题。此问题的其他解决方案还将在Intercepting Filter 中进行讨论。

5、对于 Web 应用程序来说,URL 与特定控制器对象的关联可以是强制性的。例如,假定您的站点具有类似向导的界面用于收集信息。此向导包括许多必备页面和许多基于用户输入的可选页面。在使 用 Page Controller 实现时,必须使用基类中的条件逻辑来实现可选页面,才能选择下一页面。

解决方案

Front Controller 通过让单个控制器负责传输所有请求,从而解决了在 Page Controller 中存在的分散化问题。控制器本身通常分为以下两部分实现:处理程序和命令层次结构(见图 1)。

图 1:Front Controller 结构

处理程序具有以下两项职责:

  • 检索参数。处理程序接收来自 Web 服务器的 HTTP Post 或 Get 请求,并从请求中检索相关参数。

  • 选择命令。处理程序首先使用请求中的参数选择正确的命令,然后将控制权转移给该命令以便执行处理。

图 2 显示这两项职责。

图 2:Front Controller 的典型方案

命令本身也是控制器的一部分。命令代表具体的操作,这在 Command 模式中有相应的介绍。通过将命令表示为单独的对象,控制器可以按一般方式与所有命令交互,这与调用公共命令类上的特定方法相反。在命令对象完成操作之后,将由命令选择使用哪个视图来显示页面。

Front Controller 模式具有下列优缺点:

优点

  • 集中化控制。 Front Controller 用于协调向 Web 应用程序发出的所有请求。此解决方案描述了使用单一控制器,而不是 Page Controller 中所用的分布式模型。此单一控制器处于很好的位置来实施全应用程序范围的策略,如安全性和使用情况跟踪。

  • 线程安全。由于每个请求都涉及创建新的命令对象,因此命令对象本身不需要是线程安全的。这意味着,命令类中避免了线程安全问题。但是,这并不意味着您可以完全避免线程问题,因为命令所作用的代码(即模型代码)仍然必须是线程安全的

  • 可配置性。只需要在 Web 服务器中配置一个前端控制器;处理程序执行其余的调度。这简化了 Web 服务器的配置。一些 Web 服务器是很难配置的。

缺点

  • 性能考虑事项。 Front Controller 是用来处理对 Web 应用程序的所有请求的单个控制器。在这两部分中,应该仔细检查处理程序中是否有性能问题,因为处理程序将确定负责执行请求的命令的类型。如果处理程序必须 执行数据库查询或 XML 文档查询才能作出决定,则可能导致性能非常缓慢。

  • 增加了复杂性。 Front Controller 比 Page Controller 更复杂。它通常涉及将内置控制器替换为自定义的 Front Controller。实现此解决方案会增加维护成本和新手的学习难度。

测试考虑事项

从视图中删除业务逻辑简化了视图的测试难度,因为此后可以在独立于控制器的情况下测试视图。

ASP.NET中使用HTTPHandler实现Front Controller

实现策略

Front Controller 通常分为两个部分来实现。Handler 对象从 Web 服务器接收各个请求(HTTP Get 和 Post),并检索相关参数,然后根据参数选择适当的命令。控制器的第二个部分是 Command Processor,该部分执行特定操作或命令来满足请求。命令完成后转到视图,以便显示页面。

注意:此实现策略解决了前面的示例中出现的问题。虽然此示例可能不足以证明对 Front Controller 的更改是合理的,但它说明了为什么会使用 Front Controller ,并且该实现解决了这种复杂性高得多的问题。另外,与大多数实现一样,实现此模式的方式不止一种,这只是其中的一个选择。

处理程序

ASP.NET 提供低级请求/响应 API 来处理传入的 HTTP 请求。ASP.NET 所接收的每个传入 HTTP 请求最终由实现 IHTTPHandler 接口的类的具体实例来处理。这种低级 API 非常适用于实现 Front Controller 的处理程序部分。

图 1 显示了控制器的处理程序部分的结构。

图 1 Front Controller 的处理程序部分

此解决方案完美地划分了职责。Handler 类负责处理各个 Web 请求,并将确定正确的 Command 对象这一职责委派给 CommandFactory 类。当 CommandFactory 返回 Command 对象后,Handler 将调用 Command 上的 Execute 方法来执行请求。

Handler.cs

下面的代码示例显示了如何实现 Handler 类:

using System;

using System.Web;

public class Handler : IHttpHandler

{

   public void ProcessRequest(HttpContext context) 

   {

      Command command = 

         CommandFactory.Make(context.Request.Params);

      command.Execute(context);

   }

   public bool IsReusable 

   { 

      get { return true;} 

   }

}

Command.cs

Command 类是 Command 模式的一个示例。Command 模式在此解决方案中非常有用,因为您不希望 Handler 类直接依赖于命令。一般来说,可以从 CommandFactory 返回命令对象。

using System;

using System.Web;

public interface Command

{

   void Execute(HttpContext context);

}

CommandFactory.cs

CommandFactory 类对于实现至关重要。它根据查询字符串中的参数来判断将创建哪个命令。在此示例中,如果 site 查询参数被设置为 micro 或根本没有设置,工厂将创建 MicroSite 命令对象。如果 site 被设置为 macro,工厂将创建 MacroSite 命令对象。如果该值被设置为任何其他值,工厂将返回 UnknownCommand 对象,以便进行默认错误处理。这是 Special Case 模式的一个示例。

using System;

using System.Collections.Specialized;

public class CommandFactory

{

   public static Command Make(NameValueCollection parms)

   {

      string siteName = parms["site"];

      Command command = new UnknownCommand();

      if(siteName == null || siteName.Equals("micro"))

         command = new MicroSite();

      else if(siteName.Equals("macro"))

         command = new MacroSite();

      return command;

   }

}

配置处理程序

HTTP 处理程序在 ASP.NET 配置中被声明为 web.config 文件。ASP.NET 定义了一个可以在其中添加和删除处理程序的 <httphandlers> 配置段。例如,ASP.NET 将 Page*.aspx 文件的所有请求映射到应用程序的 web.config 文件中的 Handler 类:

<httpHandlers>

   <add verb="*" path="Page*.aspx" type="Handler,FrontController" />

</httpHandlers>

命令

命令代表了网站中的可变性。在此示例中,从每个站点的数据库中检索数据的功能包含在它自己的类中,并且该类是从名为 RedirectingCommand 的基类继承而来的。RedirectingCommand 类实现了 Command 接口。调用 RedirectingCommand 类的 Execute 时,它首先调用名为 OnExecute 的抽象方法,然后转到视图。该特定视图是从名为 UrlMap 的类检索而来的。UrlMap 类从应用程序的 web.config 文件中检索映射关系。图 2 显示了该解决方案的命令部分的结构。

图 2 front controller 的命令部分

RedirectingCommand.cs

RedirectingCommand 是一个抽象基类,它调用名为 OnExecute 的抽象方法来执行特定命令,然后转到从 UrlMap检索到的视图。

using System;
using System.Web;
public abstract class RedirectingCommand : Command
{
   private UrlMap map = UrlMap.SoleInstance;
   protected abstract void OnExecute(HttpContext context);
   public void Execute(HttpContext context)
   {
      OnExecute(context);
      string url = String.Format("{0}?{1}",
         map.Map[context.Request.Url.AbsolutePath],
         context.Request.Url.Query);
      context.Server.Transfer(url);
   }
}
UrlMap.cs
UrlMap 类从应用程序的 web.config 文件加载配置信息。配置信息将所请求的 URL 的绝对路径关联到该文件所指定的另一个 URL。这样,就可以更改当请求外部页面时要将用户转到哪个实际页面。这个过程为更改视图提供了很高的灵活性,因为用户永远不会引用实际页面。下面是 UrlMap 类:
using System;
using System.Web;
using System.Xml;
using System.Configuration;
using System.Collections.Specialized;
public class UrlMap : IConfigurationSectionHandler
{
   private readonly NameValueCollection _commands = new NameValueCollection();
   public const string SECTION_NAME="controller.mapping";
   public static UrlMap SoleInstance
   {
      get {return (UrlMap) ConfigurationSettings.GetConfig(SECTION_NAME);}
   }
   object IConfigurationSectionHandler.Create(object parent,object configContext, XmlNode section)
   {
      return (object) new UrlMap(parent,configContext, section);
   }
   private UrlMap() {/*no-op*/}
   public UrlMap(object parent,object configContext, XmlNode section)
   {
      try
      {
         XmlElement entriesElement = section["entries"];
         foreach(XmlElement element in entriesElement)
         {
            _commands.Add(element.Attributes["key"].Value,element.Attributes["url"].Value);
         }
      }
      catch (Exception ex)
      {
         throw new ConfigurationException("Error while parsing configuration section.",ex,section);
      }
   }
   public NameValueCollection Map
   {
      get { return _commands; }
   }
}
下面的代码是从显示配置的 web.config 文件中摘录的:
<controller.mapping>
   <entries>
      <entry key="/patterns/frontc/3/Page1.aspx" url="ActualPage1.aspx" />
      <entry key="/patterns/frontc/3/Page2.aspx" url="ActualPage2.aspx" />
   </entries>
</controller.mapping>

MicroSite.cs

MicroSite 类与此模式前面的 LoadMicroHeader 中的代码类似。主要区别是,无法再访问页面中包含的标签。而必须将信息添加到 HttpContext 对象。下面的示例显示了 MicroSite 代码:

using System;

using System.Web;

public class MicroSite : RedirectingCommand

{

   protected override void OnExecute(HttpContext context)

   {

      string name = context.User.Identity.Name;

      context.Items["address"] = 

         WebUsersDatabase.RetrieveAddress(name);

      context.Items["site"] = "Micro-Site";

   }

}

MacroSite.cs

MacroSite 类与 MicroSite 类似,但它使用的是不同的数据库网关类 MacroUsersDatabase。这两个类都将信息存储在传递进来的 HttpContext 中,以便让视图可以检索它。下面的示例显示了 MacroSite 代码:

using System;

using System.Web;

public class MacroSite : RedirectingCommand

{

   protected override void OnExecute(HttpContext context)

   {

      string name = context.User.Identity.Name;

      context.Items["address"] = 

         MacroUsersDatabase.RetrieveAddress(name);

      context.Items["site"] = "Macro-Site";

   }

}

WebUsersDatabase.cs

WebUsersDatabase 类负责从“webusers”数据库中检索电子邮件地址。它是 Table Data Gateway [Fowler03] 模式的一个示例。

using System;

using System.Data;

using System.Data.SqlClient;

public class WebUsersDatabase

{

   public static string RetrieveAddress(string name)

   {

      string address = null;

      String selectCmd = 

         String.Format("select * from webuser where (id = '{0}')",

         name);

      SqlConnection myConnection = 

         new SqlConnection("server=(local);database=webusers;Trusted_Connection=yes");

      SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);

      DataSet ds = new DataSet();

      myCommand.Fill(ds,"webuser");

      if(ds.Tables["webuser"].Rows.Count == 1)

      {

         DataRow row = ds.Tables["webuser"].Rows[0];

         address = row["address"].ToString();

      }

      return address;

   }

}

MacroUsersDatabase.cs

MacroUsersDatabase 类负责从“macrousers”数据库中检索电子邮件地址。它是 Table Data Gateway 模式的一个示例。

using System;

using System.Data;

using System.Data.SqlClient;

public class MacroUsersDatabase

{

   public static string RetrieveAddress(string name)

   {

      string address = null;

      String selectCmd = 

         String.Format("select * from customer where (id = '{0}')",

         name);

      SqlConnection myConnection = 

         new SqlConnection("server=(local);database=macrousers;Trusted_Connection=yes");

      SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);

      DataSet ds = new DataSet();

      myCommand.Fill(ds,"customer");

      if(ds.Tables["customer"].Rows.Count == 1)

      {

         DataRow row = ds.Tables["customer"].Rows[0];

         address = row["email"].ToString();

      }

      return address;

   }

}

视图

视图最后实现。“更改需求”中的示例视图负责根据用户访问哪个站点从数据库中检索信息,然后向用户显示所产生的页面。因为数据库访问代码已移到命令,所以视图现在从ttpContext 对象检索数据。图 3 显示了代码隐藏类的结构。

图 3 视图的代码隐藏类的结构

由于仍然存在公共行为,因此仍然需要 BasePage 类以避免代码重复。

BasePage.cs

与“更改需要”中的示例相比,BasePage 类已有大幅更改。它不再负责确定要加载哪个站点头信息。它只检索由命令存储在 HttpContext 对象中的数据,并将它们分配给适当的标签:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
public class BasePage : Page
{
   protected Label eMail;
   protected Label siteName;
   virtual protected void PageLoadEvent(object sender, System.EventArgs e)
   {}
   protected void Page_Load(object sender, System.EventArgs e)
   {
      if(!IsPostBack)
      {
         eMail.Text = (string)Context.Items["address"];
         siteName.Text = (string)Context.Items["site"];
         PageLoadEvent(sender, e);
      }
   }
   #region Web Form Designer generated code
   #endregion
}

ActualPage1.aspx.cs 和 ActualPage2.aspx

ActualPage1 和 ActualPage2 是针对具体页面的代码隐藏类。它们都是从 BasePage 继承而来的,以确保在屏幕的顶部填入头信息:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
public class ActualPage1 : BasePage
{
   protected System.Web.UI.WebControls.Label pageNumber;
   protected override void PageLoadEvent(object sender, System.EventArgs e)
   {
      pageNumber.Text = "1";
   }
   #region Web Form Designer generated code
   #endregion
}
using System;
using System.Web.UI.WebControls;
public class ActualPage2 : BasePage
{
   protected Label pageNumber;
   protected override void PageLoadEvent(object sender, System.EventArgs e)
   {
      pageNumber.Text = "2";

   }

  #region Web Form Designer generated code

   #endregion

}

在从 Page Controller 实现转移到 Front Controller 实现时,不必更改这些页面。

测试考虑事项

实现对 ASP.NET 运行库的依赖性使测试变得很困难。您无法将通过继承 System.Web.UI.Page 、 System.Web.UI.IHTTPHandler 或 ASP.NET 运行库中所包含的其他各种类而得到的类进行实例化。这就无法对应用程序的大多数组成部分分别进行单元测试。自动测试此实现的所选方法是,生成 HTTP 请求,然后检索 HTTP 响应,并确定响应是否正确。此方法容易产生错误,因为这是在将响应文本与预期文本进行比较。

CommandFixture.cs

对于可测试的实现来说,导致其可测试的一个因素是 CommandFactory,因为它是独立于 ASP.NET 运行库的。因此,您可以通过编写测试步骤来验证是否获得了正确的 Command 对象。下面是 CommandFactory 类的 NUnit (http://nunit.org) 测试:

using System;
using System.Collections.Specialized;
using NUnit.Framework;
[TestFixture]
public class CommandFixture
{
   private static readonly string microKey = "micro";
   private static readonly string macroKey = "macro";
   [SetUp]
   public void BuildCommandFactory()
   {
      NameValueCollection map = new NameValueCollection();
      map.Add(microKey, "MicroSite");
      map.Add(macroKey, "MacroSite");
   }
   [Test]
   public void DefaultToMicro()
   {
      NameValueCollection map = new NameValueCollection();
      Command command = CommandFactory.Make(map);
      Assertion.AssertNotNull(command);
      Assertion.Assert(command is MicroSite);
   }
   [Test]
   public void MicroSiteCommand()
   {
      NameValueCollection map = new NameValueCollection();
      map.Add("site", "micro");
      Command command = CommandFactory.Make(map);
      Assertion.AssertNotNull(command);
      Assertion.Assert(command is MicroSite);
   }
   [Test]
   public void MacroSiteCommand()
   {
      NameValueCollection map = new NameValueCollection();
      map.Add("site", "macro");
      Command command = CommandFactory.Make(map);
      Assertion.AssertNotNull(command);
      Assertion.Assert(command is MacroSite);
   }
   [Test]
   public void Error()
   {
      NameValueCollection map = new NameValueCollection();
      map.Add("site", "xyzcommand");
      Command command = CommandFactory.Make(map);
      Assertion.AssertNotNull(command);
      Assertion.Assert(command is UnknownCommand);
   }
}

可以通过进一步的工作来隔离 Command 类。Execute 方法的一个参数是 HttpContext 对象。您可以更改此参数,使该对象独立于 ASP.NET 环境。这样,您就可以在 ASP.NET 运行库之外对命令进行单元测试。

实现 Front Controller 增加了复杂性,并导致了许多优缺点:

优点

  • 提高了灵活性。该实现展示了如何通过 Handler 类集中和协调所有请求。Handler 使用 CommandFactory 来确定要执行的具体操作。这样,就可以在不更改 Handler 类的情况下修改和扩展功能。例如,要添加另一个站点,则必须创建特定命令,并且唯一必须更改的类是 CommandFactory。

  • 简化了视图。Page Controller 示例中的视图从数据库检索数据,然后产生页面。在 Front Controller 中,视图不必再依赖数据库,因为这项工作是由各个命令来完成的。

  • 可以扩展, 但不能修改。该实现为进行多种形式的调度提供了许多机会。例如,无论执行什么方法和对象,Handler 只调用 Command 对象的 Execute 方法。因此,您可以在不修改 Handler 的情况下添加额外的命令。通过用其他工厂代替 CommandFactory,可以对该实现进行进一步扩展。

  • URL 映射。UrlMap 允许让用户看不到实际的页面名。用户输入一个 URL,然后系统将使用 web.config 文件将它映射到特定的 URL。这可以让程序员有更大的灵活性,因为这样做可以获得 Page Controller 实现中所没有的一个间接操作层。

  • 线程安全。命令对象(MicroSite 和 MacroSite)是针对每个请求分别创建的。这意味着,您不必担心这些对象中的线程安全问题。

缺点

  • 性能降低。您必须检查是否有这样的可能。所有请求都是通过 Handler 对象处理的。它使用 CommandFactory 来确定要创建哪个命令。虽然在本示例中没有性能问题,但应该仔细检查这两个类,看看是否存在任何潜在的性能问题。

  • 其他方面的问题。该实现比 Page Controller 复杂得多。该实现的确提供了更多选择,但它的代价是复杂性和许多类。您必须权衡是否值得采用该实现。在您采用该实现并构建了框架后,可以很容易地添加新的命令和视图。不过,由于 Page Controller 是在 ASP.NET 中实现的,与在其他平台上相比,Front Controller 的实现不会同样多。

  • 测试考虑事项。由于 Front Controller 是在 ASP.NET 中实现的,因此很难单独测试。要提高可测试性,应该将要测试的功能从依赖于 ASP.NET 的代码中分离到不依赖于 ASP.NET 的类中。然后,您不必启动 ASP.NET 运行库就可以测试这些类。

  • 无效的 URL。因为 Front Controller 根据输入参数和应用程序的其他当前状态来决定要转到哪个视图,因此,URL 可能不会总是转到同一个页面。这样就会让用户无法保存 URL,也就无法随后再访问该页面。


你可能感兴趣的:(动态,控制器,应用程序,数据库访问,而且)