天天看點

[轉]MVP 模式執行個體解析【轉】MVP 模式執行個體解析

【轉】MVP 模式執行個體解析

來自: http://www.tracefact.net/Software-Design/MVP-Pattern-Explained.aspx

作者: 張子陽

引言

可能有的朋友已經看過我翻譯的Jean-Paul Boodhoo的 模型-視圖-提供器 模式 一文了(如果沒有,建議你先看下再看這篇文章,畢竟這兩篇是緊密聯系的)。在那篇文章中,作者為了說明 MVP 的優點之一,易測性,引入了單元測試和NMock架構。可能有的朋友對這部分不夠熟悉,也因為本人翻譯水準有限,導緻看後感覺不夠明朗,是以我就補寫了這篇文章,對作者給出的範例程式作了些許簡化和整理,讓我們一步步地來實作一個符合MVP模式的Web頁面。

開始前的準備

在譯文中,作者使用了Northwind資料庫的Customer表來作為範例,這個表包含了太多的字段,而且字段類型缺乏變化,隻有一個自定義的Country類型,其餘均為String類型。這樣容易讓大家忽視掉MVP模式需要注意的一點,或者說是優勢之一:視圖部分,通常也就是一個Aspx頁面,向使用者顯示的資料類型隻有一種可能,就是字元串。即便你想向使用者顯示一個數字,比如金額,在顯示之前,也會要麼顯式、要麼隐式地轉換為了字元串類型;而對象的字段類型卻可能是多種多樣的。是以,View的接口定義隻包含String類型的Set屬性,而實際将各種類型向String類型轉換的工作,全部在提供器中完成。通過這樣的方式,頁面的CodeBehind将進一步簡潔,連格式轉換都移到了單獨的提供器類中了。如果上面的加粗的字型你一時不能領悟也不要緊,一點點看下去你自然會明白。

本文中,我們使用一個Book類作為我們的領域對象,它将包含 字元串、日期、數字三種類型,後面我們會看到它的代碼。本文的範例依然是以一個通過選擇Book清單的下拉框,來顯示Book的詳細資訊 的Web窗體頁面來作說明。

現在建立一個新的空解決方案,起名為 MVP-Pattern,我們開始吧。

Model(Service)層的實作

大家可能對譯文的圖1和圖3有點混淆,實際上圖1的Service層和圖3的Model層是同一個事物,它們的工作都是一樣的:實際的從資料庫(或者存儲檔案)中擷取資料、填充對象,然後傳回給提供器。

MVP.DTO 項目

我們先在解決方案下建立類庫項目 MVP.DTO,DTO代表着Data Transfer Object(資料傳輸對象),這個項目和通常三層、四層構架的業務對象(Business Object)很類似,注意DTO項目實際上不應該屬于Model層,它不會引用任何項目,但是因為各個層的項目都會引用它,是以我們在這裡先建立它:

這個項目包含這樣幾個類,首先是BookDTO,它代表着我們的Book對象,它的代碼如下:

public class BookDTO {

    private int id;              // 索引

    private string title;    // 标題

    private DateTime pubDate; // 出版日期

    private decimal price;       // 價格

    // 構造函數 及 Get屬性略...

}

接下來它還包含三個接口,這三個接口定義了 傳送 給頁面上下拉框(DropDownList)的資料,以及如何為下拉框 送資料。可能正是因為它們的目的是 資料傳送 ,而不僅僅是将資料庫表映射成業務對象,是以才會稱之為DTO,而非Business Object吧。我們一個個來看下:

首先,我們想一想DropDownList的每個清單項ListItem需要什麼資料?當然是一個Text,一個Value了,是以定義第一個接口 ILookupDTO,它代表了ListItem所需的資料,隻定義了這兩個屬性:

public interface ILookupDTO {

    string Value { get; }    // 擷取值

    string Text { get; }     // 擷取文本

}

接着,給出了一個它的簡單實作 SimpleLookupDTO :

public class SimpleLookupDTO : ILookupDTO {

    private string value;

    private string text;

    public SimpleLookupDTO(string value, string text) {

       this.value = value;

       this.text = text;

    }

    public string Value {

       get { return value; }

    }

    public string Text {

       get { return text; }

    }

}

NOTE:如果是我,我會将之命名為IListItemDTO,但是這篇文章和譯文聯系甚密,是以我盡量保持和譯文一樣的命名

接下來,我們還要要為頁面上的DropDownList傳送資料,是以再定義接口ILookupList:

public interface ILookupList{

    void Add(ILookupDTO dto);       // 添加項目

    void Clear();                   // 清除所有項目

    ILookupDTO SelectedItem{get;}       // 獲得選中項目

}

在 MVP.DTO 項目中隻定義了這個接口,但沒有給出它的實作,因為它的實作顯然和UI層很靠近,是以它的實作我們将它放到後面的 MVP.WebControls 項目(UI層)中。

最後是ILookupCollection接口及其實作。這裡,我不得不批判一下這個接口的命名,它很容易讓人困惑:因為List是一個集合,Collection也是一個集合,是以第一眼感覺就是ILookupCollection和 ILookupList應該是同一個事物,但是這裡同時出現,讓人摸不着頭腦。實際上它們是完全不同的:

  • ILookupList 更多的是描述了一個事物,即是頁面上的DropDownList,它定義的方法也是對其本身進行操作的。
  • ILookupCollection 描述的是一個行為,它僅包含一個方法,BindTo(),方法接收的參數正是ILookupList,意為将ILookupCollection的資料綁定到 ILookupList上。而ILookupCollection包含的資料,是ILookupDTO的集合(IList<ILookupDTO>,由類型外部通過構造函數傳入)。

public interface ILookupCollection {

    void BindTo(ILookupList list);

}

public class LookupCollection : ILookupCollection {

    private IList<ILookupDTO> items;

    public LookupCollection(IEnumerable<ILookupDTO> items) {

       this.items = new List<ILookupDTO>(items);  // 根據傳遞進來的items建立新的清單

    }

    public int Count {

       get { return items.Count; }     // 擷取項目數

    }

    // 将項目綁定到清單

    public void BindTo(ILookupList list) {

       list.Clear();                           // 先清空清單

       foreach (ILookupDTO dto in items) {     // 周遊集合,綁定到清單中

           list.Add(dto);

       }

    }

}

到這裡 MVP.DTO 項目就結束了,我們再來看一下大家都熟悉的資料通路層,MVP.DataAccess。

MVP.DataAccess 項目

這一是和資料最接近的一層,用來擷取來自資料庫(或者其它存儲)的資料。因為本文的目的是講述MVP模式的構架,我們不需要把注意力集中在資料通路上,是以這一層我直接HardCode了,而非從資料庫中擷取。

這一層定義了一個接口 IBookMapper:

public interface IBookMapper {

    IList<BookDTO> GetAllBooks();           // 擷取所有Book

    BookDTO FindById(int bookId);           // 擷取某一Id的Book

}

以及一個實作了此接口的BookMapper類:

public class BookMapper :IBookMapper {

    private readonly IList<BookDTO> list;

    public BookMapper() {

       list = new List<BookDTO>();

       BookDTO book;

       book = new BookDTO(1, "Head First Design Patterns", new DateTime(2007, 9, 12), 67.5M);

       list.Add(book);

       // 略... 共添加了若幹個

    }

    public IList<BookDTO> GetAllBooks() {

       return new List<BookDTO>(this.list);

    }

    public BookDTO FindById(int bookId) {

       foreach (BookDTO book in list) {

           if (book.Id == bookId)

              return new BookDTO(book.Id, book.Title, book.PubDate, book.Price);

       }

       return null;      // 沒有找到則傳回Null

    }

}

NOTE:這裡有一個技巧,在GetAllBooks()和FindById()方法中,我沒有直接傳回list清單,或者是list中的book項目,而是對它們進行了深度複制,傳回了它們的副本。這樣是為了避免在類型外部通過引用類型變量通路類型内部成員。更多内容可以參考我之前寫的 建立常量、原子性的值類型 一文(Effective C#的筆記)。

MVP.Task 項目

MVP.Task 項目是Model層的核心,之前建立的兩個項目都是為這個項目進行服務的。它包含一個接口 IBookTask,這個接口定義了Task的兩個主要工作:1、傳回所有的Book清單(用于綁定DropDownList清單);2、根據某一個Book的Id傳回該Book的詳細資訊。

public interface IBookTask {

    ILookupCollection GetBookList();        // 傳回圖書清單

    BookDTO GetDetailsForBook(int bookId);     // 傳回某一圖書

}

我覺得這個接口的定義是MVP模式的精華所在之一,GetDetailsForBook()方法很容易了解,我們幾乎現在就可以猜到它會把工作委托給MVP.DataAccess項目的BookMapper去處理,因為BookMapper已經包含了類似的方法FindById()。關鍵就在于 GetBookList()方法,注意它傳回的是ILookupCollection,而非一個IList<BookDTO>。這樣我們在後面将介紹的提供器中,隻需要在擷取到的ILookupCollection上調用BindTo方法,然後傳遞清單對象,就可以綁定清單了,實作了Web頁面和CodeBehind邏輯的分離(MVP模式的精要所在);而如果這裡我們僅僅傳回IList<Book>,那麼綁定清單的工作勢必要移交給上一層去處理。

接下來我們面臨了一個問題:MVP.DataAccess 項目中的 BookMapper.GetAllBook()方法傳回的是 IList<Book>,而這裡需要的是一個ILookupCollection。回頭看一下ILookupCollection的實作,它内部維護的是一個IList<ILookupDTO>,ILookupDTO是業務無關的,它包含了Text和Value屬性用于向頁面上的DropDownList的清單項提供資料。在本例中,ILookupDTO的Text應該為書名,而Value應該為書的Id。這樣,我們最好能建立一個Converter類,能夠進行由BookDTO到ILookupDTO,進而由IList<BookDTO> 到 IList<ILookupDTO>的轉換。最後将轉換好的IList<ILookupDTO>作為參數傳遞給ILookupCollection的構造函數,進而得到一個ILookupCollection。

注意到ILookupDTO是業務無關的,是以我們定義接口名稱,為ObjectToLookupConverter,而非BookToLookupConverter。另外,以後我們可能建立其他的類型,比如Customer(客戶)也能轉換為LookupDTO,我們定義一個泛型接口(使得Converter類不限于BookDTO才能使用):

public interface IObjectToLookupConverter<T> {

    // 将 T類型的對象obj 轉換為 ILookupDTO類型

    ILookupDTO ConvertFrom(T obj);

    // 将 IList<T> 類型的對象清單 轉換為 IList<ILookupDTO> 類型

    IList<ILookupDTO> ConvertAllFrom(IList<T> obj);

}

再定義一個抽象基類實作這個接口,抽象類實作接口的ConvertAllFrom()方法,并将其中中實際的轉換工作委托給 ConvertFrom() 方法:

public abstract class ObjectToLookupConverter<T> : IObjectToLookupConverter<T> {

    public abstract ILookupDTO ConvertFrom(T obj);

    public IList<ILookupDTO> ConvertAllFrom(IList<T> objList) {

       List<T> list = new List<T>(objList);

       return list.ConvertAll<ILookupDTO>(delegate(T obj) {

           return ConvertFrom(obj);     // 将實際的轉換委托給 ConvertFrom()方法

       });

    }

}

最後,到了實際的将 Book 轉換為 LookupDTO 的部分了,非常的簡單:

public sealed class BookToLookupConverter : ObjectToLookupConverter<BookDTO> {

    public override ILookupDTO ConvertFrom(BookDTO book) {

       return new SimpleLookupDTO(book.Id.ToString(), book.Title);

    }

}

好了,有了這些準備工作,我們實作 IBookTask接口就變得輕易的多了。現在,建立MVP.Task項目的最後一個類,BookTask。注意GetBookList()方法的實作過程,和我們上面的分析一模一樣:

public class BookTask : IBookTask {

    private readonly IBookMapper bookMapper;

    public BookTask()

       : this(new BookMapper()) {

    }

    public BookTask(IBookMapper bookMapper) {

       this.bookMapper = bookMapper;

    }

    // 擷取圖書清單

    public ILookupCollection GetBookList() {

       IList<BookDTO> bookList = bookMapper.GetAllBooks();// 擷取IList<BookDTO>

       IList<ILookupDTO> list = // 轉換為 IList<ILookupDTO>

           new BookToLookupConverter().ConvertAllFrom(bookList);

       // 建構ILookupCollection

       ILookupCollection collection = new LookupCollection(list);

       return collection;

    }

    // 擷取某一圖書的詳細資訊

    public BookDTO GetDetailsForBook(int bookId) {

       BookDTO book = bookMapper.FindById(bookId);

       return book;

    }

}

至此,Model層或者叫Service服務層的所有項目都已經結束了,我們接下來看MVP的V(View層)是如何建構的。

View 層的實作

Web 站點項目 和 MVP.WebControl 項目

你可能會奇怪為什麼現在就講述View層,而不是Presenter提供器層?這是因為Presenter是View 和 Model的一個協調者,從下面幅圖就可以看出來。是以,我們需要先看下View層如何實作,進而才能去讨論Pesenter層。

View層包含兩個項目,一個是站點項目,一個是MVP.WebControl項目,我們先看站點項目。它僅包含一個頁面:Default.aspx,内容也是簡單之極,我們先看頁面部分的HTML代碼:

<h1>MVP 模式範例</h1>

選擇圖書<asp:DropDownList runat="server" ID="ddlBook"></asp:DropDownList>

<br /><br />

<div style="line-height:140%;">

    <strong>書名:</strong><asp:Literal ID="ltrTitle" runat="server"></asp:Literal><br />

    <strong>出版日期:</strong><asp:Literal ID="ltrPubDate" runat="server"></asp:Literal><br />

    <strong>價格:</strong><asp:Literal ID="ltrPrice" runat="server"></asp:Literal>

</div>

非常的簡單,是吧?然後我們再看一下後置代碼,通常情況下,我們會在後置代碼中寫DropDownList的PostBack事件,并且設定根據得到的資料填充三個Literal控件的Text屬性。而在MVP模式中,這部分的工作将會交由提供器來完成,是以,我們隻需要為這些控件建立Set通路器,并且将頁面的引用傳給提供器就可以了(如何傳遞頁面引用給提供器後面會讨論)。我們現在在頁面的後置代碼中添加一組Set屬性,分别去為頁面的三個Literal控件指派:

public string Title {

    set { ltrTitle.Text = value; }

}

public string Price {

    set { ltrPrice.Text = value; }

}

public string PubDate {

    set { ltrPubDate.Text = value; }

}

通常情況下DropDownList的填充也是在後置代碼中完成的,而為了能讓提供器對DropDownList的資料進行填充,我們需要讓這個DropDownList能夠與ILookupList聯系起來,并進一步通過調用來自MVP.Task中的 ILookupCollection的BindTo()方法,來對清單進行綁定。

記得到現在為止我們都沒有實作 ILookupList接口,現在是時候實作它了,建立一個項目MVP.WebControl,添加對MVP.DTO的引用,然後建立ILookupList接口的實作WebLookupList。在對ILookupList接口的實作中,對DropDownList進行包裝,為了更好的代碼重用,我們傳遞DropDownList的基類ListControl,而非DropDownList本身:

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

    }

    public void Clear() {

       underlyingList.Items.Clear();

    }

    public ILookupDTO SelectedItem {

       get {

           ListItem item = underlyingList.SelectedItem;

           return new SimpleLookupDTO(item.Value, item.Text);

       }

    }

}

可以看到我們實際上将對這個接口實作的具體工作都委托給了 ListControl,這樣,當我們在ILookupList上調用Add()方法添加清單項時,便會添加到頁面的DropDownList上。

記住:我們期望能讓提供器送資料的所有Web頁面上的控件,都應該為提供器提供一個入口。在前面,我們為三個Literal空間提供的入口是Set屬性。這裡我們一樣需要提供一個Get屬性,來讓提供器能夠獲得一個ILookupList。在Default頁面的後置代碼中添加下面代碼:

public ILookupList BookList {

    get { return new WebLookupList(ddlBook); }

}

Presenter 層的實作

實作Presenter(提供器)之前我們先考慮它的作用是什麼:從Task中擷取資料,然後送到View層(Aspx頁面)中。這就暗示 提供器必須包含 Task和 View層的引用。但是如果我們是無法讓提供器引用站點項目的,因為站點項目不會生成單獨的dll檔案(基于每個頁面生成dll)。但是站點卻可以引用提供器,是以我們隻要在提供器項目中定義一個接口,然後讓頁面去實作這個接口,我們通過這個接口去為頁面送資料(調用接口的Set通路器)。

MVP.Presentation 項目

現在你可以将頁面上的三個Literal和一個DropDownList與這個View接口聯系起來了。建立MVP.Presentation項目,然後我們定義Default頁面需要實作的IViewBookView接口:

public interface IViewBookView {

    ILookupList BookList { get; }

    string Title { set; }

    string PubDate { set; }

    string Price { set; }

}

這個接口的定義完全是基于Web頁面的,你需要為頁面提供哪些資料,或者為哪個控件送資料,那麼就定義哪些屬性。然後我們讓Web項目引用MVP.Presentation項目,在修改頁面的後置代碼檔案Default.aspx.cs,讓它去實作這個接口(因為頁面已經包含了這個接口的所有定義,是以這裡隻是起到一個向提供器傳遞窗體的作用)。

public partial class _Default : System.Web.UI.Page, IViewBookView

下一步,我們要實作提供器,我們在項目中再添加一個檔案 ViewBookPresenter.cs,添加下面代碼:

public class ViewBookPresenter {

    private readonly IViewBookView view;

    private readonly IBookTask task;

    public ViewBookPresenter(IViewBookView view) : this(view, new BookTask()) { }

    public ViewBookPresenter(IViewBookView view, IBookTask task) {

       this.view = view;

       this.task = task;

    }

    // 初始化方法,綁定清單

    public void Initialize() {

       ILookupCollection collection = task.GetBookList(); // 擷取圖書清單

       collection.BindTo(view.BookList);   // 綁定到清單

       DisplayBookDetails();               // 顯示圖書資訊

    }

    // 擷取選中的圖書的Id

    private int? SelectedBookId {

       get {

           string selectedId = view.BookList.SelectedItem.Value;

           if (String.IsNullOrEmpty(selectedId)) return null;

           int? id = null;

           try {

              id = int.Parse(selectedId.Trim());

           } catch (FormatException) { }

           return id;

       }

    }

    // 顯示特定圖書的詳細資訊

    public void DisplayBookDetails() {

       int? bookId = SelectedBookId;

       if (bookId.HasValue) {

           BookDTO book = task.GetDetailsForBook(bookId.Value);

           UpdateViewFrom(book);

       }

    }

    // 更新頁面的資訊,在這裡進行格式化

    private void UpdateViewFrom(BookDTO book) {

       view.Price = book.Price.ToString("c");         

       view.PubDate = String.Format(new DateFomatter(), "{0}", book.PubDate);

       view.Title = book.Title;

    }

    // 格式日期,作為示範,所有格式化工作都放到 Presenter中

    private class DateFomatter : ICustomFormatter, IFormatProvider {

       public string Format(string format, object arg, IFormatProvider formatProvider) {

           DateTime date = (DateTime)arg;

           return string.Format("{0}年{1}月{2}日", date.Year, date.Month, date.Day);        

       }

       public object GetFormat(Type formatType) {

           return this;

       }

    }

}

上面的代碼是很直白的,隻有一個主題思想:從task中擷取資料,然後調用view接口的屬性,或者從view接口獲得DropDownList的引用(通過ILookupList),然後通過 BindTo()方法為清單填充資料。注意到Initialize()方法,它為清單填充資料,這個應該在頁面加載之前就被調用;還有DisplayBookDetails()方法,它應該在清單的SelectedIndexChanged事件被觸發時調用,是以我們還有最後一部沒有做,再次修改Default.aspx.cs檔案,設定這些方法的觸發時機。

最後一步,再次修改Default.aspx.cs檔案

在後置代碼類中添加如下代碼,完成上一小節說明的所有内容:

private ViewBookPresenter presenter;

protected override void OnInit(EventArgs e) {

    base.OnInit(e);

    presenter = new ViewBookPresenter(this);       // 建立Presenter的執行個體

    // 為DropDownList綁定事件處理方法

    ddlBook.SelectedIndexChanged += delegate {

       presenter.DisplayBookDetails();

    };

}

protected void Page_Load(object sender, EventArgs e) {

    if (!IsPostBack) {

       presenter.Initialize();      // 綁定清單

    }

}

這裡值得注意的是 ViewBookPresenter 對象的建立,它通過this關鍵字,将頁面本身傳遞了進去,而頁面本身實作了IViewBookView接口,滿足構造函數的簽名,這樣提供器通過IViewBookView便可以通路頁面上的屬性和清單,并為之提供資料。

總結

這篇文章是對 模型-視圖-提供器 模式 一文範例程式的一個刨析和說明。在本文中,我們建立了一個包含多個項目的完整的符合MVP模式的Web頁面。我們先建立了基礎項目 MVP.DTO,用于傳送資料、MVP.DataAccess,用于資料通路;接着分别建立了 Model層、View層、Presenter層,并講述了它們之間的調用關系,以及使用的要點。通過這則範例,希望大家能對MVP模式有了一定的認識和了解。

不一定項目的每個頁面,都去采用MVP模式來建構。但如果運用的好的話,可以将多個頁面共同的的某一部分(或者叫功能)抽象出來,使用同一個提供器,可以很大程度上實作代碼重用。另外也可以一個Page實作多個IView,将頁面功能分離成多個部分,需要使用哪個功能,就實作哪個IView,并使用相應的IViewPresenter進行初始化。

感謝閱讀,希望這篇文章能給你帶來幫助!