設(shè)計(jì)模式:Model View Presenter發(fā)布日期: 2006-08-07 | 更新日期: 2006-08-07
Jean-Paul Boodhoo 下載本文的代碼:DesignPatterns2006_08.exe (4423KB) ![]() 本頁(yè)內(nèi)容隨著 UI 創(chuàng)建技術(shù)(如 ASP.NET 和 Windows® Form)的功能越來(lái)越強(qiáng)大,讓 UI 層執(zhí)行更多功能已成為普遍的做法。由于沒(méi)有清晰的職責(zé)劃分,UI 層經(jīng)常成為邏輯層的全能代理,而后者實(shí)際上屬于應(yīng)用程序的其他層。Model View Presenter (MVP) 模式是專(zhuān)門(mén)適用于解決此問(wèn)題的一種設(shè)計(jì)模式。為了證明我的觀點(diǎn),我將遵循 MVP 模式為 Northwind 數(shù)據(jù)庫(kù)中的客戶(hù)創(chuàng)建一個(gè)顯示屏。 為什么 UI 層中不應(yīng)有過(guò)多邏輯?如果沒(méi)有手動(dòng)運(yùn)行應(yīng)用程序,或未能維護(hù)自動(dòng)執(zhí)行 UI 組件的高深 UI 運(yùn)行程序腳本,則很難測(cè)試應(yīng)用程序 UI 層中的代碼。這本身就是一個(gè)麻煩事,而更大的麻煩是應(yīng)用程序中普通視圖間大量的重復(fù)代碼。當(dāng)在 UI 層的不同部分之間復(fù)制執(zhí)行特定業(yè)務(wù)功能的邏輯時(shí),通常很難發(fā)現(xiàn)好的重構(gòu)候選者。MVP 設(shè)計(jì)模式使得將邏輯和代碼從 UI 層分離更為輕松,從而更易于簡(jiǎn)化測(cè)試可重用代碼。 圖 1 顯示組成示例應(yīng)用程序的主要層。請(qǐng)注意 UI 層和表示層使用不同的軟件包。您可能期望它們使用相同的軟件包,但實(shí)際上一個(gè)項(xiàng)目的 UI 層只應(yīng)由兩種 UI 元素組成 — 窗體和控件。在 Web Forms 項(xiàng)目中,通常是 ASP.NET Web Forms、用戶(hù)控件和服務(wù)器控件的集合。在 Windows Forms 中,是 Windows Forms、用戶(hù)控件和第三方程序庫(kù)的集合。此附加層用于分離顯示和邏輯。在表示層中可以有實(shí)際實(shí)現(xiàn) UI 行為的對(duì)象,如驗(yàn)證顯示、UI 的集合輸入等。 ![]() 圖 1 應(yīng)用程序體系結(jié)構(gòu) 遵循 MVP如圖 2 所示,此項(xiàng)目的 UI 是非常標(biāo)準(zhǔn)的。加載頁(yè)面時(shí),屏幕將會(huì)顯示一個(gè)填充了 Northwind 數(shù)據(jù)庫(kù)中所有客戶(hù)的下拉框。如果您從下拉列表中選擇一個(gè)客戶(hù),將會(huì)更新頁(yè)面,以顯示該客戶(hù)的信息。通過(guò)遵循 MVP 設(shè)計(jì)模式,您可將各種行為從 UI 層分離,將其置入自身的類(lèi)中。圖 3 顯示一個(gè)類(lèi)圖表,表示涉及的不同類(lèi)之間的關(guān)聯(lián)。 ![]() 圖 2 客戶(hù)信息 需要注意的很重要的一點(diǎn)是,表示器并不了解應(yīng)用程序?qū)嶋H UI 層的任何知識(shí)。它知道它可以與接口對(duì)話,但不知道也不關(guān)心接口的具體實(shí)現(xiàn)。這就促使了在不同 UI 技術(shù)間表示器的重用。 我將使用測(cè)試驅(qū)動(dòng)開(kāi)發(fā) (TDD) 來(lái)創(chuàng)建客戶(hù)屏幕功能。圖 4 顯示我將使用的第一個(gè)測(cè)試的詳細(xì)信息,以說(shuō)明我期望在頁(yè)面加載上觀察到的行為。TDD 使我可以一次將精力集中于一個(gè)問(wèn)題,只編寫(xiě)可使測(cè)試通過(guò)的足夠代碼,然后再繼續(xù)進(jìn)行。在此測(cè)試中,我將利用一個(gè)名為 NMock2 的模擬對(duì)象框架來(lái)構(gòu)建接口的模擬實(shí)現(xiàn)。 ![]() 圖 3 MVP 類(lèi)圖表 在我的 MVP 實(shí)現(xiàn)中,我決定將表示器作為其將要配合工作的視圖的附屬。在能使對(duì)象立即工作的狀態(tài)下創(chuàng)建對(duì)象總是很好的。在此應(yīng)用程序中,表示層實(shí)際上是依靠服務(wù)層來(lái)調(diào)用域功能的。由于此需求,因此也有必要建立一個(gè)帶接口的表示器,通過(guò)該接口它可以與服務(wù)類(lèi)進(jìn)行對(duì)話。這將確保一旦建立表示器后,它就可以進(jìn)行所有需要它來(lái)完成的工作。我將通過(guò)創(chuàng)建兩個(gè)特定的模擬開(kāi)始:一個(gè)用于服務(wù)層,一個(gè)用于表示器將要使用的視圖。 為什么要?jiǎng)?chuàng)建模擬?單元測(cè)試的規(guī)則是盡可能的隔離測(cè)試,以將精力集中于一個(gè)特定的對(duì)象。在此測(cè)試中,我只關(guān)注表示器的預(yù)期行為。此時(shí),我并不在意視圖接口或服務(wù)接口的實(shí)際實(shí)現(xiàn),我相信那些接口定義的協(xié)議,并相應(yīng)的設(shè)置模擬來(lái)表現(xiàn)。這可確保我將測(cè)試集中于我所期望的表示器行為,無(wú)需考慮其所依賴(lài)的對(duì)象。調(diào)用其初始化方法后,我所期望的表示器行為如下。 首先,表示器應(yīng)調(diào)用 ICustomerTask 服務(wù)層對(duì)象上的 GetCustomerList 方法(在測(cè)試中模擬)。請(qǐng)注意您可以使用 NMock 模仿模擬的行為。而對(duì)于服務(wù)層,我希望它可將模擬 ILookupCollection 返回到表示器。然后,在表示器從服務(wù)層檢索 ILookupCollection 后,它應(yīng)調(diào)用集合的 BindTo 方法并將方法傳遞到 ILookupList 的實(shí)現(xiàn)。通過(guò)使用 NMockExpect.Once 方法,我可以確定如果表示器沒(méi)有調(diào)用該方法一次(且僅一次),則測(cè)試將失敗。 編寫(xiě)該測(cè)試后,我將會(huì)處于完全非編輯狀態(tài)。我將盡可能做最簡(jiǎn)單的工作來(lái)使測(cè)試通過(guò)。 使第一次測(cè)試通過(guò)首先編寫(xiě)測(cè)試的好處之一是我現(xiàn)在擁有了一個(gè)遠(yuǎn)景藍(lán)圖,可以遵循它來(lái)對(duì)測(cè)試進(jìn)行編譯并最終通過(guò)。第一次測(cè)試包括兩個(gè)還不存在的接口。這些接口是正確編譯代碼的先決條件。我將從 IViewCustomerView 的代碼開(kāi)始: public interface IViewCustomerView { ILookupList CustomerList { get; } } 此接口提供一個(gè)屬性,該屬性可返回一個(gè) ILookupList 接口實(shí)現(xiàn)。對(duì)于該問(wèn)題,我還沒(méi)有一個(gè) ILookupList 接口,甚至沒(méi)有實(shí)施工具。為了通過(guò)此測(cè)試,我不需要明確的實(shí)施工具,這樣我可以繼續(xù)創(chuàng)建 ILookupList 接口:? public interface ILookupList { } 此時(shí),ILookupList 接口看起來(lái)沒(méi)什么用處。我的目標(biāo)是編譯并通過(guò)測(cè)試,而這些接口可以滿(mǎn)足測(cè)試的需求?,F(xiàn)在該將焦點(diǎn)轉(zhuǎn)向我要實(shí)際測(cè)試的對(duì)象 - ViewCustomerPresenter 了。?此類(lèi)尚不存在,但回頭查看該測(cè)試,您可以從中得出兩個(gè)重要事實(shí):它有一個(gè)構(gòu)造函數(shù),該函數(shù)需要視圖和服務(wù)實(shí)現(xiàn)作為依賴(lài),并且有一個(gè)空的 Initialize 方法。圖 5 中的代碼顯示如何編譯測(cè)試。 請(qǐng)牢記表示器需要其所有依賴(lài)關(guān)系,以便富有成效的進(jìn)行工作;這就是傳入視圖和服務(wù)的原因。我沒(méi)有實(shí)現(xiàn)初始化方法,因此如果運(yùn)行測(cè)試,我將得到 NotImplementedException。 如上所述,我沒(méi)有盲目的編寫(xiě)表示器代碼;通過(guò)查看測(cè)試,我已了解在調(diào)用初始化方法后表示器應(yīng)表現(xiàn)的行為。行為的實(shí)現(xiàn)代碼如下: public void Initialize() { task.GetCustomerList().BindTo(view.CustomerList); } 本文附帶的源代碼中有 CustomerTask 類(lèi)(實(shí)現(xiàn)了 ICustomerTask 接口)中 GetCustomerList 方法的完整實(shí)現(xiàn)。雖然從實(shí)現(xiàn)和測(cè)試表示器的角度看,我還無(wú)需了解是否存在工作實(shí)現(xiàn)。但正是該抽象級(jí)別使我難以通過(guò)表示器類(lèi)的測(cè)試。第一個(gè)測(cè)試現(xiàn)在正處于將要編譯和運(yùn)行的狀態(tài)。這證明在調(diào)用表示器上的 Initialize 方法時(shí),它將以我在測(cè)試中指定的方式與其依賴(lài)對(duì)象進(jìn)行交互,并且最終當(dāng)這些依賴(lài)對(duì)象的具體實(shí)現(xiàn)被插入表示器時(shí),我可以確信結(jié)果視圖(ASPX 頁(yè))將被客戶(hù)列表所填充。 填充 DropDownList到目前為止,我主要處理了接口,拋開(kāi)實(shí)際的實(shí)現(xiàn)細(xì)節(jié),將精力集中于表示器。現(xiàn)在,該建立一些探測(cè)代碼了,它最終將允許表示器以一種可測(cè)試的方式在 Web 頁(yè)面上填充列表。實(shí)現(xiàn)此功能的關(guān)鍵是將在 LookupCollection 類(lèi)的 BindTo 方法中發(fā)生的交互。如果您看一下圖 6 中 LookupCollection 類(lèi)的實(shí)現(xiàn),就會(huì)注意到它實(shí)現(xiàn)了 ILookupCollection 接口。本文的源代碼帶有隨附測(cè)試,可用于建立 LookupCollection 類(lèi)的功能。 BindTo 方法的實(shí)現(xiàn)特別有趣。請(qǐng)注意在此方法中,集合將重復(fù) ILookupDTO 實(shí)現(xiàn)本身的私有列表。ILookupDTO 是一個(gè)接口,可很好地與 UI 層的組合框綁定: public interface ILookupDTO { string Value { get; } string Text { get; } } 圖 7 顯示用于測(cè)試查找集合的 BindTo 方法的代碼,此方法將會(huì)幫助解釋 LookupCollection 與 ILookupList 之間的預(yù)期交互。最后一點(diǎn)特別有趣。在此測(cè)試中,我希望在嘗試向列表添加項(xiàng)目前,LookupCollection 將會(huì)調(diào)用 ILookupList 實(shí)現(xiàn)中的 Clear 方法。然后,我希望可以在 ILookupList 上調(diào)用 Add 10 次,而作為 Add 方法的參數(shù),LookupCollection 將在實(shí)現(xiàn) ILookupDTO 接口的對(duì)象中傳遞。若要使其與 Web 項(xiàng)目中的控件(例如下拉列表框)配合使用,則您需要?jiǎng)?chuàng)建一個(gè) ILookupList 實(shí)現(xiàn),該實(shí)現(xiàn)知道如何與 Web 項(xiàng)目中的控件配合使用。 本文附帶的源代碼包含一個(gè)名為 MVP.Web.Controls 的項(xiàng)目。該項(xiàng)目包含我選擇用于創(chuàng)建完整解決方案的所有 Web 特定控件或類(lèi)。為什么我將代碼放在此項(xiàng)目中,而不是放在 APP_CODE 目錄或 Web 項(xiàng)目中?回答是可測(cè)試性。在沒(méi)有手動(dòng)運(yùn)行應(yīng)用程序或沒(méi)有使用某種測(cè)試程序自動(dòng)執(zhí)行 UI 測(cè)試的情況下,很難直接測(cè)試 Web 項(xiàng)目中的任何控件。MVP 模式使我可在不必手動(dòng)運(yùn)行應(yīng)用程序的情況下考慮更高的抽象級(jí)別,并測(cè)試核心接口(ILookupList 和 ILookupCollection)的實(shí)現(xiàn)。我打算向 Web.Controls 項(xiàng)目中添加一個(gè)新類(lèi):WebLookupList 控件。圖 8 顯示此類(lèi)的第一次測(cè)試。 某些事項(xiàng)在圖 8 所示的測(cè)試中比較突出。顯然,測(cè)試項(xiàng)目需要一個(gè)到 System.Web 庫(kù)的引用,這樣它就可以實(shí)例化 DropDownList Web 控件。進(jìn)一步查看測(cè)試,您應(yīng)了解 WebLookupList 類(lèi)將會(huì)實(shí)現(xiàn) ILookupList 接口。它還會(huì)將 ListControl 作為一個(gè)依賴(lài)對(duì)象。System.Web.UI.WebControls 命名空間中兩個(gè)最常見(jiàn)的 ListControl 實(shí)現(xiàn)是 DropDownList 和 ListBox 類(lèi)。圖 8 中測(cè)試的主要功能是要確保 WebLookupList 正確的將實(shí)際 Web ListControl 的狀態(tài)更新為其正在委派責(zé)任的狀態(tài)。圖 9 顯示 WebLookupList 實(shí)現(xiàn)中涉及的類(lèi)的類(lèi)圖表。我可以通過(guò)圖 10 中的代碼,滿(mǎn)足對(duì) WebLookupList 控件第一次測(cè)試的要求。 ![]() 圖 9 WebLookupList 類(lèi) 請(qǐng)記住,MVP 的一個(gè)關(guān)鍵是由創(chuàng)建視圖接口引入的層的分離。表示器不了解視圖的具體實(shí)現(xiàn),以及它要對(duì)話的各個(gè) ILookupList,它只知道它可以調(diào)用這些接口定義的任何方法。最后,WebLookupList 類(lèi)是一個(gè)包裝并委托至底層 ListControl 的類(lèi)(在 System.Web.UI.WebControls 項(xiàng)目中定義的某些 ListControls 的基類(lèi))。利用這些代碼,我可以編譯并運(yùn)行 WebLookupList 控件測(cè)試,現(xiàn)在測(cè)試應(yīng)該順利通過(guò)了。我可以為 WebLookupList 再添加一個(gè)測(cè)試,以測(cè)試 Clear 方法的實(shí)際行為: [Test] public void ShouldClearUnderlyingList() { ListControl webList = new DropDownList(); ILookupList list = new WebLookupList(webList); webList.Items.Add(new ListItem("1", "1")); list.Clear(); Assert.AreEqual(0, webList.Items.Count); } 另外,我將測(cè)試在調(diào)用 WebLookupList 類(lèi)自身的方法時(shí),它是否會(huì)真正更改底層 ListControl (DropDownList) 的狀態(tài)。WebLookupList 現(xiàn)在可以完成填充 Web Form 中 DropDownList 的功能?,F(xiàn)在可將所有程序綁定在一起,就可獲得已填充客戶(hù)列表的 Web 頁(yè)面下拉列表。 實(shí)現(xiàn)視圖接口由于我在建立 Web Form 前端,因此 IViewCustomerView 接口的實(shí)現(xiàn)程序必須是 Web Form 或用戶(hù)控件。出于此列的原因,我將其設(shè)為 Web Form。頁(yè)面的常規(guī)外觀已經(jīng)創(chuàng)建,如圖 2 所示?,F(xiàn)在我只需要實(shí)現(xiàn)視圖接口。切換到 ViewCustomers.aspx 頁(yè)的源代碼,我可以添加以下代碼,表示需要此頁(yè)來(lái)實(shí)現(xiàn) IViewCustomersView 接口: public partial class ViewCustomers :Page,IViewCustomerView 如果觀察示例代碼,您將會(huì)發(fā)現(xiàn) Web 項(xiàng)目和 Presentation 是兩個(gè)完全不同的程序集。而且,Presentation 項(xiàng)目沒(méi)有引用任何 Web.UI 項(xiàng)目,這樣可進(jìn)一步維護(hù)分離層。另一方面,Web.UI 項(xiàng)目必須引用 Presentation 項(xiàng)目,因?yàn)橐晥D接口和表示器都位于該項(xiàng)目中。 通過(guò)選擇實(shí)現(xiàn) IViewCustomerView 接口,現(xiàn)在我們的 Web 頁(yè)面可以實(shí)現(xiàn)由該接口定義的任何方法或?qū)傩浴.?dāng)前 IViewCustomerView 接口上只有一個(gè)屬性,是一個(gè)可返回 ILookupList 接口任何實(shí)現(xiàn)的 getter。我已向 Web.Controls 項(xiàng)目中添加了引用,這樣就可以實(shí)例化 WebLookupListControl。我這樣做是因?yàn)?WebLookupListControl 實(shí)現(xiàn)了 ILookupList 接口,并且它知道如何委托給 ASP.NET 中的實(shí)際 WebControls。請(qǐng)查看 ViewCustomer 頁(yè)面的 ASPX,您將會(huì)發(fā)現(xiàn)客戶(hù)列表只是一個(gè) asp:DropDownList 控件: <td>Customers:</td> <td><asp:DropDownList id="customerDropDownList" AutoPostBack="true" runat="server" Width="308px"></asp:DropDownList></td> </tr> 利用這些已有代碼,我可以快速的繼續(xù)實(shí)現(xiàn)滿(mǎn)足 IViewCustomerView 接口實(shí)現(xiàn)所需的代碼: public ILookupList CustomerList { get { return new WebLookupList(this.customerDropDownList);} } 我現(xiàn)在需要調(diào)用表示器上的 Initialize 方法,以觸發(fā)該方法實(shí)際執(zhí)行一些操作。因此,視圖需要能夠?qū)嵗硎酒?,這樣就可以調(diào)用它的方法了。如果回頭查看一下表示器,您會(huì)記得它需要視圖和服務(wù)與之配合使用。ICustomerTask 接口表示位于應(yīng)用程序服務(wù)層的接口。服務(wù)層通常負(fù)責(zé)協(xié)調(diào)域?qū)ο笾g的交互,并將這些交互的結(jié)果轉(zhuǎn)換為“數(shù)據(jù)傳輸對(duì)象”(Data Transfer Objects, DTO),然后將其從服務(wù)層傳遞到表示層,再到 UI 層。但是此處有一個(gè)問(wèn)題:我已規(guī)定表示器需要與視圖和服務(wù)實(shí)現(xiàn)一同構(gòu)造。 表示器的實(shí)際實(shí)例化將在 Web 頁(yè)的源代碼中進(jìn)行。這是一個(gè)問(wèn)題,因?yàn)?UI 項(xiàng)目沒(méi)有引用任何服務(wù)層項(xiàng)目。但是,表示項(xiàng)目卻引用了服務(wù)層項(xiàng)目。通過(guò)將一個(gè)重載構(gòu)造函數(shù)添加到 ViewCustomerPresenterClass 中,可以解決此問(wèn)題: public ViewCustomerPresenter(IViewCustomerView view) : this(view, new CustomerTask()) {} 這一新的構(gòu)造函數(shù)同時(shí)滿(mǎn)足了表示器視圖和服務(wù)的實(shí)現(xiàn)要求,同時(shí)還可從服務(wù)層維護(hù) UI 層的分離?,F(xiàn)在完成源代碼的后續(xù)代碼就很簡(jiǎn)單了: protected override void OnInit(EventArgs e) { base.OnInit(e); presenter = new ViewCustomerPresenter(this); } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) presenter.Initialize(); } 請(qǐng)注意,表示器實(shí)例化的關(guān)鍵是:我將利用新建的構(gòu)造函數(shù)重載,并且 Web Form 會(huì)將其自身作為實(shí)現(xiàn)視圖接口的對(duì)象傳入。 利用實(shí)現(xiàn)的源代碼中的代碼,我可以立即創(chuàng)建并運(yùn)行應(yīng)用程序。現(xiàn)在不需要源代碼中的任何數(shù)據(jù)綁定代碼,就可以使用客戶(hù)名稱(chēng)列表來(lái)填充 Web 頁(yè)上的 DropDownList。另外,已在最終一起工作的所有代碼段上運(yùn)行了測(cè)試分?jǐn)?shù),這可確保表示層體系結(jié)構(gòu)將按預(yù)期運(yùn)轉(zhuǎn)。 現(xiàn)在我準(zhǔn)備展示一下在 DropDownList 中顯示選定客戶(hù)信息所需的步驟,以此來(lái)總結(jié)我對(duì) MVP 的討論。再次重申,我將首先編寫(xiě)一個(gè)測(cè)試,來(lái)描述我所希望觀察到的行為。(請(qǐng)參閱圖 11)。 如上所述,我將利用 NMock 程序庫(kù)來(lái)創(chuàng)建任務(wù)和視圖接口的模擬。此特定測(cè)試將通過(guò)向服務(wù)層請(qǐng)求表示特定客戶(hù)的 DTO 來(lái)驗(yàn)證表示器的行為。表示器從服務(wù)層檢索到 DTO 后,它將直接更新視圖上的屬性,這樣視圖就不必了解任何有關(guān)如何正確顯示對(duì)象信息的知識(shí)。簡(jiǎn)便起見(jiàn),我將不再討論 WebLookupList 控件上 SelectedItem 屬性的實(shí)現(xiàn);相反,我會(huì)將它留給您去檢查源代碼,以了解實(shí)現(xiàn)的詳細(xì)信息。此測(cè)試真正展示的是在表示器從服務(wù)層檢索 CustomerDTO 后,表示器和視圖之間發(fā)生的交互。如果現(xiàn)在嘗試運(yùn)行測(cè)試,我將面臨一個(gè)嚴(yán)重的失敗,因?yàn)橐晥D接口上的許多屬性都還不存在。因此,我將繼續(xù)進(jìn)行并為 IViewCustomerView 接口添加必要的成員,如圖 12 所示。 這些接口成員添加完成之后,我的 Web Form 也許會(huì)抱怨,因?yàn)樗辉贊M(mǎn)足接口協(xié)議了,所以我必須返回 Web Form 的源代碼并實(shí)現(xiàn)其余的成員。如上所述,Web 頁(yè)的整個(gè)標(biāo)記已經(jīng)創(chuàng)建,同時(shí)表格單元格已被標(biāo)記為 "runat=server" 屬性,并且已根據(jù)其應(yīng)顯示的信息進(jìn)行了命名。這樣就可以使結(jié)果代碼非常輕松的實(shí)現(xiàn)接口成員: public string CompanyName { set { this.companyNameLabel.InnerText = value; } } public string ContactName { set { this.contactNameLabel.InnerText = value; } } ... 隨著 setter 屬性的實(shí)現(xiàn),現(xiàn)在只剩下最后一件事要完成。我需要一種方法來(lái)告訴表示器顯示選定客戶(hù)的信息?;仡^看看測(cè)試,您會(huì)發(fā)現(xiàn)此行為的實(shí)現(xiàn)位于表示器的 DisplayCustomerDetails 方法中。但是,此方法不帶有任何參數(shù)。調(diào)用時(shí),表示器將返回視圖,從中提取其所需的任何信息(使用 ILookupList 檢索),然后使用該信息檢索選定客戶(hù)的詳細(xì)信息。從 UI 角度看,我需要做的就是將 DropDownList 的 AutoPostBack 屬性設(shè)置為 true,我還需要將以下事件處理程序掛鉤代碼添加到頁(yè)面的 OnInit 方法中: protected override void OnInit(EventArgs e) { base.OnInit(e); presenter = new ViewCustomerPresenter(this); this.customerDropDownList.SelectedIndexChanged += delegate { presenter.DisplayCustomerDetails(); }; } 此事件處理程序可確保在下拉列表中選擇新客戶(hù)時(shí),視圖將請(qǐng)求表示器顯示該客戶(hù)的詳細(xì)信息。 重要的是注意這是典型行為。當(dāng)視圖請(qǐng)求表示器執(zhí)行操作時(shí),它不會(huì)給予任何特定的詳細(xì)信息,并且將由表示器來(lái)決定是否返回視圖,并使用視圖接口來(lái)獲取其所需的任何信息。圖 13 顯示實(shí)現(xiàn)表示器中所需行為的代碼。 希望您現(xiàn)在可以了解添加表示器層的價(jià)值了。表示器負(fù)責(zé)嘗試檢索需要顯示其詳細(xì)信息的客戶(hù) ID。這就是通常在源代碼中執(zhí)行的代碼,但是它現(xiàn)在位于類(lèi)中,我可以在任何表示層技術(shù)以外對(duì)其進(jìn)行完全的測(cè)試和實(shí)踐。 如果表示器能夠從視圖中檢索有效的客戶(hù) ID,則它將轉(zhuǎn)向服務(wù)層并請(qǐng)求表示該客戶(hù)詳細(xì)信息的 DTO。表示器獲得 DTO 后,它將使用 DTO 中包含的信息更新視圖。要注意的關(guān)鍵一點(diǎn)是視圖接口的簡(jiǎn)單性,除 ILookupList 接口以外,視圖接口完全由字符串 DataTypes 組成。表示器的最終職責(zé)是正確地轉(zhuǎn)換和格式化從 DTO 中檢索的信息,這樣它就可以作為字符串,實(shí)際被傳遞到視圖。雖然未在此例中說(shuō)明,但表示器還可負(fù)責(zé)從視圖中讀取信息,并將其轉(zhuǎn)換為服務(wù)層所期待的必要類(lèi)型。 完成所有代碼段后,我現(xiàn)在就可以運(yùn)行應(yīng)用程序了。首次加載頁(yè)面時(shí),我會(huì)獲得一個(gè)客戶(hù)列表,并且在 DropDownList 中顯示(未選中)第一個(gè)客戶(hù)。如果我選擇一個(gè)客戶(hù),則會(huì)出現(xiàn)回發(fā),視圖與表示器之間發(fā)生交互,并且會(huì)使用相關(guān)的客戶(hù)信息更新 Web 頁(yè)面。 未來(lái)計(jì)劃Model View Presenter 設(shè)計(jì)模式實(shí)際上就是許多開(kāi)發(fā)人員已經(jīng)熟悉的模板視圖控制器的一個(gè)最新版本;兩者的主要區(qū)別是 MVP 真正將 UI 從應(yīng)用程序的域/服務(wù)層中分離。雖然從需求角度看,此示例十分簡(jiǎn)單,但它可以幫助您抽象化 UI 與應(yīng)用程序其他層之間的交互。而且,現(xiàn)在您可了解多種方法:您可間接使用這些層來(lái)自動(dòng)測(cè)試您的應(yīng)用程序。隨著您對(duì) MVP 模式的深入研究,我希望您可以找到其他方法,從源代碼中提取更多格式和條件邏輯,并將其置入可測(cè)試視圖/表示器交互模型中。 請(qǐng)將您的問(wèn)題和意見(jiàn)發(fā)送至 mmpatt@microsoft.com。 Jean-Paul Boodhoo 是 ThoughtWorks 的一名高級(jí) .NET 交付專(zhuān)家,他曾參與了許多使用 .NET 框架和各種靈活方法的企業(yè)級(jí)應(yīng)用程序交付。他經(jīng)常利用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)提供有關(guān)使用 .NET 功能的演示??赏ㄟ^(guò) mailtio:bitwisejp@gmail.com 或 www./blog 聯(lián)系 Jean-Paul。 本文摘自 MSDN Magazine 的 2006 年 8 月號(hào)。
Figure 4 The First Test
[Test] public void ShouldLoadListOfCustomersOnInitialize() { mockery = new Mockery(); ICustomerTask mockCustomerTask = mockery.NewMock<ICustomerTask>(); IViewCustomerView mockViewCustomerView = mockery.NewMock<IViewCustomerView>(); ILookupList mockCustomerLookupList = mockery.NewMock<ILookupList>(); ViewCustomerPresenter presenter = new ViewCustomerPresenter(mockViewCustomerView, mockCustomerTask); ILookupCollection mockLookupCollection = mockery.NewMock<ILookupCollection>(); Expect.Once.On(mockCustomerTask).Method( "GetCustomerList").Will(Return.Value(mockLookupCollection)); Expect.Once.On(mockViewCustomerView).GetProperty( "CustomerList").Will(Return.Value(mockCustomerLookupList)); Expect.Once.On(mockLookupCollection).Method( "BindTo").With(mockCustomerLookupList); presenter.Initialize(); } Figure 5 Compiling the Test
public class ViewCustomerPresenter { private readonly IViewCustomerView view; private readonly ICustomerTask task; public ViewCustomerPresenter( IViewCustomerView view, ICustomerTask task) { this.view = view; this.task = task; } public void Initialize() { throw new NotImplementedException(); } } Figure 6 The LookupCollection Class
public class LookupCollection : ILookupCollection { private IList<ILookupDTO> items; public LookupCollection(IEnumerable<ILookupDTO> items) { this.items = new List<ILookupDTO>(items); } public int Count { get { return items.Count; } } public void BindTo(ILookupList list) { list.Clear(); foreach (ILookupDTO dto in items) list.Add(dto); } } Figure 7 A Test that Describes Behavior
[Test] public void ShouldBeAbleToBindToLookupList() { IList<ILookupDTO> dtos = new IList; ILookupList mockLookupList = mockery.NewMock<ILookupList>(); Expect.Once.On(mockLookupList).Method("Clear"); for (int i = 0; i < 10; i++) { SimpleLookupDTO dto = new SimpleLookupDTO(i.ToString(),i.ToString()); dtos.Add(dto); Expect.Once.On(mockLookupList).Method("Add").With(dto); } new LookupCollection(dtos).BindTo(mockLookupList); } Figure 8 First Test for WebLookupList Control
[Test] public void ShouldAddItemToUnderlyingList() { ListControl webList = new DropDownList(); ILookupList list = new WebLookupList(webList); SimpleLookupDTO dto = new SimpleLookupDTO("1","1"); list.Add(dto); Assert.AreEqual(1, webList.Items.Count); Assert.AreEqual(dto.Value, webList.Items[0].Value); Assert.AreEqual(dto.Text, webList.Items[0].Text); } Figure 10 WebLookupList Control
public class WebLookupList : ILookupList { private ListControl underlyingList; public WebLookupList(ListControl underlyingList) { this.underlyingList = underlyingList; } public void Add(ILookupDTO dto) { underlyingList.Items.Add(new ListItem(dto.Text, dto.Value)); } } Figure 11 One Last Test
[Test] public void ShouldDisplayCustomerDetails() { SimpleLookupDTO lookupDTO = new SimpleLookupDTO("1","JPBOO"); CustomerDTO dto = new CustomerDTO("BLAH", "BLAHCOMPNAME", "BLAHCONTACTNAME", "BLAHCONTACTTILE", "ADDRESS", "CITY", "REGION", "POSTALCODE", Country.CANADA, "4444444", "4444444"); Expect.Once.On(mockViewCustomerView).GetProperty( "CustomerList").Will(Return.Value(mockCustomerLookupList)); Expect.Once.On(mockCustomerLookupList).GetProperty( "SelectedItem").Will(Return.Value(lookupDTO)); Expect.Once.On(mockCustomerTask).Method( "GetDetailsForCustomer").With(1).Will(Return.Value(dto)); Expect.Once.On(mockViewCustomerView).SetProperty( "CompanyName").To(dto.CompanyName); Expect.Once.On(mockViewCustomerView).SetProperty( "ContactName").To(dto.ContactName); Expect.Once.On(mockViewCustomerView).SetProperty( "ContactTitle").To(dto.ContactTitle); Expect.Once.On(mockViewCustomerView).SetProperty( "Address").To(dto.Address); Expect.Once.On(mockViewCustomerView).SetProperty( "City").To(dto.City); Expect.Once.On(mockViewCustomerView).SetProperty( "Region").To(dto.Region); Expect.Once.On(mockViewCustomerView).SetProperty( "PostalCode").To(dto.PostalCode); Expect.Once.On(mockViewCustomerView).SetProperty( "Country").To(dto.CountryOfResidence.Name); Expect.Once.On(mockViewCustomerView).SetProperty( "Phone").To(dto.Phone); Expect.Once.On(mockViewCustomerView).SetProperty("Fax").To(dto.Fax); presenter.DisplayCustomerDetails(); } Figure 12 Completing the IVewCustomerView Interface
public interface IViewCustomerView { ILookupList CustomerList{get;} string CompanyName{set;} string ContactName{set;} string ContactTitle{set;} string Address{set;} string City{set;} string Region{set;} string PostalCode{set;} string Country{set;} string Phone{set;} string Fax{set;} } Figure 13 Completing the Presenter
public void DisplayCustomerDetails() { int? customerId = SelectedCustomerId; if (customerId.HasValue) { CustomerDTO customer = task.GetDetailsForCustomer(customerId.Value); UpdateViewFrom(customer); } } private int? SelectedCustomerId { get { string selectedId = view.CustomerList.SelectedItem.Value; if (String.IsNullOrEmpty(selectedId)) return null; int? id = null; try { id = int.Parse(selectedId.Trim()); } catch (FormatException) {} return id; } } private void UpdateViewFrom(CustomerDTO customer) { view.CompanyName = customer.CompanyName; view.ContactName = customer.ContactName; view.ContactTitle = customer.ContactTitle; view.Address = customer.Address; view.City = customer.City; view.Region = customer.Region; view.Country = customer.CountryOfResidence.Name; view.Phone = customer.Phone; view.Fax = customer.Fax; view.PostalCode = customer.PostalCode; } |
|