使用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 Proce***equest(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,也就無法隨後再訪問該頁面。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章