引言
可能有的朋友已經(jīng)看過我翻譯的Jean-Paul Boodhoo的 模型-視圖-提供器 模式 一文了(如果沒有,建議你先看下再看這篇文章,畢竟這兩篇是緊密聯(lián)系的)。在那篇文章中,作者為了說明 MVP 的優(yōu)點(diǎn)之一,易測性,引入了單元測試和NMock框架。可能有的朋友對這部分不夠熟悉,也因?yàn)楸救朔g水平有限,導(dǎo)致看后感覺不夠明朗,所以我就補(bǔ)寫了這篇文章,對作者給出的范例程序作了些許簡化和整理,讓我們一步步地來實(shí)現(xiàn)一個符合MVP模式的Web頁面。
開始前的準(zhǔn)備
在譯文中,作者使用了Northwind數(shù)據(jù)庫的Customer表來作為范例,這個表包含了太多的字段,而且字段類型缺乏變化,只有一個自定義的Country類型,其余均為String類型。這樣容易讓大家忽視掉MVP模式需要注意的一點(diǎn),或者說是優(yōu)勢之一:視圖部分,通常也就是一個Aspx頁面,向用戶顯示的數(shù)據(jù)類型只有一種可能,就是字符串。即便你想向用戶顯示一個數(shù)字,比如金額,在顯示之前,也會要么顯式、要么隱式地轉(zhuǎn)換為了字符串類型;而對象的字段類型卻可能是多種多樣的。所以,View的接口定義只包含String類型的Set屬性,而實(shí)際將各種類型向String類型轉(zhuǎn)換的工作,全部在提供器中完成。通過這樣的方式,頁面的CodeBehind將進(jìn)一步簡潔,連格式轉(zhuǎn)換都移到了單獨(dú)的提供器類中了。如果上面的加粗的字體你一時不能領(lǐng)悟也不要緊,一點(diǎn)點(diǎn)看下去你自然會明白。
本文中,我們使用一個Book類作為我們的領(lǐng)域?qū)ο?,它將包?字符串、日期、數(shù)字三種類型,后面我們會看到它的代碼。本文的范例依然是以一個通過選擇Book列表的下拉框,來顯示Book的詳細(xì)信息 的Web窗體頁面來作說明。
現(xiàn)在創(chuàng)建一個新的空解決方案,起名為 MVP-Pattern,我們開始吧。
Model(Service)層的實(shí)現(xiàn)
大家可能對譯文的圖1和圖3有點(diǎn)混淆,實(shí)際上圖1的Service層和圖3的Model層是同一個事物,它們的工作都是一樣的:實(shí)際的從數(shù)據(jù)庫(或者存儲文件)中獲取數(shù)據(jù)、填充對象,然后返回給提供器。
MVP.DTO 項(xiàng)目
我們先在解決方案下創(chuàng)建類庫項(xiàng)目 MVP.DTO,DTO代表著Data Transfer Object(數(shù)據(jù)傳輸對象),這個項(xiàng)目和通常三層、四層構(gòu)架的業(yè)務(wù)對象(Business Object)很類似,注意DTO項(xiàng)目實(shí)際上不應(yīng)該屬于Model層,它不會引用任何項(xiàng)目,但是因?yàn)楦鱾€層的項(xiàng)目都會引用它,所以我們在這里先創(chuàng)建它:
這個項(xiàng)目包含這樣幾個類,首先是BookDTO,它代表著我們的Book對象,它的代碼如下:
public class BookDTO { private int id; // 索引 private string title; // 標(biāo)題 private DateTime pubDate; // 出版日期 private decimal price; // 價格 // 構(gòu)造函數(shù) 及 Get屬性略... }
接下來它還包含三個接口,這三個接口定義了 傳送 給頁面上下拉框(DropDownList)的數(shù)據(jù),以及如何為下拉框 送數(shù)據(jù)。可能正是因?yàn)樗鼈兊哪康氖?數(shù)據(jù)傳送 ,而不僅僅是將數(shù)據(jù)庫表映射成業(yè)務(wù)對象,所以才會稱之為DTO,而非Business Object吧。我們一個個來看下:
首先,我們想一想DropDownList的每個列表項(xiàng)ListItem需要什么數(shù)據(jù)?當(dāng)然是一個Text,一個Value了,所以定義第一個接口 ILookupDTO,它代表了ListItem所需的數(shù)據(jù),只定義了這兩個屬性:
public interface ILookupDTO { string Value { get; } // 獲取值 string Text { get; } // 獲取文本 }
接著,給出了一個它的簡單實(shí)現(xiàn) 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,但是這篇文章和譯文聯(lián)系甚密,所以我盡量保持和譯文一樣的命名
接下來,我們還要要為頁面上的DropDownList傳送數(shù)據(jù),所以再定義接口ILookupList:
public interface ILookupList{ void Add(ILookupDTO dto); // 添加項(xiàng)目 void Clear(); // 清除所有項(xiàng)目 ILookupDTO SelectedItem{get;} // 獲得選中項(xiàng)目 }
在 MVP.DTO 項(xiàng)目中只定義了這個接口,但沒有給出它的實(shí)現(xiàn),因?yàn)樗膶?shí)現(xiàn)顯然和UI層很靠近,所以它的實(shí)現(xiàn)我們將它放到后面的 MVP.WebControls 項(xiàng)目(UI層)中。
最后是ILookupCollection接口及其實(shí)現(xiàn)。這里,我不得不批判一下這個接口的命名,它很容易讓人困惑:因?yàn)長ist是一個集合,Collection也是一個集合,所以第一眼感覺就是ILookupCollection和 ILookupList應(yīng)該是同一個事物,但是這里同時出現(xiàn),讓人摸不著頭腦。實(shí)際上它們是完全不同的:
- ILookupList 更多的是描述了一個事物,即是頁面上的DropDownList,它定義的方法也是對其本身進(jìn)行操作的。
- ILookupCollection 描述的是一個行為,它僅包含一個方法,BindTo(),方法接收的參數(shù)正是ILookupList,意為將ILookupCollection的數(shù)據(jù)綁定到 ILookupList上。而ILookupCollection包含的數(shù)據(jù),是ILookupDTO的集合(IList<ILookupDTO>,由類型外部通過構(gòu)造函數(shù)傳入)。
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); // 根據(jù)傳遞進(jìn)來的items創(chuàng)建新的列表 }
public int Count { get { return items.Count; } // 獲取項(xiàng)目數(shù) }
// 將項(xiàng)目綁定到列表 public void BindTo(ILookupList list) { list.Clear(); // 先清空列表 foreach (ILookupDTO dto in items) { // 遍歷集合,綁定到列表中 list.Add(dto); } } }
到這里 MVP.DTO 項(xiàng)目就結(jié)束了,我們再來看一下大家都熟悉的數(shù)據(jù)訪問層,MVP.DataAccess。
MVP.DataAccess 項(xiàng)目
這一是和數(shù)據(jù)最接近的一層,用來獲取來自數(shù)據(jù)庫(或者其它存儲)的數(shù)據(jù)。因?yàn)楸疚牡哪康氖侵v述MVP模式的構(gòu)架,我們不需要把注意力集中在數(shù)據(jù)訪問上,所以這一層我直接HardCode了,而非從數(shù)據(jù)庫中獲取。
這一層定義了一個接口 IBookMapper:
public interface IBookMapper { IList<BookDTO> GetAllBooks(); // 獲取所有Book BookDTO FindById(int bookId); // 獲取某一Id的Book }
以及一個實(shí)現(xiàn)了此接口的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項(xiàng)目,而是對它們進(jìn)行了深度復(fù)制,返回了它們的副本。這樣是為了避免在類型外部通過引用類型變量訪問類型內(nèi)部成員。更多內(nèi)容可以參考我之前寫的 創(chuàng)建常量、原子性的值類型 一文(Effective C#的筆記)。
MVP.Task 項(xiàng)目
MVP.Task 項(xiàng)目是Model層的核心,之前創(chuàng)建的兩個項(xiàng)目都是為這個項(xiàng)目進(jìn)行服務(wù)的。它包含一個接口 IBookTask,這個接口定義了Task的兩個主要工作:1、返回所有的Book列表(用于綁定DropDownList列表);2、根據(jù)某一個Book的Id返回該Book的詳細(xì)信息。
public interface IBookTask { ILookupCollection GetBookList(); // 返回圖書列表 BookDTO GetDetailsForBook(int bookId); // 返回某一圖書 }
我覺得這個接口的定義是MVP模式的精華所在之一,GetDetailsForBook()方法很容易理解,我們幾乎現(xiàn)在就可以猜到它會把工作委托給MVP.DataAccess項(xiàng)目的BookMapper去處理,因?yàn)锽ookMapper已經(jīng)包含了類似的方法FindById()。關(guān)鍵就在于 GetBookList()方法,注意它返回的是ILookupCollection,而非一個IList<BookDTO>。這樣我們在后面將介紹的提供器中,只需要在獲取到的ILookupCollection上調(diào)用BindTo方法,然后傳遞列表對象,就可以綁定列表了,實(shí)現(xiàn)了Web頁面和CodeBehind邏輯的分離(MVP模式的精要所在);而如果這里我們僅僅返回IList<Book>,那么綁定列表的工作勢必要移交給上一層去處理。
接下來我們面臨了一個問題:MVP.DataAccess 項(xiàng)目中的 BookMapper.GetAllBook()方法返回的是 IList<Book>,而這里需要的是一個ILookupCollection。回頭看一下ILookupCollection的實(shí)現(xiàn),它內(nèi)部維護(hù)的是一個IList<ILookupDTO>,ILookupDTO是業(yè)務(wù)無關(guān)的,它包含了Text和Value屬性用于向頁面上的DropDownList的列表項(xiàng)提供數(shù)據(jù)。在本例中,ILookupDTO的Text應(yīng)該為書名,而Value應(yīng)該為書的Id。這樣,我們最好能創(chuàng)建一個Converter類,能夠進(jìn)行由BookDTO到ILookupDTO,進(jìn)而由IList<BookDTO> 到 IList<ILookupDTO>的轉(zhuǎn)換。最后將轉(zhuǎn)換好的IList<ILookupDTO>作為參數(shù)傳遞給ILookupCollection的構(gòu)造函數(shù),從而得到一個ILookupCollection。
注意到ILookupDTO是業(yè)務(wù)無關(guān)的,所以我們定義接口名稱,為ObjectToLookupConverter,而非BookToLookupConverter。另外,以后我們可能創(chuàng)建其他的類型,比如Customer(客戶)也能轉(zhuǎn)換為LookupDTO,我們定義一個泛型接口(使得Converter類不限于BookDTO才能使用):
public interface IObjectToLookupConverter<T> { // 將 T類型的對象obj 轉(zhuǎn)換為 ILookupDTO類型 ILookupDTO ConvertFrom(T obj);
// 將 IList<T> 類型的對象列表 轉(zhuǎn)換為 IList<ILookupDTO> 類型 IList<ILookupDTO> ConvertAllFrom(IList<T> obj); }
再定義一個抽象基類實(shí)現(xiàn)這個接口,抽象類實(shí)現(xiàn)接口的ConvertAllFrom()方法,并將其中中實(shí)際的轉(zhuǎn)換工作委托給 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); // 將實(shí)際的轉(zhuǎn)換委托給 ConvertFrom()方法 }); } }
最后,到了實(shí)際的將 Book 轉(zhuǎn)換為 LookupDTO 的部分了,非常的簡單:
public sealed class BookToLookupConverter : ObjectToLookupConverter<BookDTO> { public override ILookupDTO ConvertFrom(BookDTO book) { return new SimpleLookupDTO(book.Id.ToString(), book.Title); } }
好了,有了這些準(zhǔn)備工作,我們實(shí)現(xiàn) IBookTask接口就變得輕易的多了。現(xiàn)在,創(chuàng)建MVP.Task項(xiàng)目的最后一個類,BookTask。注意GetBookList()方法的實(shí)現(xiàn)過程,和我們上面的分析一模一樣:
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 = // 轉(zhuǎn)換為 IList<ILookupDTO> new BookToLookupConverter().ConvertAllFrom(bookList);
// 構(gòu)建ILookupCollection ILookupCollection collection = new LookupCollection(list);
return collection; }
// 獲取某一圖書的詳細(xì)信息 public BookDTO GetDetailsForBook(int bookId) { BookDTO book = bookMapper.FindById(bookId); return book; } }
至此,Model層或者叫Service服務(wù)層的所有項(xiàng)目都已經(jīng)結(jié)束了,我們接下來看MVP的V(View層)是如何構(gòu)建的。
View 層的實(shí)現(xiàn)
Web 站點(diǎn)項(xiàng)目 和 MVP.WebControl 項(xiàng)目
你可能會奇怪為什么現(xiàn)在就講述View層,而不是Presenter提供器層?這是因?yàn)镻resenter是View 和 Model的一個協(xié)調(diào)者,從下面幅圖就可以看出來。所以,我們需要先看下View層如何實(shí)現(xiàn),進(jìn)而才能去討論P(yáng)esenter層。
View層包含兩個項(xiàng)目,一個是站點(diǎn)項(xiàng)目,一個是MVP.WebControl項(xiàng)目,我們先看站點(diǎn)項(xiàng)目。它僅包含一個頁面:Default.aspx,內(nèi)容也是簡單之極,我們先看頁面部分的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事件,并且設(shè)置根據(jù)得到的數(shù)據(jù)填充三個Literal控件的Text屬性。而在MVP模式中,這部分的工作將會交由提供器來完成,所以,我們只需要為這些控件建立Set訪問器,并且將頁面的引用傳給提供器就可以了(如何傳遞頁面引用給提供器后面會討論)。我們現(xiàn)在在頁面的后置代碼中添加一組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的數(shù)據(jù)進(jìn)行填充,我們需要讓這個DropDownList能夠與ILookupList聯(lián)系起來,并進(jìn)一步通過調(diào)用來自MVP.Task中的 ILookupCollection的BindTo()方法,來對列表進(jìn)行綁定。
記得到現(xiàn)在為止我們都沒有實(shí)現(xiàn) ILookupList接口,現(xiàn)在是時候?qū)崿F(xiàn)它了,新建一個項(xiàng)目MVP.WebControl,添加對MVP.DTO的引用,然后創(chuàng)建ILookupList接口的實(shí)現(xiàn)WebLookupList。在對ILookupList接口的實(shí)現(xiàn)中,對DropDownList進(jìn)行包裝,為了更好的代碼重用,我們傳遞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); } } }
可以看到我們實(shí)際上將對這個接口實(shí)現(xiàn)的具體工作都委托給了 ListControl,這樣,當(dāng)我們在ILookupList上調(diào)用Add()方法添加列表項(xiàng)時,便會添加到頁面的DropDownList上。
記住:我們期望能讓提供器送數(shù)據(jù)的所有Web頁面上的控件,都應(yīng)該為提供器提供一個入口。在前面,我們?yōu)槿齻€Literal空間提供的入口是Set屬性。這里我們一樣需要提供一個Get屬性,來讓提供器能夠獲得一個ILookupList。在Default頁面的后置代碼中添加下面代碼:
public ILookupList BookList { get { return new WebLookupList(ddlBook); } }
Presenter 層的實(shí)現(xiàn)
實(shí)現(xiàn)Presenter(提供器)之前我們先考慮它的作用是什么:從Task中獲取數(shù)據(jù),然后送到View層(Aspx頁面)中。這就暗示 提供器必須包含 Task和 View層的引用。但是如果我們是無法讓提供器引用站點(diǎn)項(xiàng)目的,因?yàn)檎军c(diǎn)項(xiàng)目不會生成單獨(dú)的dll文件(基于每個頁面生成dll)。但是站點(diǎn)卻可以引用提供器,所以我們只要在提供器項(xiàng)目中定義一個接口,然后讓頁面去實(shí)現(xiàn)這個接口,我們通過這個接口去為頁面送數(shù)據(jù)(調(diào)用接口的Set訪問器)。
MVP.Presentation 項(xiàng)目
現(xiàn)在你可以將頁面上的三個Literal和一個DropDownList與這個View接口聯(lián)系起來了。創(chuàng)建MVP.Presentation項(xiàng)目,然后我們定義Default頁面需要實(shí)現(xiàn)的IViewBookView接口:
public interface IViewBookView { ILookupList BookList { get; } string Title { set; } string PubDate { set; } string Price { set; } }
這個接口的定義完全是基于Web頁面的,你需要為頁面提供哪些數(shù)據(jù),或者為哪個控件送數(shù)據(jù),那么就定義哪些屬性。然后我們讓W(xué)eb項(xiàng)目引用MVP.Presentation項(xiàng)目,在修改頁面的后置代碼文件Default.aspx.cs,讓它去實(shí)現(xiàn)這個接口(因?yàn)轫撁嬉呀?jīng)包含了這個接口的所有定義,所以這里只是起到一個向提供器傳遞窗體的作用)。
public partial class _Default : System.Web.UI.Page, IViewBookView
下一步,我們要實(shí)現(xiàn)提供器,我們在項(xiàng)目中再添加一個文件 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; } }
// 顯示特定圖書的詳細(xì)信息 public void DisplayBookDetails() { int? bookId = SelectedBookId;
if (bookId.HasValue) { BookDTO book = task.GetDetailsForBook(bookId.Value); UpdateViewFrom(book); } }
// 更新頁面的信息,在這里進(jìn)行格式化 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中獲取數(shù)據(jù),然后調(diào)用view接口的屬性,或者從view接口獲得DropDownList的引用(通過ILookupList),然后通過 BindTo()方法為列表填充數(shù)據(jù)。注意到Initialize()方法,它為列表填充數(shù)據(jù),這個應(yīng)該在頁面加載之前就被調(diào)用;還有DisplayBookDetails()方法,它應(yīng)該在列表的SelectedIndexChanged事件被觸發(fā)時調(diào)用,所以我們還有最后一部沒有做,再次修改Default.aspx.cs文件,設(shè)置這些方法的觸發(fā)時機(jī)。
最后一步,再次修改Default.aspx.cs文件
在后置代碼類中添加如下代碼,完成上一小節(jié)說明的所有內(nèi)容:
private ViewBookPresenter presenter;
protected override void OnInit(EventArgs e) { base.OnInit(e); presenter = new ViewBookPresenter(this); // 創(chuàng)建Presenter的實(shí)例
// 為DropDownList綁定事件處理方法 ddlBook.SelectedIndexChanged += delegate { presenter.DisplayBookDetails(); }; }
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { presenter.Initialize(); // 綁定列表 } }
這里值得注意的是 ViewBookPresenter 對象的創(chuàng)建,它通過this關(guān)鍵字,將頁面本身傳遞了進(jìn)去,而頁面本身實(shí)現(xiàn)了IViewBookView接口,滿足構(gòu)造函數(shù)的簽名,這樣提供器通過IViewBookView便可以訪問頁面上的屬性和列表,并為之提供數(shù)據(jù)。
總結(jié)
這篇文章是對 模型-視圖-提供器 模式 一文范例程序的一個刨析和說明。在本文中,我們創(chuàng)建了一個包含多個項(xiàng)目的完整的符合MVP模式的Web頁面。我們先創(chuàng)建了基礎(chǔ)項(xiàng)目 MVP.DTO,用于傳送數(shù)據(jù)、MVP.DataAccess,用于數(shù)據(jù)訪問;接著分別創(chuàng)建了 Model層、View層、Presenter層,并講述了它們之間的調(diào)用關(guān)系,以及使用的要點(diǎn)。通過這則范例,希望大家能對MVP模式有了一定的認(rèn)識和了解。
不一定項(xiàng)目的每個頁面,都去采用MVP模式來構(gòu)建。但如果運(yùn)用的好的話,可以將多個頁面共同的的某一部分(或者叫功能)抽象出來,使用同一個提供器,可以很大程度上實(shí)現(xiàn)代碼重用。另外也可以一個Page實(shí)現(xiàn)多個IView,將頁面功能分離成多個部分,需要使用哪個功能,就實(shí)現(xiàn)哪個IView,并使用相應(yīng)的IViewPresenter進(jìn)行初始化。
|