MVP設計模式

維基百科前端

Model-view-presenter (MVP)使用者接口設計模式的一種,被廣範用於便捷自動化單元測試和在呈現邏輯中改良分離關注點(separation of concerns)。web

  • Model 定義使用者接口所須要被顯示的資料模型,一個模型包含着相關的商業邏輯。
  • View 視圖爲呈現使用者接口的終端,用以表現來自 Model 的資料,和使用者命令路由再通過 Presenter 對事件處理後的資料。
  • Presenter 包含着元件的事件處理,負責檢索 Model 取得資料,和將取得的資料通過格式轉換與 View 進行溝通。

MVP 設計模式一般會再加上 Controller 作爲總體應用程序的後端程序工做。數據庫

百度百科後端

mvp 的全稱爲Model-View-Presenter,Model提供數據,View負責顯示,Controller/Presenter負責邏輯的處理。 MVP與MVC有着一個重大的區別:在MVP中View並不直接使用Model,它們之間的通訊是經過Presenter (MVC中的Controller)來進行的,全部的交互都發生在Presenter內部,而在MVC中View會直接從Model中讀取數據而不是經過 Controller。設計模式

1 MVC和MVP 服務器

MVP 是從經典的模式MVC演變而來,它們的基本思想有相通的地方:Controller/Presenter負責邏輯的處理,Model提供數據,View負 責顯示。做爲一種新的模式,MVP與MVC有着一個重大的區別:在MVP中View並不直接使用Model,它們之間的通訊是經過Presenter (MVC中的Controller)來進行的,全部的交互都發生在Presenter內部,而在MVC中View會從直接Model中讀取數據而不是經過 Controller。框架

在MVC裏,View是能夠直接訪問Model的!從而,View裏會包含Model信息,不可避免的還要包括一些 業務邏輯。 在MVC模型裏,更關注的Model的不變,而同時有多個對Model的不一樣顯示,及View。因此,在MVC模型裏,Model不依賴於View,可是 View是依賴於Model的。不只如此,由於有一些業務邏輯在View裏實現了,致使要更改View也是比較困難的,至少那些業務邏輯是沒法重用的。[1] ide

2解決MVC問題函數

在 MVP裏,Presenter徹底把Model和View進行了分離,主要的程序邏輯在Presenter裏實現。並且,Presenter與具體的 View是沒有直接關聯的,而是經過定義好的接口進行交互,從而使得在變動View時候能夠保持Presenter的不變,即重用! 不只如此,咱們還能夠編寫測試用的View,模擬用戶的各類操做,從而實現對Presenter的測試--而不須要使用自動化的測試工具。 咱們甚至能夠在Model和View都沒有完成時候,就能夠經過編寫Mock Object(即實現了Model和View的接口,但沒有具體的內容的)來測試Presenter的邏輯。 在MVP裏,應用程序的邏輯主要在Presenter來實現,其中的View是很薄的一層。所以就有人提出了Presenter First的設計模式,就是根據User Story來首先設計和開發Presenter。在這個過程當中,View是很簡單的,可以把信息顯示清楚就能夠了。在後面,根據須要再隨便更改View, 而對Presenter沒有任何的影響了。 若是要實現的UI比較複雜,並且相關的顯示邏輯還跟Model有關係,就能夠在View和Presenter之間放置一個Adapter。由這個 Adapter來訪問Model和View,避免二者之間的關聯。而同時,由於Adapter實現了View的接口,從而能夠保證與Presenter之 間接口的不變。這樣就能夠保證View和Presenter之間接口的簡潔,又不失去UI的靈活性。 在MVP模式裏,View只應該有簡單的Set/Get的方法,用戶輸入和設置界面顯示的內容,除此就不該該有更多的內容,毫不允許直接訪問 Model--這就是與MVC很大的不一樣之處。[1] 工具

3優勢

一、模型與視圖徹底分離,咱們能夠修改視圖而不影響模型

二、能夠更高效地使用模型,由於全部的交互都發生在一個地方——Presenter內部

三、咱們能夠將一個Presenter用於多個視圖,而不須要改變Presenter的邏輯。這個特性很是的有用,由於視圖的變化老是比模型的變化頻繁。

四、若是咱們把邏輯放在Presenter中,那麼咱們就能夠脫離用戶接口來測試這些邏輯(單元測試)[1] 

4缺點

因爲對視圖的渲染放在了Presenter中,因此視圖和Presenter的交互會過於頻繁。還有一點須要明白,若是Presenter過多地渲染了視 圖,每每會使得它與特定的視圖的聯繫過於緊密。一旦視圖須要變動,那麼Presenter也須要變動了。好比說,本來用來呈現Html的 Presenter如今也須要用於呈現Pdf了,那麼視圖頗有可能也須要變動。

 

微軟文檔

設計模式:Model View Presenter

Jean-Paul Boodhoo

 

隨 着 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 頁面。

相關文章
相關標籤/搜索