天天看點

[譯]模型-視圖-提供器 模式模型-視圖-提供器 模式

模型-視圖-提供器 模式

出處: http://msdn.microsoft.com/en-us/magazine/cc188690.aspx

引言

随着像Asp.Net和Windows窗體這樣的使用者界面建立技術越來越強大,讓使用者界面層做多于它本應做的事是很常見的。沒有一個清晰的職責劃分,UI層經常淪為一個包含實際上應屬于程式其他層的邏輯的容器。有一個稱為 模型(Model)-視圖(View)-提供器(Presenter)(MVP)的設計模式,特别适合解決這個問題。為了表明我的觀點,我将為Northwind資料庫中的客戶建一個遵循MVP模式的顯示螢幕(display screen)。

為什麼在UI層包含太多的邏輯是很糟糕的?在既不手動運作應用程式,也不維護醜陋的自動執行UI元件的UI運作者腳本(runner script)的情況下,位于應用程式UI層中的代碼是非常難于調試的。雖然這本身就是一個很大的問題,一個更大的問題是在應用程式的公共視圖之間會有大量的重複代碼。當執行某一特定業務的功能在UI層的不同部分之間拷貝,通常很難找到好的可選重構方法。MVP設計模式使得将UI層中的邏輯和代碼 重構為 更加易于測試的新型的、可重用的代碼 更加容易。

圖1

示範了組成一個範例應用程式的主要層。注意對于UI和表現(Pesentation)有着各自的包(Package)。你可能會想它們是一樣的,但是實際上項目中的UI層應該隻包含各種不同的UI元素――窗體和控件。典型地,在一個Web窗體項目中是Asp.Net Web窗體、使用者控件、伺服器控件的集合;在Windows項目中,它是Windows 窗體、使用者控件以及第三方庫(Libraries)的集合。這一額外的層就是将顯示和邏輯分隔開的層。在表現層,你擁有實際上實作UI行為的對象――諸如驗證顯示,從UI層收集使用者輸入 等等。

[譯]模型-視圖-提供器 模式模型-視圖-提供器 模式
圖1.應用程式構架

遵循MVP

如同你在

圖2

中所見的,這個項目的UI相當标準。當頁面加載時,螢幕将會顯示一個包含Northwind資料庫中所有客戶的下拉框。如果你從下拉框中選擇一個客戶,頁面會更新為這個客戶的資訊。通過遵循MVP設計模式,你可以從UI中将行為(Behavior)重構到它們自己的類中。

圖3

顯示了一個類圖,它說明了參與其中的各個不同類之間的聯系。

[譯]模型-視圖-提供器 模式模型-視圖-提供器 模式
圖2. 使用者資訊
[譯]模型-視圖-提供器 模式模型-視圖-提供器 模式
圖3. MVP類圖

注意到提供器對于應用程式實際的UI層一無所知非常重要。它知道它可以同接口對話,但是它不知道也不關心接口的實作是什麼。這提升了在完全不同的UI技術間提供器的重用。

我将使用測試驅動開發(TDD)建立客戶界面的功能。

代碼4

示範了第一次測試的細節,我将通過這個測試來描述我期望在頁面加載時觀察到的行為。TDD讓我每次關注于一個問題,僅編寫可以讓測試通過的代碼,然後繼續進行下面的工作。在測試中,我将會利用一個稱為NMork2 的僞對象架構,它允許我建立接口的僞實作(mock implementation)。

代碼4.第一個測試

[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實作中,我決定提供器将作為視圖所要與之工作的依賴。通常,建立對象使之處于可以立刻進行工作的狀态是一種好的做法。在這個應用程式中,表現層依賴于服務層,實際上由服務層調用領域功能(domain functionality)。因為這個需求,建立一個含有可以與服務類對話的接口的提供器也是有意義的。這樣確定了一旦提供器建立好了,它就已經準備好做它需要做的所有工作了。我以建立兩個特定的mocks作為開始:一個用于服務層,一個用于提供器将與協作的視圖。

為什麼使用mocks?單元測試的一個規則就是盡可能地隔離測試以便集中于某一特定的對象。在這個測試中,我隻關心提供器所期待的行為。目前我并不關心view接口或者service接口的實際實作。我信任由這些接口定義的契約(contract),并且設定mocks去相應運作(behave)。這樣確定了我的測試僅僅圍繞着我對提供器所期望的行為,而不是它所依賴的任何東西。我期望的,在提供器的初始化方法被調用後所表現的行為如下:

首先,提供器應該調用一次服務層ICustomerTask對象(已經在測試中Mock了)的GetCustomerList方法。注意通過使用NMock,我可以模拟Mock的行為。以服務層來說,我想要傳回一個ILookupCollection給提供器。然後,在提供器從服務層收到ILookupCollection以後,它可以調用集合的BindTo方法 并且向方法傳遞一個ILookupList方法的實作。通過使用NMockExpect。一旦我可以确定方法,如果提供器沒有調用這個方法一次并且隻一次,那麼測試将會失敗。

寫完測試以後,我處于一個完全不可編譯的狀态。我将要做一些可能的最簡單的事讓測試通過。

讓第一個測試通過

先寫一個測試的好處之一是我現在有了一個我可以遵循的使得測試編譯并最終通過的藍圖(這個測試)。第一個測試還有兩個尚不存在的接口。這些接口是代碼正确通過編譯的第一個先決條件。我們将以IViewCustomerView的代碼作為開始:

public interface IViewCustomerView {

    ILookupList CustomerList { get; }

這個接口暴露一個傳回ILookupList接口實作的屬性。我還沒有ILookupList接口或是它的一個實作,就此而言。出于使測試通過的目的,我不需要一個顯示的實作,是以我可以這樣去建立ILookupList接口:

public interface ILookupList { }

ILookupList接口現在看上去相當的沒用。我的目标是使得測試編譯并且通過,并且這些接口滿足測試的需要。現在是時候将焦點轉移到我們實際要進行測試的對象上了――ViewCustomerPresenter。這個類現在還不存在,但是看下測試,你可以發現關于它的兩個要點:它有一個既需要視圖實作也需要服務實作作為依賴的構造函數,并且它有一個無傳回值的初始化方法。

代碼5

示範了如何使測試通過編譯:

代碼5. 編譯這個測試

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

應該記得,為了讓提供器有意義地工作,它需要獲得它的所有依賴;這就是為什麼傳遞視圖和服務進去。我沒有實作初始化方法,是以如果我運作這個測試我會得到一個NotImplementedException 異常。

如果我已經提到的,我不會盲目地對提供器進行編碼;我已經知道,通過觀察這個測試,在初始化方法被調用時,提供器應該顯示出什麼樣的行為。這個行為的實作如下所示:

public void Initialize() {

    task.GetCustomerList().BindTo(view.CustomerList);

在這篇文章所附帶的源代碼中,在CustomerTask類(它實作了ICustomerTask接口)中有GetCustomerList方法的完整實作。然而,從實作和測試提供器的角度來說,我不需要知道是否有一個可以工作的實作。正是這種級别的抽象允許我在提供器類的測試中穿行。第一個測試現在處于可以編譯并運作的狀态。這證明了當提供器的初始化方法被調用,它将會以一種我在測試中所指定的方式與它所依賴的類型進行互動,并且最終,當這些依賴的具體實作注入到提供器中,我可以确定結果視圖(ASPX頁面)将會由客戶清單所填充。

填充 DropDownList

迄今為止,我主要在處理接口以便将實際的實作細節抽象出來、将注意力集中在提供器上。現在是時候通過一種可測試的方式建立一些底層代碼(plumbing),這些底層代碼将最終允許提供器在Web頁面上填充一個清單。完成這個工作的關鍵是将發生在LookupCollection類的BindTo方法中的互動。如果你看下

代碼6

中LookupCollection類的實作,你将注意到它實作了IlookupCollection接口。這篇文章的源碼含有附帶的測試,用于建立LookupCollection類的功能。

代碼6. LookupCollection類

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方法的實作值得特别注意。注意到在這個方法中,集合周遊了它自己的私有ILookupDTO 清單的實作。ILookupDTO是一個接口,它迎合了UI層的綁定下拉框。

public interface ILookupDTO {

    string Value { get; }  

    string Text { get; }

代碼7

示範了測試lookup集合的BindTo方法的代碼,這有助于解釋LookupCollection和IlookupList之間所期望的互動。最後一行值得特别注意。在這個測試中,我期望在試圖添加項目到清單之前,LookupCollection将會調用IlookupList實作的Clear方法。然後我期望Add方法在IlookupList上調用10次,并且LookupCollection将傳遞一個實作了ILookupDTO接口的對象,作為Add方法的一個參數。為了能夠實際工作在一個Web項目中的控件上(比如一個下拉清單),你将需要建立一個IlookupList的實作,它知道如何與Web項目中的控件工作。

代碼7 一個描述行為的測試

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

這篇文章附帶的源碼中包含一個名為MVP.Web.Controls的項目。這個項目包含了我選擇建立的用于完成解決方案的任何基于Web的控件或者類。為什麼我要把代碼放到這個項目中,而沒有放在App_Code目錄或者Web項目本身中?易測性。在沒有手動運作應用程式或者使用某種類型的測試機器人自動操作UI的情況下,居于Web項目中的任何東西都是難于直接測試的。MVP模式允許我在一個較高的層次上考慮抽象,并且測試核心接口(IlookupList和ILookupCollection)的實作,而不需要手動地運作程式。我将在Web.Controls項目中添加一個新類,一個WebLookupList控件。

代碼8

示範了這個類的第一次測試:

代碼8. WebLookupList 控件的第一次測試

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

測試中關鍵的部分在

中顯示了。這個測試項目顯然需要System.Web庫的一個引用,以便它可以初始化DropDownList Web控件。看下這個測試,你應該看到WebLookupList類将會實作IlookupList接口。它也将把ListControl作為一個依賴。在System.Web.UI.WebControls命名空間中的兩個最常見的ListControl的實作就是DropDownList和ListBox類了。

中的一個關鍵特色就是我确信WebLookupList正确的更新了Web ListControl的狀态,它将職責委托給了這個Web ListControl。

圖9

顯示了參與WebLookupList實作的類的類圖。通過

代碼10

,我可以滿足WebLookupList控件第一次測試的需求。

代碼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 類

記得,MVP模式的要點之一是通過view接口的建立引入了各層之間的分離。提供器不知道某一視圖的具體實作,以及它所要互動的IlookupList;它隻知道它可以調用由這些接口所定義的任何方法。最終,WebLookupList是一個包裝了ListControl并且将職責委托給了ListControl(一些定義在System.Web.UI.WebControls項目中的ListControls的基類)的類。在這些代碼完成好了以後,我現在可以編譯并運作WebLookupList控件的測試了,它應該可以通過。我可以為WebLookupList控件再添加一個測試來測試Clear方法的實際行為。

public void ShouldClearUnderlyingList(){

    ListControl webList = new DropDownList();

    webList.Items.Add(new ListItem("1", "1"));

    list.Clear();

    Assert.AreEqual(0, webList.Items.Count);

我再次測試到,當WebLookupList類本身的方法被調用時,實際上會改變它底層的ListControl(DropDownList)的狀态。WebLookupList現在完全擁有完成填充Web表單上一個DropDownList的特色了。現在是時候讓我把所有東西都結合到一起,然後讓客戶清單填充這個Web頁面的下拉框了。

實作View接口

因為我正在建立一個Web窗體前端(界面),将IViewCustomerView接口實作為一個Web窗體或者使用者控件将是有意義的。出于這個專欄的目的,我将建立一個Web窗體。如同你在

中所見到的,這個頁面大概的樣子已經建立好了。現在我隻需要實作View接口。切換到ViewCustomers.aspx頁面的後置代碼中,我可以添加下面的代碼,表示這個頁面需要實作IViewCustomersView接口:

public partial class ViewCustomers : Page,IViewCustomerView

如果你看一下代碼範例,你将會注意到Web項目和表現(Presentation)是兩個完全不同的程式集。同樣,表現項目沒有包含對Web.UI項目的任何引用,進一步維持着層的分隔。另一方面,Web.UI項目必須包含一個對表現項目的一個引用,因為它包含了View接口和提供器。

通過選擇實作IViewCustomerView接口,我們的Web頁面現在需要實作由那個接口所定義的任何方法和屬性。現在IViewCustomerView接口隻有一個屬性,這是一個傳回任何實作了ILookupList接口的隻讀屬性。我添加了一個對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);}

我現在需要在提供器上調用初始化方法,這個方法将觸發它去做些實際的工作。為了完成這個,視圖需要能夠初始化提供器,以便它的方法可以被調用。如果你回頭看下提供器,你将會記得它需要與視圖和服務工作。ICustomerTask代表一個居于應用程式服務層中的一個接口。典型地服務層負責監管領域對象間的互動,以及将這些互動的結果轉換成資料傳遞對象(DTOs),然後這些DTO對象從服務層傳遞到表現層,接着傳遞到UI層。然而,這裡有一個問題,我規定提供器需要視圖和服務的實作才能建立。

提供器實際的初始化将發生在Web頁面的後置代碼中。這是一個問題,因為UI項目不包含對服務層項目的引用。然而,表現層項目包含,它有一個對服務層項目的引用。這允許我通過在ViewCustomverPresenter中添加一個重載的構造函數來解決這個問題:

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窗體将它本身作為一個實作了View接口的對象進行傳遞!

後置代碼已經實作,現在我可以生成并運作應用程式了。Web頁面上的DropDownList現在填充了客戶名稱清單,而在後置代碼中不需要任何的資料綁定代碼。不僅如此,曾經運作的各個小部分的測試最終協同工作了,確定了表現層構架将會如期望般運作。

我将通過示範顯示一個在DropDownList中選中的客戶資訊,把我關于MVP的讨論聯系起來。再一次,我通過寫一個描述了我希望觀察到的行為的測試作為開始(看

代碼11

)。

代碼11. 最後一個測試

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(mockCustomerLookupList).GetProperty(

        "SelectedItem").Will(Return.Value(lookupDTO));

        "GetDetailsForCustomer").With(1).Will(Return.Value(dto));

    Expect.Once.On(mockViewCustomerView).SetProperty(

        "CompanyName").To(dto.CompanyName);

        "ContactName").To(dto.ContactName);

        "ContactTitle").To(dto.ContactTitle);

        "Address").To(dto.Address);

        "City").To(dto.City);

        "Region").To(dto.Region);

        "PostalCode").To(dto.PostalCode);

        "Country").To(dto.CountryOfResidence.Name);

        "Phone").To(dto.Phone);

    Expect.Once.On(mockViewCustomerView).SetProperty("Fax").To(dto.Fax);

    presenter.DisplayCustomerDetails();

和前面一樣,我利用NMock庫建立task和View接口的Mocks。這個特定的測試通過向服務層請求一個代表某一特定客戶的DTO,驗證了提供器的行為。一旦提供器從服務層獲得DTO,它将直接更新視圖的屬性,這就避免了視圖需要知道如何正确地顯示來自對象的資訊。為了簡潔,我不打算去讨論WebLookupList控件的SeletedItem屬性的實作;然而,我将把它留給你,通過檢查源代碼來檢視實作的細節。這個測試真正示範的是當提供器從服務層收到一個Customer DTO 時發生在提供器和視圖之間的互動。如果我現在試圖運作這個測試,我将會處于一個嚴重的錯誤狀态,因為很多的屬性view接口沒有定義。是以我将繼續為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窗體就開始抱怨了,因為它不再滿足接口的定義,是以我不得不回頭看下我的Web窗體的後置代碼,并且實作那些剩下的成員。如同前面所陳述的,Web頁面的整個标記都已經建立了,并讓那些标記了“runat=server”的表格單元格根據将在它中所要顯示的資訊來為它命名。這将使實作接口成員的代碼非常的輕易:

public string CompanyName{

    set { this.companyNameLabel.InnerText = value; }

public string ContactName{

    set { this.contactNameLabel.InnerText = value; }

...

實作了Set屬性通路器,還剩下一件事需要做。我需要有一種方式通知提供器,以便顯示選中客戶的資訊。回頭看下測試,你可以看到這個行為的實作位于提供器的DisplayCustomerDetails方法上。然而,這個方法不會接受任何參數。當調用時,提供器将會回頭找視圖,從它中拖出任何所需要的資訊(它通過使用ILookupList擷取),然後使用這些資訊擷取所請求的客戶的詳細内容。從UI的角度來看,我需要做的全部就是将DropDownList的AutoPostBack屬性設為True,我也需要添加下面的事件處理程式,和Page的OnInit方法挂接起來。

protected override void OnInit(EventArgs e)

    this.customerDropDownList.SelectedIndexChanged += delegate{

        presenter.DisplayCustomerDetails();

    };

這個事件處理程式確定,無論什麼時候下拉框中的一個新的客戶被選中,視圖将會請求提供器顯示客戶的細節。

注意到這是一個典型的行為很重要。當一個視圖請求提供器做一些事情,它不提供任何的特定細節,而是由提供器去通路視圖,通過view接口擷取它所需要的任何資訊。

代碼13

顯示了實作提供器的行為所需要的代碼。

代碼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;

希望你現在已經明白了添加提供器層的價值。試圖擷取一個客戶Id并且顯示它的詳細資訊都是提供器的責任。這段代碼通常都是實作在後置代碼中,但是現在它位于一個類中,這樣我就可以在任何的表現層技術之外,對它進行完全的測試和演練(

譯注:

同一段代碼可以應用于WinForm和WebForm,讓窗體都去實作view接口就可以了)。

在提供器從視圖中獲得一個正确的客戶Id的事件中,它轉向服務層并請求一個DTO,這個DTO代表了客戶的細節。一旦提供器擁有了DTO,它使用包含在DTO中的資訊更新視圖。注意到一個關鍵點就是View接口很簡潔;伴于ILookupList接口,view接口隻包含了String 類型。正确地轉換并且格式化由DTO擷取的資訊,以便它可以以字元串形式送出給視圖,最終都是提供器的責任。雖然在範例中沒有示範,提供器也應該負責從視圖中讀取資訊,并且将它轉變為服務層所期望的必要類型。

所有的小部分都已經就位,現在我可以運作應用程式了。當頁面第一次加載,我擷取了客戶的一個清單,并且第一個客戶(未選擇)顯示在DropDownList中。如果我選擇一個客戶,産生一個PostBack,視圖和提供器發生互動,使用相關的客戶資訊更新頁面。

接下來是什麼?

模型-視圖-提供器模式實際上僅僅是許多開發者已經熟悉的 模型-視圖-控制器 模式的更新。關鍵的變化是MVP完全将UI從應用程式的 領域/服務層分離出來。盡管從需求角度來看,這個例子相當的簡單,但它可以幫助你從你的應用程式中将UI層與其他層的互動抽象出來。當你深入鑽研到MVP模式中,我希望你可以找到其他方法将盡可能多的格式化和條件判斷邏輯從你的後置代碼中分離出來,并将它們置于可測試的 視圖/提供器 互動模型中。