[译]模型-视图-提供器 模式
模型-視圖-提供器 模式
出處:http://msdn.microsoft.com/en-us/magazine/cc188690.aspx
引言
隨著像Asp.Net和Windows窗體這樣的用戶界面創(chuàng)建技術(shù)越來(lái)越強(qiáng)大,讓用戶界面層做多于它本應(yīng)做的事是很常見(jiàn)的。沒(méi)有一個(gè)清晰的職責(zé)劃分,UI層經(jīng)常淪為一個(gè)包含實(shí)際上應(yīng)屬于程序其他層的邏輯的容器。有一個(gè)稱(chēng)為 模型(Model)-視圖(View)-提供器(Presenter)(MVP)的設(shè)計(jì)模式,特別適合解決這個(gè)問(wèn)題。為了表明我的觀點(diǎn),我將為Northwind數(shù)據(jù)庫(kù)中的客戶建一個(gè)遵循MVP模式的顯示屏幕(display screen)。
為什么在UI層包含太多的邏輯是很糟糕的?在既不手動(dòng)運(yùn)行應(yīng)用程序,也不維護(hù)丑陋的自動(dòng)執(zhí)行UI組件的UI運(yùn)行者腳本(runner script)的情況下,位于應(yīng)用程序UI層中的代碼是非常難于調(diào)試的。雖然這本身就是一個(gè)很大的問(wèn)題,一個(gè)更大的問(wèn)題是在應(yīng)用程序的公共視圖之間會(huì)有大量的重復(fù)代碼。當(dāng)執(zhí)行某一特定業(yè)務(wù)的功能在UI層的不同部分之間拷貝,通常很難找到好的可選重構(gòu)方法。MVP設(shè)計(jì)模式使得將UI層中的邏輯和代碼 重構(gòu)為 更加易于測(cè)試的新型的、可重用的代碼 更加容易。
圖1演示了組成一個(gè)范例應(yīng)用程序的主要層。注意對(duì)于UI和表現(xiàn)(Pesentation)有著各自的包(Package)。你可能會(huì)想它們是一樣的,但是實(shí)際上項(xiàng)目中的UI層應(yīng)該只包含各種不同的UI元素――窗體和控件。典型地,在一個(gè)Web窗體項(xiàng)目中是Asp.Net Web窗體、用戶控件、服務(wù)器控件的集合;在Windows項(xiàng)目中,它是Windows 窗體、用戶控件以及第三方庫(kù)(Libraries)的集合。這一額外的層就是將顯示和邏輯分隔開(kāi)的層。在表現(xiàn)層,你擁有實(shí)際上實(shí)現(xiàn)UI行為的對(duì)象――諸如驗(yàn)證顯示,從UI層收集用戶輸入 等等。
圖1.應(yīng)用程序構(gòu)架
遵循MVP
如同你在 圖2 中所見(jiàn)的,這個(gè)項(xiàng)目的UI相當(dāng)標(biāo)準(zhǔn)。當(dāng)頁(yè)面加載時(shí),屏幕將會(huì)顯示一個(gè)包含Northwind數(shù)據(jù)庫(kù)中所有客戶的下拉框。如果你從下拉框中選擇一個(gè)客戶,頁(yè)面會(huì)更新為這個(gè)客戶的信息。通過(guò)遵循MVP設(shè)計(jì)模式,你可以從UI中將行為(Behavior)重構(gòu)到它們自己的類(lèi)中。圖3顯示了一個(gè)類(lèi)圖,它說(shuō)明了參與其中的各個(gè)不同類(lèi)之間的聯(lián)系。
圖2. 用戶信息
圖3. MVP類(lèi)圖
注意到提供器對(duì)于應(yīng)用程序?qū)嶋H的UI層一無(wú)所知非常重要。它知道它可以同接口對(duì)話,但是它不知道也不關(guān)心接口的實(shí)現(xiàn)是什么。這提升了在完全不同的UI技術(shù)間提供器的重用。
我將使用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)創(chuàng)建客戶界面的功能。代碼4 演示了第一次測(cè)試的細(xì)節(jié),我將通過(guò)這個(gè)測(cè)試來(lái)描述我期望在頁(yè)面加載時(shí)觀察到的行為。TDD讓我每次關(guān)注于一個(gè)問(wèn)題,僅編寫(xiě)可以讓測(cè)試通過(guò)的代碼,然后繼續(xù)進(jìn)行下面的工作。在測(cè)試中,我將會(huì)利用一個(gè)稱(chēng)為NMork2 的偽對(duì)象框架,它允許我創(chuàng)建接口的偽實(shí)現(xiàn)(mock implementation)。
代碼4.第一個(gè)測(cè)試
[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();
}
在我的MVP實(shí)現(xiàn)中,我決定提供器將作為視圖所要與之工作的依賴(lài)。通常,創(chuàng)建對(duì)象使之處于可以立刻進(jìn)行工作的狀態(tài)是一種好的做法。在這個(gè)應(yīng)用程序中,表現(xiàn)層依賴(lài)于服務(wù)層,實(shí)際上由服務(wù)層調(diào)用領(lǐng)域功能(domain functionality)。因?yàn)檫@個(gè)需求,創(chuàng)建一個(gè)含有可以與服務(wù)類(lèi)對(duì)話的接口的提供器也是有意義的。這樣確保了一旦提供器創(chuàng)建好了,它就已經(jīng)準(zhǔn)備好做它需要做的所有工作了。我以創(chuàng)建兩個(gè)特定的mocks作為開(kāi)始:一個(gè)用于服務(wù)層,一個(gè)用于提供器將與協(xié)作的視圖。
為什么使用mocks?單元測(cè)試的一個(gè)規(guī)則就是盡可能地隔離測(cè)試以便集中于某一特定的對(duì)象。在這個(gè)測(cè)試中,我只關(guān)心提供器所期待的行為。目前我并不關(guān)心view接口或者service接口的實(shí)際實(shí)現(xiàn)。我信任由這些接口定義的契約(contract),并且設(shè)置mocks去相應(yīng)運(yùn)作(behave)。這樣確保了我的測(cè)試僅僅圍繞著我對(duì)提供器所期望的行為,而不是它所依賴(lài)的任何東西。我期望的,在提供器的初始化方法被調(diào)用后所表現(xiàn)的行為如下:
首先,提供器應(yīng)該調(diào)用一次服務(wù)層ICustomerTask對(duì)象(已經(jīng)在測(cè)試中Mock了)的GetCustomerList方法。注意通過(guò)使用NMock,我可以模擬Mock的行為。以服務(wù)層來(lái)說(shuō),我想要返回一個(gè)ILookupCollection給提供器。然后,在提供器從服務(wù)層收到ILookupCollection以后,它可以調(diào)用集合的BindTo方法 并且向方法傳遞一個(gè)ILookupList方法的實(shí)現(xiàn)。通過(guò)使用NMockExpect。一旦我可以確定方法,如果提供器沒(méi)有調(diào)用這個(gè)方法一次并且只一次,那么測(cè)試將會(huì)失敗。
寫(xiě)完測(cè)試以后,我處于一個(gè)完全不可編譯的狀態(tài)。我將要做一些可能的最簡(jiǎn)單的事讓測(cè)試通過(guò)。
讓第一個(gè)測(cè)試通過(guò)
先寫(xiě)一個(gè)測(cè)試的好處之一是我現(xiàn)在有了一個(gè)我可以遵循的使得測(cè)試編譯并最終通過(guò)的藍(lán)圖(這個(gè)測(cè)試)。第一個(gè)測(cè)試還有兩個(gè)尚不存在的接口。這些接口是代碼正確通過(guò)編譯的第一個(gè)先決條件。我們將以IViewCustomerView的代碼作為開(kāi)始:
public interface IViewCustomerView {
??? ILookupList CustomerList { get; }
}
這個(gè)接口暴露一個(gè)返回ILookupList接口實(shí)現(xiàn)的屬性。我還沒(méi)有ILookupList接口或是它的一個(gè)實(shí)現(xiàn),就此而言。出于使測(cè)試通過(guò)的目的,我不需要一個(gè)顯示的實(shí)現(xiàn),所以我可以這樣去創(chuàng)建ILookupList接口:
public interface ILookupList { }
ILookupList接口現(xiàn)在看上去相當(dāng)?shù)臎](méi)用。我的目標(biāo)是使得測(cè)試編譯并且通過(guò),并且這些接口滿足測(cè)試的需要。現(xiàn)在是時(shí)候?qū)⒔裹c(diǎn)轉(zhuǎn)移到我們實(shí)際要進(jìn)行測(cè)試的對(duì)象上了――ViewCustomerPresenter。這個(gè)類(lèi)現(xiàn)在還不存在,但是看下測(cè)試,你可以發(fā)現(xiàn)關(guān)于它的兩個(gè)要點(diǎn):它有一個(gè)既需要視圖實(shí)現(xiàn)也需要服務(wù)實(shí)現(xiàn)作為依賴(lài)的構(gòu)造函數(shù),并且它有一個(gè)無(wú)返回值的初始化方法。代碼5 演示了如何使測(cè)試通過(guò)編譯:
代碼5. 編譯這個(gè)測(cè)試
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();
??? }
}
應(yīng)該記得,為了讓提供器有意義地工作,它需要獲得它的所有依賴(lài);這就是為什么傳遞視圖和服務(wù)進(jìn)去。我沒(méi)有實(shí)現(xiàn)初始化方法,所以如果我運(yùn)行這個(gè)測(cè)試我會(huì)得到一個(gè)NotImplementedException 異常。
如果我已經(jīng)提到的,我不會(huì)盲目地對(duì)提供器進(jìn)行編碼;我已經(jīng)知道,通過(guò)觀察這個(gè)測(cè)試,在初始化方法被調(diào)用時(shí),提供器應(yīng)該顯示出什么樣的行為。這個(gè)行為的實(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è)試提供器的角度來(lái)說(shuō),我不需要知道是否有一個(gè)可以工作的實(shí)現(xiàn)。正是這種級(jí)別的抽象允許我在提供器類(lèi)的測(cè)試中穿行。第一個(gè)測(cè)試現(xiàn)在處于可以編譯并運(yùn)行的狀態(tài)。這證明了當(dāng)提供器的初始化方法被調(diào)用,它將會(huì)以一種我在測(cè)試中所指定的方式與它所依賴(lài)的類(lèi)型進(jìn)行交互,并且最終,當(dāng)這些依賴(lài)的具體實(shí)現(xiàn)注入到提供器中,我可以確定結(jié)果視圖(ASPX頁(yè)面)將會(huì)由客戶列表所填充。
填充 DropDownList
迄今為止,我主要在處理接口以便將實(shí)際的實(shí)現(xiàn)細(xì)節(jié)抽象出來(lái)、將注意力集中在提供器上。現(xiàn)在是時(shí)候通過(guò)一種可測(cè)試的方式創(chuàng)建一些底層代碼(plumbing),這些底層代碼將最終允許提供器在Web頁(yè)面上填充一個(gè)列表。完成這個(gè)工作的關(guān)鍵是將發(fā)生在LookupCollection類(lèi)的BindTo方法中的交互。如果你看下 代碼6 中LookupCollection類(lèi)的實(shí)現(xiàn),你將注意到它實(shí)現(xiàn)了IlookupCollection接口。這篇文章的源碼含有附帶的測(cè)試,用于創(chuàng)建LookupCollection類(lèi)的功能。
代碼6. LookupCollection類(lèi)
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);
??? }
}
BindTo方法的實(shí)現(xiàn)值得特別注意。注意到在這個(gè)方法中,集合遍歷了它自己的私有ILookupDTO 列表的實(shí)現(xiàn)。ILookupDTO是一個(gè)接口,它迎合了UI層的綁定下拉框。
public interface ILookupDTO {
??? string Value { get; }??
??? string Text { get; }
}
代碼7 演示了測(cè)試lookup集合的BindTo方法的代碼,這有助于解釋LookupCollection和IlookupList之間所期望的交互。最后一行值得特別注意。在這個(gè)測(cè)試中,我期望在試圖添加項(xiàng)目到列表之前,LookupCollection將會(huì)調(diào)用IlookupList實(shí)現(xiàn)的Clear方法。然后我期望Add方法在IlookupList上調(diào)用10次,并且LookupCollection將傳遞一個(gè)實(shí)現(xiàn)了ILookupDTO接口的對(duì)象,作為Add方法的一個(gè)參數(shù)。為了能夠?qū)嶋H工作在一個(gè)Web項(xiàng)目中的控件上(比如一個(gè)下拉列表),你將需要?jiǎng)?chuàng)建一個(gè)IlookupList的實(shí)現(xiàn),它知道如何與Web項(xiàng)目中的控件工作。
代碼7 一個(gè)描述行為的測(cè)試
[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);
}
這篇文章附帶的源碼中包含一個(gè)名為MVP.Web.Controls的項(xiàng)目。這個(gè)項(xiàng)目包含了我選擇創(chuàng)建的用于完成解決方案的任何基于Web的控件或者類(lèi)。為什么我要把代碼放到這個(gè)項(xiàng)目中,而沒(méi)有放在App_Code目錄或者Web項(xiàng)目本身中?易測(cè)性。在沒(méi)有手動(dòng)運(yùn)行應(yīng)用程序或者使用某種類(lèi)型的測(cè)試機(jī)器人自動(dòng)操作UI的情況下,居于Web項(xiàng)目中的任何東西都是難于直接測(cè)試的。MVP模式允許我在一個(gè)較高的層次上考慮抽象,并且測(cè)試核心接口(IlookupList和ILookupCollection)的實(shí)現(xiàn),而不需要手動(dòng)地運(yùn)行程序。我將在Web.Controls項(xiàng)目中添加一個(gè)新類(lèi),一個(gè)WebLookupList控件。代碼8 演示了這個(gè)類(lèi)的第一次測(cè)試:
代碼8. WebLookupList 控件的第一次測(cè)試
[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);
}
測(cè)試中關(guān)鍵的部分在代碼8中顯示了。這個(gè)測(cè)試項(xiàng)目顯然需要System.Web庫(kù)的一個(gè)引用,以便它可以初始化DropDownList Web控件。看下這個(gè)測(cè)試,你應(yīng)該看到WebLookupList類(lèi)將會(huì)實(shí)現(xiàn)IlookupList接口。它也將把ListControl作為一個(gè)依賴(lài)。在System.Web.UI.WebControls命名空間中的兩個(gè)最常見(jiàn)的ListControl的實(shí)現(xiàn)就是DropDownList和ListBox類(lèi)了。代碼8中的一個(gè)關(guān)鍵特色就是我確信WebLookupList正確的更新了Web ListControl的狀態(tài),它將職責(zé)委托給了這個(gè)Web ListControl。圖9 顯示了參與WebLookupList實(shí)現(xiàn)的類(lèi)的類(lèi)圖。通過(guò)代碼10,我可以滿足WebLookupList控件第一次測(cè)試的需求。
代碼10 WebLookupList 控件
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));
??? }
}
圖9. WebLookupList 類(lèi)
記得,MVP模式的要點(diǎn)之一是通過(guò)view接口的創(chuàng)建引入了各層之間的分離。提供器不知道某一視圖的具體實(shí)現(xiàn),以及它所要交互的IlookupList;它只知道它可以調(diào)用由這些接口所定義的任何方法。最終,WebLookupList是一個(gè)包裝了ListControl并且將職責(zé)委托給了ListControl(一些定義在System.Web.UI.WebControls項(xiàng)目中的ListControls的基類(lèi))的類(lèi)。在這些代碼完成好了以后,我現(xiàn)在可以編譯并運(yùn)行WebLookupList控件的測(cè)試了,它應(yīng)該可以通過(guò)。我可以為WebLookupList控件再添加一個(gè)測(cè)試來(lái)測(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è)試到,當(dāng)WebLookupList類(lèi)本身的方法被調(diào)用時(shí),實(shí)際上會(huì)改變它底層的ListControl(DropDownList)的狀態(tài)。WebLookupList現(xiàn)在完全擁有完成填充Web表單上一個(gè)DropDownList的特色了。現(xiàn)在是時(shí)候讓我把所有東西都結(jié)合到一起,然后讓客戶列表填充這個(gè)Web頁(yè)面的下拉框了。
實(shí)現(xiàn)View接口
因?yàn)槲艺趧?chuàng)建一個(gè)Web窗體前端(界面),將IViewCustomerView接口實(shí)現(xiàn)為一個(gè)Web窗體或者用戶控件將是有意義的。出于這個(gè)專(zhuān)欄的目的,我將創(chuàng)建一個(gè)Web窗體。如同你在 圖2 中所見(jiàn)到的,這個(gè)頁(yè)面大概的樣子已經(jīng)創(chuàng)建好了。現(xiàn)在我只需要實(shí)現(xiàn)View接口。切換到ViewCustomers.aspx頁(yè)面的后置代碼中,我可以添加下面的代碼,表示這個(gè)頁(yè)面需要實(shí)現(xiàn)IViewCustomersView接口:
public partial class ViewCustomers : Page,IViewCustomerView
如果你看一下代碼范例,你將會(huì)注意到Web項(xiàng)目和表現(xiàn)(Presentation)是兩個(gè)完全不同的程序集。同樣,表現(xiàn)項(xiàng)目沒(méi)有包含對(duì)Web.UI項(xiàng)目的任何引用,進(jìn)一步維持著層的分隔。另一方面,Web.UI項(xiàng)目必須包含一個(gè)對(duì)表現(xiàn)項(xiàng)目的一個(gè)引用,因?yàn)樗薞iew接口和提供器。
通過(guò)選擇實(shí)現(xiàn)IViewCustomerView接口,我們的Web頁(yè)面現(xiàn)在需要實(shí)現(xiàn)由那個(gè)接口所定義的任何方法和屬性。現(xiàn)在IViewCustomerView接口只有一個(gè)屬性,這是一個(gè)返回任何實(shí)現(xiàn)了ILookupList接口的只讀屬性。我添加了一個(gè)對(duì)Web.Controls項(xiàng)目的引用,以便我可以初始化WebLookupListControl。這樣做是因?yàn)閃ebLookupListControl實(shí)現(xiàn)了ILookupList接口,并且它知道如何(將工作)委托給實(shí)際的Asp.Net中的WebControls。看一下ViewCustomer頁(yè)面的Aspx文件,你將會(huì)看到客戶列表僅僅是一個(gè)簡(jiǎn)單的asp:DropDownList控件:
<td>Customers:</td>
<td><asp:DropDownList id="customerDropDownList" AutoPostBack="true"
??????? runat="server" Width="308px"></asp:DropDownList></td>
</tr>
這些已經(jīng)就位了,我們可以立刻繼續(xù)去實(shí)現(xiàn)滿足IViewCustomerView接口實(shí)現(xiàn)的代碼了:
public ILookupList CustomerList {
??? get { return new WebLookupList(this.customerDropDownList);}
}
我現(xiàn)在需要在提供器上調(diào)用初始化方法,這個(gè)方法將觸發(fā)它去做些實(shí)際的工作。為了完成這個(gè),視圖需要能夠初始化提供器,以便它的方法可以被調(diào)用。如果你回頭看下提供器,你將會(huì)記得它需要與視圖和服務(wù)工作。ICustomerTask代表一個(gè)居于應(yīng)用程序服務(wù)層中的一個(gè)接口。典型地服務(wù)層負(fù)責(zé)監(jiān)管領(lǐng)域?qū)ο箝g的交互,以及將這些交互的結(jié)果轉(zhuǎn)換成數(shù)據(jù)傳遞對(duì)象(DTOs),然后這些DTO對(duì)象從服務(wù)層傳遞到表現(xiàn)層,接著傳遞到UI層。然而,這里有一個(gè)問(wèn)題,我規(guī)定提供器需要視圖和服務(wù)的實(shí)現(xiàn)才能創(chuàng)建。
提供器實(shí)際的初始化將發(fā)生在Web頁(yè)面的后置代碼中。這是一個(gè)問(wèn)題,因?yàn)閁I項(xiàng)目不包含對(duì)服務(wù)層項(xiàng)目的引用。然而,表現(xiàn)層項(xiàng)目包含,它有一個(gè)對(duì)服務(wù)層項(xiàng)目的引用。這允許我通過(guò)在ViewCustomverPresenter中添加一個(gè)重載的構(gòu)造函數(shù)來(lái)解決這個(gè)問(wèn)題:
public ViewCustomerPresenter(IViewCustomerView view) : this(view, new CustomerTask()) {}
新的構(gòu)造函數(shù)滿足了提供器的需求:同時(shí)擁有視圖和服務(wù)的實(shí)現(xiàn),并且保持將UI層從服務(wù)層中分離出來(lái)。現(xià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();
}
注意到初始化提供器的關(guān)鍵是:我利用了我新創(chuàng)建的重載構(gòu)造函數(shù),并且Web窗體將它本身作為一個(gè)實(shí)現(xiàn)了View接口的對(duì)象進(jìn)行傳遞!
后置代碼已經(jīng)實(shí)現(xiàn),現(xiàn)在我可以生成并運(yùn)行應(yīng)用程序了。Web頁(yè)面上的DropDownList現(xiàn)在填充了客戶名稱(chēng)列表,而在后置代碼中不需要任何的數(shù)據(jù)綁定代碼。不僅如此,曾經(jīng)運(yùn)行的各個(gè)小部分的測(cè)試最終協(xié)同工作了,確保了表現(xiàn)層構(gòu)架將會(huì)如期望般運(yùn)作。
我將通過(guò)演示顯示一個(gè)在DropDownList中選中的客戶信息,把我關(guān)于MVP的討論聯(lián)系起來(lái)。再一次,我通過(guò)寫(xiě)一個(gè)描述了我希望觀察到的行為的測(cè)試作為開(kāi)始(看 代碼11)。
代碼11. 最后一個(gè)測(cè)試
[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();
}
和前面一樣,我利用NMock庫(kù)創(chuàng)建task和View接口的Mocks。這個(gè)特定的測(cè)試通過(guò)向服務(wù)層請(qǐng)求一個(gè)代表某一特定客戶的DTO,驗(yàn)證了提供器的行為。一旦提供器從服務(wù)層獲得DTO,它將直接更新視圖的屬性,這就避免了視圖需要知道如何正確地顯示來(lái)自對(duì)象的信息。為了簡(jiǎn)潔,我不打算去討論WebLookupList控件的SeletedItem屬性的實(shí)現(xiàn);然而,我將把它留給你,通過(guò)檢查源代碼來(lái)查看實(shí)現(xiàn)的細(xì)節(jié)。這個(gè)測(cè)試真正演示的是當(dāng)提供器從服務(wù)層收到一個(gè)Customer DTO 時(shí)發(fā)生在提供器和視圖之間的交互。如果我現(xiàn)在試圖運(yùn)行這個(gè)測(cè)試,我將會(huì)處于一個(gè)嚴(yán)重的錯(cuò)誤狀態(tài),因?yàn)楹芏嗟膶傩詖iew接口沒(méi)有定義。所以我將繼續(xù)為IViewCustomerView接口添加必要的成員,如同你在 代碼12 看到的:
代碼12. 完成 IViewCustomerView 接口
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;}
}
剛添加完這些接口成員,我的Web窗體就開(kāi)始抱怨了,因?yàn)樗辉贊M足接口的定義,所以我不得不回頭看下我的Web窗體的后置代碼,并且實(shí)現(xiàn)那些剩下的成員。如同前面所陳述的,Web頁(yè)面的整個(gè)標(biāo)記都已經(jīng)創(chuàng)建了,并讓那些標(biāo)記了“runat=server”的表格單元格根據(jù)將在它中所要顯示的信息來(lái)為它命名。這將使實(shí)現(xiàn)接口成員的代碼非常的輕易:
public string CompanyName{
??? set { this.companyNameLabel.InnerText = value; }
}
public string ContactName{
??? set { this.contactNameLabel.InnerText = value; }
}
...
實(shí)現(xiàn)了Set屬性訪問(wèn)器,還剩下一件事需要做。我需要有一種方式通知提供器,以便顯示選中客戶的信息。回頭看下測(cè)試,你可以看到這個(gè)行為的實(shí)現(xiàn)位于提供器的DisplayCustomerDetails方法上。然而,這個(gè)方法不會(huì)接受任何參數(shù)。當(dāng)調(diào)用時(shí),提供器將會(huì)回頭找視圖,從它中拖出任何所需要的信息(它通過(guò)使用ILookupList獲取),然后使用這些信息獲取所請(qǐng)求的客戶的詳細(xì)內(nèi)容。從UI的角度來(lái)看,我需要做的全部就是將DropDownList的AutoPostBack屬性設(shè)為T(mén)rue,我也需要添加下面的事件處理程序,和Page的OnInit方法掛接起來(lái)。
protected override void OnInit(EventArgs e)
{
??? base.OnInit(e);
??? presenter = new ViewCustomerPresenter(this);
??? this.customerDropDownList.SelectedIndexChanged += delegate{
??????? presenter.DisplayCustomerDetails();
??? };
}
這個(gè)事件處理程序確保,無(wú)論什么時(shí)候下拉框中的一個(gè)新的客戶被選中,視圖將會(huì)請(qǐng)求提供器顯示客戶的細(xì)節(jié)。
注意到這是一個(gè)典型的行為很重要。當(dāng)一個(gè)視圖請(qǐng)求提供器做一些事情,它不提供任何的特定細(xì)節(jié),而是由提供器去訪問(wèn)視圖,通過(guò)view接口獲取它所需要的任何信息。代碼13 顯示了實(shí)現(xiàn)提供器的行為所需要的代碼。
代碼13 完成提供器
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;
}
希望你現(xiàn)在已經(jīng)明白了添加提供器層的價(jià)值。試圖獲取一個(gè)客戶Id并且顯示它的詳細(xì)信息都是提供器的責(zé)任。這段代碼通常都是實(shí)現(xiàn)在后置代碼中,但是現(xiàn)在它位于一個(gè)類(lèi)中,這樣我就可以在任何的表現(xiàn)層技術(shù)之外,對(duì)它進(jìn)行完全的測(cè)試和演練(譯注:同一段代碼可以應(yīng)用于WinForm和WebForm,讓窗體都去實(shí)現(xiàn)view接口就可以了)。
在提供器從視圖中獲得一個(gè)正確的客戶Id的事件中,它轉(zhuǎn)向服務(wù)層并請(qǐng)求一個(gè)DTO,這個(gè)DTO代表了客戶的細(xì)節(jié)。一旦提供器擁有了DTO,它使用包含在DTO中的信息更新視圖。注意到一個(gè)關(guān)鍵點(diǎn)就是View接口很簡(jiǎn)潔;伴于ILookupList接口,view接口只包含了String 類(lèi)型。正確地轉(zhuǎn)換并且格式化由DTO獲取的信息,以便它可以以字符串形式提交給視圖,最終都是提供器的責(zé)任。雖然在范例中沒(méi)有演示,提供器也應(yīng)該負(fù)責(zé)從視圖中讀取信息,并且將它轉(zhuǎn)變?yōu)榉?wù)層所期望的必要類(lèi)型。
所有的小部分都已經(jīng)就位,現(xiàn)在我可以運(yùn)行應(yīng)用程序了。當(dāng)頁(yè)面第一次加載,我獲取了客戶的一個(gè)列表,并且第一個(gè)客戶(未選擇)顯示在DropDownList中。如果我選擇一個(gè)客戶,產(chǎn)生一個(gè)PostBack,視圖和提供器發(fā)生交互,使用相關(guān)的客戶信息更新頁(yè)面。
接下來(lái)是什么?
模型-視圖-提供器模式實(shí)際上僅僅是許多開(kāi)發(fā)者已經(jīng)熟悉的 模型-視圖-控制器 模式的更新。關(guān)鍵的變化是MVP完全將UI從應(yīng)用程序的 領(lǐng)域/服務(wù)層分離出來(lái)。盡管從需求角度來(lái)看,這個(gè)例子相當(dāng)?shù)暮?jiǎn)單,但它可以幫助你從你的應(yīng)用程序中將UI層與其他層的交互抽象出來(lái)。當(dāng)你深入鉆研到MVP模式中,我希望你可以找到其他方法將盡可能多的格式化和條件判斷邏輯從你的后置代碼中分離出來(lái),并將它們置于可測(cè)試的 視圖/提供器 交互模型中。
總結(jié)
以上是生活随笔為你收集整理的[译]模型-视图-提供器 模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C# is与as
- 下一篇: IOS开发中单例模式使用详解