.NET Web應用程式框架構建模式

來源:互聯網
上載者:User
[簡介]

  本文對應於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 Form設計器所必需的。
//
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實現,在實際項目中,商務邏輯遠遠不止這樣,但是上述的代碼展現了一個基本的模型,在增加了代碼和複雜度的同時也帶來了顯而易見的好處,如:模組依賴的降低、代碼重複的減少、職責和問題的分離、代碼的可測試性等等。

  到現在您是否決定了使用Model-View-Controller (MVC)模式來將動態Web應用程式的使用者介面組件與商務邏輯分隔開來,要構建的應用程式將以動態方式構造網頁,但是目前的頁面導航都是基於靜態導航的形式。
在更加複雜的應用系統中,如何考慮儘可能避免導航代碼的重複,甚至考慮基於可配置的規則來動態確定頁面導航,那麼Page Controller和Front Controller某種意義來說是對於MVC模式在更加複雜的系統的最佳化。

  中等複雜程度的最佳化—Page Controller(頁面控制器)

  使用Page Controller模式接受來自頁面請求的輸入、調用請求對模型執行的操作以及確定應用於結果頁面的正確視圖。分隔調度邏輯和所有視圖相關代碼。如果合適,建立用於所有頁面控制器的公用基類,以避免代碼重複並提高一致性和可測試性。圖5顯示了頁面控制器與模型和視圖的關係。


圖6、使用BaseController消除代碼重複

  頁面控制器可接收頁面請求、提取所有相關資料、調用對模型的所有更新以及向視圖轉寄請求。而視圖又將根據該模型檢索要顯示的資料。定義獨立頁面控制器將分隔模型與Web請求細節(例如會話管理,或使用查詢字串或隱藏表單域向頁面傳遞參數)。按照這種基本形式,為Web應用程式中的每個連結建立控制器。控制器因而將變得非常簡單,因為每次僅須考慮一個操作。

  為每個網頁(或操作)建立獨立控制器可能會導致大量代碼重複。因此應該建立BaseController類以合并驗證參數(請參閱圖6)等公用函數。每個獨立頁面控制器都可以從BaseController繼承此公用功能。除了從公用基類繼承之外,還可以定義一組協助器類,控制器可以調用這些類來執行公用功能。

  大多數情況下,頁面控制器取決於基於HTTP的Web請求的具體細節。因此,頁面控制器代碼通常包含對HTTP頭、查詢字串、表單域、多部分表單請求等的引用。因此在Web應用程式架構之外測試控制器代碼非常困難。唯一方法是通過類比HTTP請求和分析結果來測試控制器。這種類型的測試既費時且易出錯。因此,要提高可測試性,可以將依賴Web的代碼和不依賴Web的代碼分別放入兩個單獨類中(請參閱圖7),這是Page Controller的變體實現。


圖7、分隔依賴Web的代碼和不依賴Web的代碼

  Page Controller是大多數動態Web應用程式的預設實現方式,它簡單,Web應用程式框架內建,可擴充性及其開發人員責任的分隔等等優點使其在Web開發得到廣泛的應用,不過每頁面一控制器、較深的繼承樹和對於Web架構的依賴等等也是其比較明顯的限制。

  高度複雜的最佳化

  —Front Controller(前端控制器)

  Front Controller通過讓單個控制器負責傳輸所有請求,從而解決了在Page Controller中存在的分散化問題。控制器本身通常分為以下兩部分實現:處理常式和命令階層(見圖8)。


圖8、Front Controller結構

  處理常式具有以下兩項職責:

  1) 檢索參數。處理常式接收來自Web伺服器的HTTP Post或Get請求,並從請求中檢索相關參數。

  2) 選擇命令。處理常式首先使用請求中的參數選擇正確的命令,然後將控制權轉移給該命令以便執行處理。


圖9、Front Controller的典型方案

  在前端控制器中,所有請求都通過單個(通常是兩組件)控制器來傳送。控制器的第一個組件是處理常式,第二個組件是Commands(命令)[Gof95]的層次。命令本身是控制器的一部分,代表控制器觸發的特定操作。在執行該操作之後,命令選擇要使用哪個視圖來呈現頁面。通常,構建的此控制器架構使用設定檔將請求映射到操作,因此,它在構建之後便於更改。當然,其缺點在於這種設計固有的複雜程度。

  遺漏之後的補充

  到目前為止,已經完整地展示了Web表示模式中MVC的實現架構,同時在此技術上對於中等複雜和高度複雜的情況提出了Page Controller和Front Controller作為最佳化的手段,在構建Web表示的應用程式,已經給出了相對完整的解決方案,細心的讀者是否發現,是不是還少了一點什嗎?在構建Web應用程式的時候,除了構建一個“良構”的系統(這表現在MVC的分離和系統的可擴充),我們還需要考慮應用程式的安全和效能,因此我們選取了Interceptiing Filter和Page Cache作為Web表示模式的補充內容。

  Intercepting Filter(篩選器)

  如何圍繞Web頁面請求來實現公用的預先處理和後處理步驟?這是篩選器需要解決的問題,使用此模式,您建立一串可組合的篩選器,以便在Web頁面請求期間實現公用的預先處理和後處理任務。


圖10、一串可組合篩選器

  篩選器構成了一系列獨立模組,在將頁面請求傳遞到控制器對象之前,這些模組可以連結在一起以執行一組公用的處理步驟。因為各個篩選器實現的是完全相同的介面,所以它們彼此之間沒有顯式依賴性。因此,可以在不會影響現有篩選器的情況下添加新的篩選器。您甚至可以在部署時添加篩選器,方法是基於設定檔動態地對它們進行執行個體化。

  對各個篩選器的設計應該儘可能讓它們不對是否存在其他篩選器作出任何假設。這樣可以維護可組合性,即添加、刪除或重新排資料行篩選的能力。此外,某些實現Intercepting Filter模式的架構不會保證篩選器的執行順序。如果發現多個篩選器之間存在很強的相互依賴性,最好採取調用協助器類的常規方法,因為這樣可以保證保留篩選器序列中的約束資訊。Intercepting Filter的直接實現方式是一個篩選器鏈,這個篩選器鏈可以用來遍曆一個由所有篩選器組成的列表。Web請求處理常式在將控制權傳遞到應用程式邏輯之前將首先執行篩選器鏈。


圖11、Intercepting Filter類別關係圖

  當Web伺服器收到頁面請求時,請求處理常式首先將控制權傳遞給FilterChain(篩選器鏈)對象。此對象維護著一個包含所有篩選器的列表,並按順序調用每個篩選器。FilterChain可以從設定檔中讀取篩選器順序,以實現部署時的可組合性。每個篩選器都有機會修改傳入請求。例如,它可以修改URL,或添加應用程式要使用的頭欄位。執行完所有篩選器之後,"請求處理常式"將控制權傳遞給控制器,後者將執行應用程式功能(見圖12)。


圖12、Intercepting Filter序列關係圖

  因為在處理Web請求時通常需要有截取篩選器,所以大多數Web架構都為應用程式開發人員提供了將截取篩選器掛靠到要求-回應過程中的機制。

  Page Cache(頁面緩衝)

  如果動態產生的Web頁被頻繁請求並且構建時需要耗用大量的系統資源,那麼,如何才能改進這類網頁的回應時間?頁面緩衝通過對從動態網頁產生的內容進行緩衝來提高請求響應的輸送量。預設情況下,在ASP.NET中支援頁面緩衝,但除非定義有效到期策略,否則,不會對來自任何給定響應的輸出進行緩衝。要定義到期策略,可以使用低級OutputCache API或進階@OutputCache指令。

  頁面緩衝的基本結構是相對簡單的。Web伺服器維護包含預先產生的頁面的本機資料儲存(見圖13)。


圖13、頁面緩衝的基本配置

  下面的順序圖表闡明了頁面緩衝可以改進效能的原因。第一個順序圖表(見圖2)描述尚未緩衝所需頁面的初始狀態(即所謂的緩衝未命中)。在這種情況下,Web伺服器必須訪問資料庫,並產生HTML頁,再將其儲存在緩衝中,然後將它返回給用戶端瀏覽器。請注意,此過程比不進行緩衝的情況稍慢,因為它執行了下列額外步驟:

  1. 確定頁面是否已緩衝

  2. 將頁面轉換為HTML代碼,然後儲存在緩衝中

  與資料庫訪問和HTML產生相比,其中的任一步驟都不應該花費很長時間。但是,因為在此情況下需要進行額外處理,所以您必須確保在系統完成與緩衝未命中關聯的步驟之後連續多次命中緩衝(14所示)。


圖14、緩衝未命中的序列(當頁面不在緩衝中時)

  在圖15所示的快取命中情況下,頁面已經處於緩衝中。通過跳過資料庫訪問、頁面產生和頁面儲存,快取命中節省了迴圈。


圖15、快取命中的序列(頁面處於緩衝中時)

  在ASP.NET中可以採用下述三種模式來實現緩衝策略:

  1.將<%@ OutputCache Duration="60" VaryByParam="none" %>這樣的指令插入到要緩衝的每個頁面中。指令指定重新整理間隔(以秒為單位)。重新整理間隔不依賴於外來事件,而且緩衝不能全部重新整理。

  2.Vary-By-Parameter Caching.此模式使用Absolute Expiration的變型,該變型使開發人員能夠指定影響頁面內容的參數。因此,緩衝將儲存頁面的多個版本,並按參數值為這些頁面版本編製索引。

  3.Sliding Expiration Caching.此模式與Absolute Expiration(絕對到期)的類似之處是,頁面在指定的時間內是有效。但是,在每次請求時都會重設重新整理間隔。例如,您可能使用滑動到期緩衝,將一個頁面緩衝最長10分鐘。只要對頁面的請求是在10分鐘內發出的,就將到期時間再延長10分鐘。

  結束語

  自此,我們已經完整地介紹了Web表示叢集中相關的各個模式,關於在MVC模式中Observer(觀察者)模式的用法,我們會用專門的篇幅來介紹,文字中提到的一些引用可以在http://www.microsoft.com/china/msdn/architecture/patterns/EspBiblio 找到相對詳細的列表,當然,網路就是您最好的資源。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.