【文摘】設計模式:Model View Presenter

來源:互聯網
上載者:User
設計模式:Model View Presenter發布日期: 2006-08-07 | 更新日期: 2006-08-07

Jean-Paul Boodhoo

下載本文的代碼:DesignPatterns2006_08.exe (4423KB)

本頁內容
遵循 MVP
使第一次測試通過
填充 DropDownList
實現視圖介面
未來計劃

隨著 UI 建立技術(如 ASP.NET 和 Windows Form)的功能越來越強大,讓 UI 層執行更多功能已成為普遍的做法。由於沒有清晰的職責劃分,UI 層經常成為邏輯層的全能代理,而後者實際上屬於應用程式的其他層。Model View Presenter (MVP) 模式是專門適用於解決此問題的一種設計模式。為了證明我的觀點,我將遵循 MVP 模式為 Northwind 資料庫中的客戶建立一個顯示屏。

為什麼 UI 層中不應有過多邏輯?如果沒有手動運行應用程式,或未能維護自動執行 UI 組件的高深 UI 運行程式指令碼,則很難測試應用程式 UI 層中的代碼。這本身就是一個麻煩事,而更大的麻煩是應用程式中普通視圖間大量的重複代碼。當在 UI 層的不同部分之間複製執行特定業務功能的邏輯時,通常很難發現好的重構候選者。MVP 設計模式使得將邏輯和代碼從 UI 層分離更為輕鬆,從而更易於簡化測試可重用代碼。

圖 1 顯示組成應用程式範例的主要層。請注意 UI 層和展示層使用不同的軟體包。您可能期望它們使用相同的軟體包,但實際上一個項目的 UI 層只應由兩種 UI 元素組成 — 表單和控制項。在 Web Forms 項目中,通常是 ASP.NET Web Forms、使用者控制項和伺服器控制項的集合。在 Windows Forms 中,是 Windows Forms、使用者控制項和第三方程式庫的集合。此附加層用於分離顯示和邏輯。在展示層中可以有實際實現 UI 行為的對象,如驗證顯示、UI 的集合輸入等。

圖 1 應用程式體繫結構

遵循 MVP

2 所示,此項目的 UI 是非常標準的。載入頁面時,螢幕將會顯示一個填充了 Northwind 資料庫中所有客戶的下拉框。如果您從下拉式清單中選擇一個客戶,將會更新頁面,以顯示該客戶的資訊。通過遵循 MVP 設計模式,您可將各種行為從 UI 層分離,將其置入自身的類中。圖 3 顯示一個類圖表,表示涉及的不同類之間的關聯。

圖 2 客戶資訊

需要注意的很重要的一點是,表示器並不瞭解應用程式實際 UI 層的任何知識。它知道它可以與介面對話,但不知道也不關心介面的具體實現。這就促使了在不同 UI 技術間表示器的重用。

我將使用測試驅動開發 (TDD) 來建立客戶螢幕功能。圖 4 顯示我將使用的第一個測試的詳細資料,以說明我期望在頁面載入上觀察到的行為。TDD 使我可以一次將精力集中於一個問題,只編寫可使測試通過的足夠代碼,然後再繼續進行。在此測試中,我將利用一個名為 NMock2 的類比對象架構來構建介面的類比實現。

圖 3 MVP 類圖表

在我的 MVP 實現中,我決定將表示器作為其將要配合工作的視圖的附屬。在能使對象立即工作的狀態下建立對象總是很好的。在此應用程式中,展示層實際上是依靠服務層來調用域功能的。由於此需求,因此也有必要建立一個帶介面的表示器,通過該介面它可以與服務類進行對話。這將確保一旦建立表示器後,它就可以進行所有需要它來完成的工作。我將通過建立兩個特定的類比開始:一個用於服務層,一個用於表示器將要使用的視圖。

為什麼要建立類比?單元測試的規則是儘可能的隔離測試,以將精力集中於一個特定的對象。在此測試中,我只關注表示器的預期行為。此時,我並不在意視圖介面或服務介面的實際實現,我相信那些介面定義的協議,並相應的設定類比來表現。這可確保我將測試集中於我所期望的表示器行為,無需考慮其所依賴的對象。調用其初始化方法後,我所期望的表示器行為如下。

首先,表示器應調用 ICustomerTask 服務層對象上的 GetCustomerList 方法(在測試中類比)。請注意您可以使用 NMock 模仿類比的行為。而對於服務層,我希望它可將類比 ILookupCollection 返回到表示器。然後,在表示器從服務層檢索 ILookupCollection 後,它應調用集合的 BindTo 方法並將方法傳遞到 ILookupList 的實現。通過使用 NMockExpect.Once 方法,我可以確定如果表示器沒有調用該方法一次(且僅一次),則測試將失敗。

編寫該測試後,我將會處於完全非編輯狀態。我將儘可能做最簡單的工作來使測試通過。

返回頁首

使第一次測試通過

首先編寫測試的好處之一是我現在擁有了一個遠景藍圖,可以遵循它來對測試進行編譯並最終通過。第一次測試包括兩個還不存在的介面。這些介面是正確編譯代碼的先決條件。我將從 IViewCustomerView 的代碼開始:

public interface IViewCustomerView{ILookupList CustomerList { get; }}

此介面提供一個屬性,該屬性可返回一個 ILookupList 介面實現。對於該問題,我還沒有一個 ILookupList 介面,甚至沒有實施工具。為了通過此測試,我不需要明確的實施工具,這樣我可以繼續建立 ILookupList 介面:


public interface ILookupList { }

此時,ILookupList 介面看起來沒什麼用處。我的目標是編譯並通過測試,而這些介面可以滿足測試的需求。現在該將焦點轉向我要實際測試的對象 - ViewCustomerPresenter 了。
此類尚不存在,但回頭查看該測試,您可以從中得出兩個重要事實:它有一個建構函式,該函數需要視圖和服務實現作為依賴,並且有一個空的 Initialize 方法。圖 5 中的代碼顯示如何編譯測試。

請牢記表示器需要其所有依賴關係,以便富有成效的進行工作;這就是傳入視圖和服務的原因。我沒有實現初始化方法,因此如果運行測試,我將得到 NotImplementedException。

如上所述,我沒有盲目的編寫表示器代碼;通過查看測試,我已瞭解在調用初始化方法後表示器應表現的行為。行為的實現代碼如下:

public void Initialize(){task.GetCustomerList().BindTo(view.CustomerList);}

本文附帶的原始碼中有 CustomerTask 類(實現了 ICustomerTask 介面)中 GetCustomerList 方法的完整實現。雖然從實現和測試表示器的角度看,我還無需瞭解是否存在工作實現。但正是該抽象層級使我難以通過表示器類的測試。第一個測試現在正處於將要編譯和啟動並執行狀態。這證明在調用表示器上的 Initialize 方法時,它將以我在測試中指定的方式與其依賴對象進行互動,並且最終當這些依賴對象的具體實現被插入表示器時,我可以確信結果檢視(ASPX 頁)將被客戶列表所填充。

返回頁首

填充 DropDownList

到目前為止,我主要處理了介面,拋開實際的實現細節,將精力集中於表示器。現在,該建立一些探測代碼了,它最終將允許表示器以一種可測試的方式在 Web 頁面上填充列表。實現此功能的關鍵是將在 LookupCollection 類的 BindTo 方法中發生的互動。如果您看一 6 中 LookupCollection 類的實現,就會注意到它實現了 ILookupCollection 介面。本文的原始碼帶有隨附測試,可用於建立 LookupCollection 類的功能。

BindTo 方法的實現特別有趣。請注意在此方法中,集合將重複 ILookupDTO 實現本身的私人列表。ILookupDTO 是一個介面,可很好地與 UI 層的組合框綁定:

public interface ILookupDTO{string Value { get; }string Text { get; }}

圖 7 顯示用於測試尋找集合的 BindTo 方法的代碼,此方法將會協助解釋 LookupCollection 與 ILookupList 之間的預期互動。最後一點特別有趣。在此測試中,我希望在嘗試向列表添加項目前,LookupCollection 將會調用 ILookupList 實現中的 Clear 方法。然後,我希望可以在 ILookupList 上調用 Add 10 次,而作為 Add 方法的參數,LookupCollection 將在實現 ILookupDTO 介面的對象中傳遞。若要使其與 Web 項目中的控制項(例如下拉式清單方塊)配合使用,則您需要建立一個 ILookupList 實現,該實現知道如何與 Web 項目中的控制項配合使用。

本文附帶的原始碼包含一個名為 MVP.Web.Controls 的項目。該項目包含我選擇用於建立完整解決方案的所有 Web 特定控制項或類。為什麼我將代碼放在此項目中,而不是放在 APP_CODE 目錄或 Web 項目中?回答是可測試性。在沒有手動運行應用程式或沒有使用某種測試程式自動執行 UI 測試的情況下,很難直接測試 Web 項目中的任何控制項。MVP 模式使我可在不必手動運行應用程式的情況下考慮更高的抽象層級,並測試核心介面(ILookupList 和 ILookupCollection)的實現。我打算向 Web.Controls 項目中添加一個新類:WebLookupList 控制項。圖 8 顯示此類的第一次測試。

某些事項在圖 8 所示的測試中比較突出。顯然,測試專案需要一個到 System.Web 庫的引用,這樣它就可以執行個體化 DropDownList Web 控制項。進一步查看測試,您應瞭解 WebLookupList 類將會實現 ILookupList 介面。它還會將 ListControl 作為一個依賴對象。System.Web.UI.WebControls 命名空間中兩個最常見的 ListControl 實現是 DropDownList 和 ListBox 類。圖 8 中測試的主要功能是要確保 WebLookupList 正確的將實際 Web ListControl 的狀態更新為其正在委派責任的狀態。圖 9 顯示 WebLookupList 實現中涉及的類的類圖表。我可以通過圖 10 中的代碼,滿足對 WebLookupList 控制項第一次測試的要求。

圖 9 WebLookupList 類

請記住,MVP 的一個關鍵是由建立視圖介面引入的層的分離。表示器不瞭解視圖的具體實現,以及它要對話的各個 ILookupList,它只知道它可以調用這些介面定義的任何方法。最後,WebLookupList 類是一個封裝並委託至底層 ListControl 的類(在 System.Web.UI.WebControls 項目中定義的某些 ListControls 的基類)。利用這些代碼,我可以編譯並運行 WebLookupList 控制項測試,現在測試應該順利通過了。我可以為 WebLookupList 再添加一個測試,以測試 Clear 方法的實際行為:

[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);}

另外,我將測試在調用 WebLookupList 類自身的方法時,它是否會真正更改底層 ListControl (DropDownList) 的狀態。WebLookupList 現在可以完成填充 Web Form 中 DropDownList 的功能。現在可將所有程式綁定在一起,就可獲得已填充客戶列表的 Web 頁面下拉式清單。

返回頁首

實現視圖介面

由於我在建立 Web Form 前端,因此 IViewCustomerView 介面的實現程式必須是 Web Form 或使用者控制項。出於此列的原因,我將其設為 Web Form。頁面的常規外觀已經建立, 2 所示。現在我只需要實現視圖介面。切換到 ViewCustomers.aspx 頁的原始碼,我可以添加以下代碼,表示需要此頁來實現 IViewCustomersView 介面:

public partial class ViewCustomers :Page,IViewCustomerView

如果觀察範例程式碼,您將會發現 Web 項目和 Presentation 是兩個完全不同的程式集。而且,Presentation 項目沒有引用任何 Web.UI 項目,這樣可進一步維護分離層。另一方面,Web.UI 項目必須引用 Presentation 項目,因為視圖介面和表示器都位於該項目中。

通過選擇實現 IViewCustomerView 介面,現在我們的 Web 頁面可以實現由該介面定義的任何方法或屬性。當前 IViewCustomerView 介面上只有一個屬性,是一個可返回 ILookupList 介面任何實現的 getter。我已向 Web.Controls 項目中添加了引用,這樣就可以執行個體化 WebLookupListControl。我這樣做是因為 WebLookupListControl 實現了 ILookupList 介面,並且它知道如何委託給 ASP.NET 中的實際 WebControls。請查看 ViewCustomer 頁面的 ASPX,您將會發現客戶列表只是一個 asp:DropDownList 控制項:

<td>Customers:</td><td><asp:DropDownList id="customerDropDownList" AutoPostBack="true"runat="server" Width="308px"></asp:DropDownList></td></tr>

利用這些已有代碼,我可以快速的繼續實現滿足 IViewCustomerView 介面實現所需的代碼:

public ILookupList CustomerList{get { return new WebLookupList(this.customerDropDownList);}}

我現在需要調用表示器上的 Initialize 方法,以觸發該方法實際執行一些操作。因此,視圖需要能夠執行個體化表示器,這樣就可以調用它的方法了。如果回頭查看一下表示器,您會記得它需要視圖和服務與之配合使用。ICustomerTask 介面表示位於應用程式服務層的介面。服務層通常負責協調域對象之間的互動,並將這些互動的結果轉換為“資料轉送對象”(Data Transfer Objects, DTO),然後將其從服務層傳遞到展示層,再到 UI 層。但是此處有一個問題:我已規定表示器需要與視圖和服務實現一同構造。

表示器的實際執行個體化將在 Web 頁的原始碼中進行。這是一個問題,因為 UI 項目沒有引用任何服務層項目。但是,表示項目卻引用了服務層項目。通過將一個重載建構函式添加到 ViewCustomerPresenterClass 中,可以解決此問題:

public ViewCustomerPresenter(IViewCustomerView view) :this(view, new CustomerTask()) {}

這一新的建構函式同時滿足了表示器視圖和服務的實現要求,同時還可從服務層維護 UI 層的分離。現在完成原始碼的後續代碼就很簡單了:

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();}

請注意,表示器執行個體化的關鍵是:我將利用建立的建構函式重載,並且 Web Form 會將其自身作為實現視圖介面的對象傳入。

利用實現的原始碼中的代碼,我可以立即建立並運行應用程式。現在不需要原始碼中的任何資料繫結代碼,就可以使用客戶名稱列表來填充 Web 頁上的 DropDownList。另外,已在最終一起工作的所有程式碼片段上運行了測試分數,這可確保展示層體繫結構將按預期運轉。

現在我準備展示一下在 DropDownList 中顯示選定客戶資訊所需的步驟,以此來總結我對 MVP 的討論。再次重申,我將首先編寫一個測試,來描述我所希望觀察到的行為。(請參閱圖 11)。

如上所述,我將利用 NMock 程式庫來建立任務和視圖介面的類比。此特定測試將通過向服務層請求表示特定客戶的 DTO 來驗證表示器的行為。表示器從服務層檢索到 DTO 後,它將直接更新視圖上的屬性,這樣視圖就不必瞭解任何有關如何正確顯示對象資訊的知識。簡便起見,我將不再討論 WebLookupList 控制項上 SelectedItem 屬性的實現;相反,我會將它留給您去檢查原始碼,以瞭解實現的詳細資料。此測試真正展示的是在表示器從服務層檢索 CustomerDTO 後,表示器和視圖之間發生的互動。如果現在嘗試運行測試,我將面臨一個嚴重的失敗,因為視圖介面上的許多屬性都還不存在。因此,我將繼續進行並為 IViewCustomerView 介面添加必要的成員, 12 所示。

這些介面成員添加完成之後,我的 Web Form 也許會抱怨,因為它不再滿足介面協議了,所以我必須返回 Web Form 的原始碼並實現其餘的成員。如上所述,Web 頁的整個標記已經建立,同時表格儲存格已被標記為 "runat=server" 屬性,並且已根據其應顯示的資訊進行了命名。這樣就可以使結果代碼非常輕鬆的實現介面成員:

public string CompanyName{set { this.companyNameLabel.InnerText = value; }}public string ContactName{set { this.contactNameLabel.InnerText = value; }}...

隨著 setter 屬性的實現,現在只剩下最後一件事要完成。我需要一種方法來告訴表示器顯示選定客戶的資訊。回頭看看測試,您會發現此行為的實現位於表示器的 DisplayCustomerDetails 方法中。但是,此方法不帶有任何參數。調用時,表示器將返回視圖,從中提取其所需的任何資訊(使用 ILookupList 檢索),然後使用該資訊檢索選定客戶的詳細資料。從 UI 角度看,我需要做的就是將 DropDownList 的 AutoPostBack 屬性設定為 true,我還需要將以下事件處理常式掛鈎代碼添加到頁面的 OnInit 方法中:

protected override void OnInit(EventArgs e){base.OnInit(e);presenter = new ViewCustomerPresenter(this);this.customerDropDownList.SelectedIndexChanged += delegate{presenter.DisplayCustomerDetails();};}

此事件處理常式可確保在下拉式清單中選擇新客戶時,視圖將請求表示器顯示該客戶的詳細資料。

重要的是注意這是典型行為。當視圖請求表示器執行操作時,它不會給予任何特定的詳細資料,並且將由表示器來決定是否返回視圖,並使用視圖介面來擷取其所需的任何資訊。圖 13 顯示實現表示器中所需行為的代碼。

希望您現在可以瞭解添加表示器層的價值了。表示器負責嘗試檢索需要顯示其詳細資料的客戶 ID。這就是通常在原始碼中執行的代碼,但是它現在位於類中,我可以在任何錶示層技術以外對其進行完全的測試和實踐。

如果表示器能夠從視圖中檢索有效客戶 ID,則它將轉向服務層並請求表示該客戶詳細資料的 DTO。表示器獲得 DTO 後,它將使用 DTO 中包含的資訊更新視圖。要注意的關鍵一點是視圖介面的簡單性,除 ILookupList 介面以外,視圖介面完全由字串 DataTypes 組成。表示器的最終職責是正確地轉換和格式化從 DTO 中檢索的資訊,這樣它就可以作為字串,實際被傳遞到視圖。雖然未在此例中說明,但表示器還可負責從視圖中讀取資訊,並將其轉換為服務層所期待的必要類型。

完成所有程式碼片段後,我現在就可以運行應用程式了。首次載入頁面時,我會獲得一個客戶列表,並且在 DropDownList 中顯示(未選中)第一個客戶。如果我選擇一個客戶,則會出現回傳,視圖與表示器之間發生互動,並且會使用相關的客戶資訊更新 Web 頁面。

返回頁首

未來計劃

Model View Presenter 設計模式實際上就是許多開發人員已經熟悉的模板視圖控制器的一個最新版本;兩者的主要區別是 MVP 真正將 UI 從應用程式的域/服務層中分離。雖然從需求角度看,此樣本十分簡單,但它可以協助您抽象化 UI 與應用程式其他層之間的互動。而且,現在您可瞭解多種方法:您可間接使用這些層來自動化的測試您的應用程式。隨著您對 MVP 模式的深入研究,我希望您可以找到其他方法,從原始碼中提取更多格式和條件邏輯,並將其置入可測試檢視/表示器互動模型中。

請將您的問題和意見發送至 mmpatt@microsoft.com。

Jean-Paul Boodhoo 是 ThoughtWorks 的一名進階 .NET 交付專家,他曾參與了許多使用 .NET 架構和各種靈活方法的企業級應用程式交付。他經常利用測試驅動開發提供有關使用 .NET 功能的示範。可通過 mailtio:bitwisejp@gmail.com 或 www.jpboodhoo.com/blog 聯絡 Jean-Paul。

本文摘自 MSDN Magazine 的 2006 年 8 月號。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.