已經決定使用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,也就無法隨後再訪問該頁面。