第三章Web表示模式
體系結構設計者在設計第一個作品時比較精簡和干練。在第一次設計時,并清除自己做什么,因此比較小心謹慎。第二個作品是最危險的一個作品,此時他會對第一個作品做修飾和潤色,以及把第一次設計的邊緣性設計思想都用在第二個作品,結果導致設計過頭。
最初的Web很簡單,只是有幾個簡單的Html頁面組成,實現(xiàn)信息共享。隨著業(yè)務的發(fā)展,需要根據(jù)業(yè)務來決定顯示什么,于是開發(fā)了CGI編程,把大量的業(yè)務邏輯寫到CGI中,然后輸出到頁面。隨著發(fā)展,CGI編程模式受到了挑戰(zhàn),不能滿足發(fā)展的需求,于是開發(fā)了asp/jsp等葉面編程模型,這樣可能把業(yè)務邏輯通過腳本嵌入到Html頁面中,這樣大量重復的代碼充斥在系統(tǒng)中,導致難于維護。
在設計Web系統(tǒng)時有一個矛盾,那就是簡單和復雜的沖突。我們在設計系統(tǒng)中力求簡單和干練。但為了實現(xiàn)重用以及隨著業(yè)務復雜性的增加,會不可避免的應用復雜設計方式。如何平衡這個二者呢?有一個辦法可以幫組我們衡量,那就是這種引入的復雜性是為了滿足當前需求呢,還是為了滿足將來需求呢?如果是前者那就可以增加復雜性,這種是合理的。如果是為了將來預留的,那就是設計過頭。因此在設計時,要了解關鍵Web程序設計問題、可能的解決方案以及利弊。
模式概述
此模式集群直接從長期存在的MVC開始,自從業(yè)務邏輯和表示邏輯分開后,該設計模式經久考驗。該模式最初是在設計階段編寫,最后被微軟映射到了asp.net MVC平臺上。
在用 Microsoft® ASP.NET 實現(xiàn) MVC 時,是從一個簡單系統(tǒng)示例作為起點的:編寫在一個頁面上,而且將應用程序邏輯嵌入表示元素中。隨著系統(tǒng)越來越復雜,使用了 ASP.NET 的代碼隱藏功能來將表示代碼(視圖)與模型控制器代碼分開。這始終能夠很好地工作,直至要求迫使您不得不考慮在沒有控制器的情況下重復使用模型代碼以避免 應用程序冗余。此時,需要創(chuàng)建獨立模型以抽象化業(yè)務邏輯,并使用代碼隱藏功能使模型適應視圖代碼。然后,實現(xiàn)過程結束對該 MVC 方法測試含義的討論。
迄今為止,使用 Model-View-Controller 模式時一直強調模型和視圖;控制器扮演相對次要的角色。實際上,在此模式中工作的控制器是 ASP.NET 中的隱式控制器。它負責感知用戶事件(請求和回發(fā)),并將這些事件寫入適當?shù)南到y(tǒng)響應。
在 基于 Web 的動態(tài)應用程序中,在每次進行頁請求時都會重復許多共同任務,如用戶驗證、確認、提取請求參數(shù)和查找與表示相關的數(shù)據(jù)庫。如果不對這些任務進行管理,則會很快導致不必要的代碼重復。因為這些任務與感知用戶事件和確定正確的系統(tǒng)響應完全相關,所以用來放置該行為的邏輯位置是在控制器中。
功能更強大的控制
此群集中的下一個模式是 Page Controller (頁面控制器),該模式是對 Model-View-Controller 的優(yōu)化,并且能夠滿足下一個級別的復雜程度。此模式在頁面范圍中使用一個控制器,接受來自頁請求的輸入,針對模型調用請求的操作,然后確定要用作結果頁面的正確視圖。重復邏輯(如確認)被移到了基本控制器類中。
使用 ASP.NET 實現(xiàn) Page Controller 用常見的外觀示例闡述了 ASP.NET 內置的頁面控制器的強大功能。實現(xiàn)過程還結合使用 Template Method(模板方法)模式與 Page Controller(頁面控制器)模式來定義操作中的算法框架,并將其中的一些步驟交由子類負責
隨 著應用程序越來越復雜,頁面控制器最終在基類中累積了大量邏輯,此問題通常通過加深頁面控制器的繼承層次結構來解決。如果應用程序十分復雜,這兩種因素都 會導致代碼難以維護和擴展。同樣,某些應用程序需要動態(tài)配置導航圖,這有可能跨越多個頁面控制器。達到這種復雜程度時,應該考慮 Front Controller(前端控制器)。
Front Controller , 是該目錄中的下一個模式,它也是對 Model-View-Controller 的優(yōu)化。在前端控制器中,所有請求都通過單個(通常是兩部件)控制器來傳送。控制器的第一個部件是處理程序,第二個部件是 Commands(命 令)的層次。命令本身是控制器的一部分,代表控制器觸發(fā)的特定操作。在執(zhí)行該操作之后,命令選擇要使用哪個視圖來呈現(xiàn)頁面。通常,構建的此控制器框架使用配置 文件將請求映射到操作,因此,它在構建之后便于更改。當然,其缺點在于這種設計固有的復雜程度。
篩選器和緩存
此群集中的最后兩種模式涉及到篩選器和緩存。
I ntercepting Filter (截取篩選器)為以下問題提供了解決方案:如何針對 HTTP 請求實現(xiàn)常見的預處理和后處理。在 Intercepting Filter 中,最適合執(zhí)行非應用程序特定的常見任務,如安全性檢查、日志記錄、壓縮、編碼和解碼。 Intercepting Filter 通常涉及到執(zhí)行某個特定任務。在針對 HTTP 請求執(zhí)行多項任務時,多個篩選器會鏈接到一起。在 ASP.NET 中使用 HTTP 模塊實現(xiàn) Intercepting Filter 強調可在 ASP.NET 中方便地實現(xiàn)此模式。
Page Cache (頁面緩存)通過保留創(chuàng)建成本極高的常用動態(tài)網頁的副本來滿足 Web 應用程序日益增加的可伸縮性和性能要求。在最初創(chuàng)建該頁面之后,會發(fā)送一份副本以便響應以后的請求。Page Cache 還討論幾個關鍵的緩存設計因素,如緩存刷新、數(shù)據(jù)刷新和緩存粒度。在 ASP.NET 中使用絕對過期實現(xiàn) Page Cache 闡述了 ASP.NET 內置的頁面緩存功能。
PageController(頁面控制器)
上下文
您已經決定使用 Model-View-Controller (MVC) 模式來將動態(tài) Web 應用程序的用戶界面組件與業(yè)務邏輯分隔開來。要構建的應用程序將以動態(tài)方式構造網頁,但網頁間導航多為靜態(tài)導航。
問題
如何以最佳方式為適度復雜的 Web 應用程序構建控制器,從而既能避免代碼重復,又能實現(xiàn)重用性和靈活性?
影響因素
以下因素影響這種情況中的系統(tǒng),在考慮上述問題解決方案時必須協(xié)調這些因素:
MVC 模式通常主要關注模型與視圖之間的分隔,而對于控制器的關注較少。在許多胖客戶端方案中,控制器和視圖之間的分隔相對次要,因此通常會被忽略 [Fowler03]。但在瘦客戶端應用程序中,視圖和控制器本來就是分隔的,這是因為顯示是在客戶端瀏覽器中進行的,而控制器是服務器端應用程序的一部 分。因此有必要對控制器進行更為仔細的研究。
在動態(tài) Web 應用程序中,多用戶操作可以導致不同的控制器邏輯,然后顯示相同頁面。例如,在基于 Web 的簡單電子郵件應用程序中,發(fā)送郵件和從收件箱中刪除郵件這兩個操作都可能將用戶返回(刷新后的)收件箱頁面。雖然這兩種活動完成之后顯示相同頁面,但應 用程序必須根據(jù)上一頁面以及用戶所單擊的按鈕來執(zhí)行不同的操作。
顯示大多數(shù)動態(tài)網頁的代碼都包括非常相似的步驟:驗證用戶身份、從查詢字符串或表單域中提取頁面參數(shù)、收集會話信息、從數(shù)據(jù)源檢索數(shù)據(jù)、生成頁面動態(tài)部分以及添加適用的頁眉和頁腳。這可能導致大量的代碼重復。
腳 本化服務器頁面(例如 ASP.NET)可能很容易創(chuàng)建,但在應用程序不斷增大時可能帶來一些缺點。腳本化頁面不能較好地分隔控制器和視圖,因而降低了重用的可能性。例如,如果 多個操作將導致相同頁面,在多個控制器之間重用顯示代碼則會比較困難,這是因為顯示代碼與控制器代碼混合在一起。對散布于業(yè)務邏輯和顯示邏輯之間的腳本化 服務器頁面也更加難以進行測試和調試。最后,開發(fā)腳本化服務器頁面要求同時精通開發(fā)業(yè)務邏輯和制作美觀高效的 HTML 頁面,而很少有人兼?zhèn)溥@兩項技能。基于上述考慮,因此有必要最大程度地減少腳本化服務器頁面代碼,而在實際類中開發(fā)業(yè)務邏輯。
正如 MVC 模式中的相關敘述,測試用戶界面代碼往往耗時而單調。如果可以分隔用戶界面專用代碼和實際業(yè)務邏輯,測試業(yè)務邏輯則會更為簡單,且可重復性更強。對于顯示部分和應用程序控制器部分都是如此。
通用外觀和導航結構往往可以提高 Web 應用程序的可用性和品牌認知度。但通用外觀可能會導致顯示代碼重復,特別是在腳本化服務器頁面中嵌入代碼時。因此,需要一種機制以提高頁面間顯示邏輯的重用性。
解決方案
使用 Page Controller 模式接受來自頁面請求的輸入、調用請求對模型執(zhí)行的操作以及確定應用于結果頁面的正確視圖。分隔調度邏輯和所有視圖相關代碼。如果合適,創(chuàng)建用于所有頁面控制器的公用基類,以避免代碼重復并提高一致性和可測試性。圖 1 顯示了頁面控制器與模型和視圖的關系。
頁面控制器可接收頁面請求、提取所有相關數(shù)據(jù)、調用對模型的所有更新以及向視圖轉發(fā)請求。而視圖又將根據(jù)該模型檢索要顯示的數(shù)據(jù)。定義獨立頁面控制 器將分隔模型與 Web 請求細節(jié)(例如會話管理,或使用查詢字符串或隱藏表單域向頁面?zhèn)鬟f參數(shù))。按照這種基本形式,為 Web 應用程序中的每個鏈接創(chuàng)建控制器。控制器因而將變得非常簡單,因為每次僅須考慮一個操作。
為每個網頁(或操作)創(chuàng)建獨立控制器可能會導致大量代碼重復。因此應該創(chuàng)建 BaseController 類以合并驗證參數(shù)(請參閱圖 2)等公用函數(shù)。每個獨立頁面控制器都可以從 BaseController 繼承此公用功能。除了從公用基類繼承之外,還可以定義一組幫助器類,控制器可以調用這些類來執(zhí)行公用功能。
如果多數(shù)頁面相似,并且可以將公用功能放入一個基類,則此方法非常有效。頁面變化越多,必須插入繼承樹的級別也就越多。比如,所有頁面都分析參數(shù),但只有顯示列表的頁面才從數(shù)據(jù)庫檢索數(shù)據(jù),而需要輸入數(shù)據(jù)的頁面則會更新模型而不檢索數(shù)據(jù)。現(xiàn)在可以引入兩個新基類,即 ListController 和 DataEntryController,這兩個類都是繼承 BaseController 而得到的。然后列表頁可以從 ListController 繼承,而數(shù)據(jù)輸入頁則可以從 DataEntryController 繼 承。雖然這種方法在這個簡單示例中非常有效,但在處理實際業(yè)務應用時,繼承樹可能很深且非常復雜。您可能希望向基類中添加條件邏輯,以適應某些變體,但如 此操作將違反封裝原則,基類也會因此在更改系統(tǒng)時造成較大麻煩。因此在應用程序變得更為復雜時,應當考慮使用幫助器類或者 Front Controller 模式。
因為很多時候都需要對 Web 應用程序使用頁面控制器,因此多數(shù) Web 應用程序框架都默認實現(xiàn)頁面控制器。大多數(shù)框架以服務器頁面的形式包含了頁面控制器(例如 ASP、JSP 和 php)。服務器頁面實際上組合了視圖和控制器的功能,但沒有提供顯示代碼與控制器代碼之間的相應分隔。遺憾的是,對于有些框架,混合視圖相關代碼與控制 器相關代碼很輕松,但要正確分隔控制器邏輯卻很困難。因此,Page Controller 方式在很多開發(fā)人員中口碑不佳。現(xiàn)在很多開發(fā)人員都將 Page Controller 與較差設計聯(lián)系在一起,而將 Front Controller 與較好設計聯(lián)系在一起。實際上,這種感覺是由于具體的實現(xiàn)在不完善的情況下造成的;Page Controller 和 Front Controller 都是可行性極佳的體系結構選擇。
因此,最好將控制器邏輯單獨放入可以從服務器頁面調用的獨立類。ASP.NET 頁面框架提供了可以實現(xiàn)這種分隔的完善機制,這種機制稱為"代碼隱藏類"。
變體
大多數(shù)情況下,頁面控制器取決于基于 HTTP 的 Web 請求的具體細節(jié)。因此,頁面控制器代碼通常包含對 HTTP 頭、查詢字符串、表單域、多部分表單請求等的引用。因此在 Web 應用程序框架之外測試控制器代碼非常困難。唯一方法是通過模擬 HTTP 請求和分析結果來測試控制器。這種類型的測試既費時且易出錯。因此,要提高可測試性,可以將依賴 Web 的代碼和不依賴 Web 的代碼分別放入兩個單獨類中(請參閱圖 3)。
在此示例中,AspNetController 封裝了在應用程序框架 (ASP.NET) 上的所有依賴項。例如,它可以提取來自 Web 請求的所有傳入參數(shù),并使用獨立于 Web 界面的方式(例如,使用集合)將其傳遞至 BaseController。此方法不僅可提高可測試性,而且允許通過其他用戶界面重用該控制器代碼,例如胖客戶端界面或自定義腳本語言。
此方法的缺點在于增加了開銷。現(xiàn)在新增了一個類,并且在處理每個請求前必須首先對其進行轉換。因此,應盡可能控制器受環(huán)境影響的部分,并權衡選擇降低依賴性與提高開發(fā)效率及執(zhí)行效率。
結果上下文
使用 Page Controller 模式存在下列優(yōu)缺點。
優(yōu)點
簡單性。由于每個動態(tài)網頁由特定控制器處理,所以這些控制器僅需進行有限范圍的處理,從而可以保持簡單性。由于每個頁面控制器僅處理一個網頁,所以 Page Controller 尤其適用于導航結構簡單的 Web 應用程序。
內置框架功能。在 其多數(shù)基本形式下,控制器已經置入大多數(shù) Web 應用程序平臺。例如,如果用戶單擊網頁中指向由 ASP.NET 腳本生成的動態(tài)頁面的鏈接,則 Web 服務器將分析與該鏈接關聯(lián)的 URL,并執(zhí)行相關 ASP.NET 頁面。實際上,這些 ASP.NET 頁面是用戶所執(zhí)行操作的控制器。ASP.NET 頁面框架還提供了用于執(zhí)行控制器代碼的代碼隱藏類。代碼隱藏類提供了控制器和視圖之間的更好分隔,并且允許創(chuàng)建合并所有控制器公用的功能控制器基類。有關 示例,請參閱"在 ASP.NET 中實現(xiàn) Page Controller"。
增強型重用性。創(chuàng)建控制器基類可以減少代碼重復,并允許在不同的頁面控制器重用公用代碼。可以通過在基類中實現(xiàn)重復邏輯來重用代碼。然后,所有具體的 Page Controller 對象將自動繼承此邏輯。如果該邏輯的實現(xiàn)對于不同頁面而有所不同,則仍可以在基類中使用 Template Method,并實現(xiàn)基本的執(zhí)行結構;具體子步驟的實現(xiàn)可能因頁面的不同而有所變化。
可擴展性。通過使用幫助器類,可以很簡便地擴展頁面控制器。如果控制器中的邏輯過于復雜,則可以向幫助器類委派部分邏輯。除了繼承之外,幫助器類還提供了另一種重用機制。
開發(fā)人員責任的分隔。使用 Page Controller 類有助于分離開發(fā)隊伍中各成員的責任。控制器開發(fā)人員必須熟悉由應用程序所實現(xiàn)的域模型和業(yè)務邏輯。另一方面,視圖設計者可以專注于結果的顯示風格。
缺點
由于其簡單性,Page Controller 是大多數(shù)動態(tài) Web 應用程序的默認實現(xiàn)方式。但是應該了解下列限制:
每個頁面一個控制器。 Page Controller 的 主要缺點是要為每個網頁創(chuàng)建一個控制器。該特點非常適用于具有一組靜態(tài)頁面和簡單導航路徑的應用程序。有些較為復雜的應用程序要求對頁面和頁面間的導航映 射進行動態(tài)映射。如果將此邏輯分散于眾多頁面控制器,則可能導致應用程序難以維護,即使某些邏輯可以放入基本控制器。另外,Web 框架的內置功能可能會降低對 URL 和資源路徑命名時的靈活程度(雖然可以使用 ISAPI 篩選器等較低級別的機制進行一定程度的補償)。在這些方案中,請考慮使用 Front Controller 截取所有 Web 請求,并根據(jù)可配置規(guī)則將這些請求轉發(fā)至相應處理程序。
較深的繼承樹。繼承似乎是基于對象編程方式的既最可愛又最討厭的功能之一。如果僅通過使用繼承來重用公用功能,則可能降低繼承層次結構的靈活性。有關詳細信息,請參閱"在 ASP.NET 中實現(xiàn) Page Controller"。
對于 Web 框架的依賴。在基本形式中,頁面控制器仍然依賴于 Web 應用程序環(huán)境,且不能單獨進行測試。可以使用包裝機制來分隔依賴 Web 的部分,但這樣需要增加一個級別的間接性。
測試考慮事項
因為Page Controller 依賴于 Web 應用程序框架的具體細節(jié)(例如,查詢字符串和 HTTP 頭),因此不能在 Web 框架之外對控制器類進行實例化和測試操作。如果需要對控制器類運行一組自動單元測試,則每次測試時都必須啟動 Web 應用程序。然后必須使用可執(zhí)行所需功能的格式提交 HTTP 請求。此配置為測試帶來許多依賴性和不良影響。要提高可測試性,請考慮將業(yè)務邏輯(包括變得更加復雜的控制器邏輯)與依賴 Web 的代碼分隔開來。
新聞熱點
疑難解答