UI的設計模式MVP模式–Supervising Controller

13 October 2011 — Written by Sky Chang
#ASP.NET#ASP.NET MVC#Design Patten#Unit Test

最近雜事一堆,所以一直沒有空把MVP模式的Supervising Controller給補完,今天終於下定決心,把MVP的另外一個模式Supervising Controller給補完吧!

斯斯有兩種,MVP也有兩種

MVP設計模式有分兩種,最先提出來的是Martin Fowler,最後於2006年將MVP分成Supervising Controller和Passive View;基本上這兩個的概念是相同的,但最大的差別只在於那個"P" ( 也就是Presenter ),Presenter能控制的東西程度不同罷了,Supervising Controller就沒辦法完全掌控Model。而這次我們要看的是其中一個MVP模式Supervising Controller

Supervising Controller

如果已經有讀過Presenter View的人,可以看一下架構圖後,跳到最後一段。

所謂沒圖沒真相,沒程式碼就不叫寫程式,所以我們第一件事情,先給個Supervising Controller的圖吧。

image

沒錯,這是MVP Supervising Controller的圖,不是MVC的,我相信如果有看過MVC的人,大概也找不太出兩者間的差異吧。那至於這個架構圖和MVC有甚麼不一樣!?,好問題!,但我們留到後面再看。

正式開始

假設今天我們要用ASP.NET寫一個查詢程式,非常非常簡單的查詢,只要輸入,名子,就會尋找到指定的資料,並且顯示於下方,畫面大致上是這樣。

image_thumb10

啥!?很醜!!,而且又和前面的Presenter View一樣!?那是因為後面要比較的關係嘛,就不要太苛求了,總之,就是在TextBox輸入名子,按下Button後,會在下面兩個Label顯示姓名和地址…

image_thumb9

程式碼大概長這樣。

我們會先有一個User的類別,內容很簡單,就兩個屬性,名子和地址。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class User
    {
        public string name { get; set; }
        public string address { get; set; }
    }
}

接下來,我們實作一下存取User的類別,簡單的說,要存取User都是透過這個類別來和底層的資料傳遞;當然,既然是範例,就沒有和真實的資料庫做一個交流,也只是很單純的定義一個方法GetByName,且當輸入Sky的時候,就會傳回一個User物件回來;如果是正式的環境,我們這裡可能使用SQL存取,也可能使用ORM技術存取。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class UserRepository
    {
        public User GetByName(string userName)
        {
            //作假資料,如果搜尋到Sky,就new一個新的user回傳。
            if (userName == "Sky")
            {
                User user = new User();

                user.name = "Sky";
                user.address = "台中";
                return user;
            }
            return null;
        }
    }
}

接下來,是視覺的頁面,這頁沒甚麼,就很一般,把一些東西拖拖、拉拉,就好了,頁面有點長就是了。

<%@ Page Title="首頁" Language="C#" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="WebAppMvpDemo._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="Form1" runat="server">
    <div class="page">
        <div class="header">
            <div class="title">
                <h1>
                    MVP架構測試
                </h1>
            </div>
        </div>
        <div class="main">
            <div>
                <h2>歡迎使用 ASP.NET!</h2>
                <p>此為MVP架構測試。</p>
            </div>
            <div>
                <asp:TextBox ID="SearchUserTextBox" runat="server"></asp:TextBox>
                <asp:Button ID="Button1" runat="server" Text="Button" onclick="Button1_Click" />
           </div>
           <div>
                <asp:Label ID="UserNameLabel" runat="server" Text="Label"></asp:Label>
                <asp:Label ID="UserAddressLabel" runat="server" Text="Label"></asp:Label>
           </div>
        </div>
    </div>
    </form>
</body>
</html>

接下來是主要的程式碼,基本上就是在處理Button的Click事件,我們會先把剛剛寫好,專門用來存取User類別的UserRepository類別建立起來,來方便我們存取User,然後使用UserRepository的GetByName方法來尋找使用者;當然,事前會稍微做一些簡單的判斷,看看是不是空值之類的;

( 關於資料存取,這種做法是"比較"好一點的作法,通常我們會設計一個介面,來降低與UserRepository的耦合,這才是Repository模式的做法,不過這邊為了敘述方便,就不透過介面了,看不懂的人也沒關係,可以先跳過;此外,本來還想直接寫SQL在Click事件裡面,但想到這樣還要準備資料庫,發現更累XDD,所以最後還是用這種方法來Demo,而SQL散落於世界各地的寫法,其實是最不好的寫法,不過我以前公司幾乎都是這樣寫就是了XDD )

從UserRepository會吐回來User物件,然後我們把值顯示於ASP.NET 控制項,就完成了這簡單的Demo。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebAppMvpDemo
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            UserRepository userRepository = new UserRepository();
            if (string.IsNullOrEmpty(SearchUserTextBox.Text))
            {
                return;
            }

            User user = userRepository.GetByName(SearchUserTextBox.Text);
            if (user == null)
            {
                return;
            }

            UserNameLabel.Text = user.name;
            UserAddressLabel.Text = user.address;
        }
    }
}

沒錯,這幾乎就是一般的ASP.NET 開發的寫法,雖然比asp好太多了,但還是耦合得太緊,沒辦法測試,如果要測試的話,還是必須開啟網頁,填填看TextBox,然後按下Button,其次就是維護也不容易,所以接下來,我們來把這個範例改成MVP的Supervising Conroller試試看。

首先,我們先處理Model的部分,但其實,Model我們早就已經寫好了,也就是User這個類別,而這個類別改成MVP的Supervising Controller架構,其實也不需要做變動,但是Repository模式的這個部分還沒做好,我們先定義一個介面IUserRepository。

( Repository模式和MVP Supervising Controller模式,兩者之間並不是必須,也就是說,今天我實作MVP Supervising Controller,是可以不用做Repository模式的,但Repository模式可以讓整個資料層分離的更好,所以就一起講吧=w= )

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WebAppMvpDemo
{
    public interface IUserRepository
    {
        User GetByName(string userName);
    }
}

然後裡面再實作這個介面;不過剛剛我們已經先做好了,也就是UserRepository,其實這邊的程式碼和之前的UserRepository沒甚麼差別,唯一的差異只有UserRepository實作IUserRepository,也就是這行。

public class UserRepository : IUserRepository

以下是UserRepository完整的程式碼。

( 為什麼要做一個介面?其實原因很簡單,就是為了方便我們測試時抽換,只要有實作此介面的實體,就可以輕易地去抽換掉,後面會繼續講解。 )

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class UserRepository : IUserRepository
    {
        public User GetByName(string userName)
        {
            //作假資料,如果搜尋到Sky,就new一個新的user回傳。
            if (userName == "Sky")
            {
                User user = new User();

                user.name = "Sky";
                user.address = "台中";
                return user;
            }
            return null;
        }
    }
}

好的,完成了以後,接下來我們來實作View的部分,那View在哪裡?,以我這邊的範例,頁面是SupervisingControllerView.aspx,所以,View就是SupervisingControllerView.aspx和SupervisingControllerView.cs檔。

而在準備View之前,也必須先準備好Interface,"ISupervisingControllerDetialView”,然後裡面定義了SearchUser、user兩個必須實作的方法,其實這兩個方法,就是未來要給Presenter傳遞給View要顯示用的。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WebAppMvpDemo
{
    public interface ISupervisingControllerDetialView
    {
        string SearchUser { get; }
        User user { set; }
    }
}

ok~接下來我們要處理SupervisingControllerView.cs了,雖然程式很簡單,但還是很長,而且有很多地方需要注意的,首先,我們會讓此類別( SupervisingControllerView )去實作此介面,目的也是一樣,為了降低耦合與測試。而我們也會在這個類別裡面去實作Presenter,目前我們還沒寫到Presenter,但這邊會用到Presenter的原因其實很簡單,因為是由Presenter來控制,如最下面的Button Click所看到,我們沒有將程式寫在SupervisingControllerView裡面了,而是去呼叫Presenter的OnUserSearched方法;接下來往回看一點,有兩個存取控制項的方法,這些方法會利用Model來回傳或是設定控制項 ( 這個範例我們使用User這個Model去塞到控制項裡面去做顯示 );最後,最關鍵的是new Presenter(this)這段,這段也就是說把SupervisingControllerView塞到Presenter裡面去,讓Presenter能參照到SupervisingControllerView,這代表甚麼呢?也就是說,Presenter可以控制到SupervisingControllerView,所以Presenter裡面不會再有SearchUserTextBox.Text這種和控制項有關的程式碼,而通通都是透過SupervisingControllerView所定義的方法來取得控制項的資訊,換言之,Presenter和控制項再也無關,View就是View,Presenter就是Presenter,個兼其職。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebAppMvpDemo
{
    //讓此類別實作IDefault2DetialView
    public partial class SupervisingControllerView : System.Web.UI.Page, ISupervisingControllerDetialView
    {
        //目前還沒寫到此類別。也就是MVP的P
        private SupervisingControllerPresenter _presenter;

        protected void Page_Load(object sender, EventArgs e)
        {
            //將自己,也就是SupervisingControllerView注入到P裡面去,目的是為了讓P能控制View。
            _presenter = new SupervisingControllerPresenter(this);
        }

        public User user
        {
            //直接讓View去繫結Model,
            //所以在架構圖上View和Model有一條線的原因就在這裡。
            set { 
                UserAddressLabel.Text = value.address;
                UserNameLabel.Text = value.name; 
            }
        }

        public string SearchUser
        {
            get { return SearchUserTextBox.Text; }
        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            //當Click事件發生的時候,不由View處理,而是轉交給Presenter處理。
            _presenter.OnUserSearched();
        }

    }
}

然後我們看一下Presenter,其實Presenter還比較簡單一點,如果你已經看到這邊,恭喜你,快解脫了XDD;Presenter,會定義兩個介面,而利用建構子的方式傳進有實作這兩個介面的物件,換言之,當我們要測試的時候,就可以傳入有實作這兩個介面的物件進來 ( 通常我們使用Mock來做模擬物件 ),而這也是為什麼前面要一直定義介面的關係,就是為了降低耦合,讓測試能更好進行,至於OnUserSearched方法,其實和之前的程式沒有多大改變,就只是進行了資料撈取的動作,只是不會直接寫到控制項裡面去了,而是會透過實作ISupervisingControllerDetialView的物件來進行存取的動作,好處就如之前說的,隔離了控制項的部分,讓耦合降更低。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class SupervisingControllerPresenter
    {
        //定義這兩個型別,這也是關鍵,也就是說,我們可以自行去設計並抽換。
        //也因此和資料存取還有View的部分就沒那麼緊密的耦合。
        private readonly IUserRepository _userRepository;
        private readonly ISupervisingControllerDetialView _view;

        //這裡的建構子使用一種叫做建構子鍊的方式,
        //但關注的是,我們利用建構子來傳進有實作的介面的物件,
        //換言之,我們可以自行去設計,只要有實作介面即可。
        //( 但是比較建議使用Moq方式,可以再參考我的Blog.... )
        public SupervisingControllerPresenter(ISupervisingControllerDetialView view)
            : this(view, new UserRepository())
        {
        }

        public SupervisingControllerPresenter(ISupervisingControllerDetialView view, IUserRepository userService)
        {
            _view = view;
            _userRepository = userService;
        }

        //這裡定義一個方法,就是當按下Button時,View會進行此方法的呼叫。
        public void OnUserSearched()
        {
            if (string.IsNullOrEmpty(_view.SearchUser))
            {
                return;
            }

            User user = _userRepository.GetByName(_view.SearchUser);
            if (user == null)
            {
                return;
            }

            //和Presenter View最大的差異,傳遞給View的不再是User的屬性,
            //而是整個Model
            _view.user = user;

        }
    }
}

結論

其實到這邊,使用Supervising Controller架構的程式已經完成了。

我們可以看到,整體是透過Presenter來進行整個的控制,當View有事件發生時,會轉交給Presenter來進行處理,而Presenter也會利用View給的介面來進行資料的存取;另一方面,透過各種資料存取方式來取得的Model,也會利用Presenter來傳遞給View。

最後照老規局,我們把圖重新貼回來看看,如下圖,View和Presenter的關係是雙向的,但是Presenter透過ISupervisingControllerDatialView這個介面來降低與View的耦合,也就是說,Presenter可以不用太在乎View,可以更方便的去進行測試;另一方面,Model最為獨立,所以可以專心的去開發Model,而View的部分,就專心的去呈現頁面的顯示;所以結論就可以很輕易地去拆開進行測試!

image

Presenter View的差異

如果之前有讀過Presenter View,然後還把這篇讀完的人,真的辛苦了,但是兩者間的差異其實非常小,而且其實也只差異於View和Model之間的那條線。

簡單的說Presenter View的Presenter是完全掌控Model,View提供的是所有控制項的接口,但是Supervising Controller裡面,Presenter則沒有完全的去控制,而是將Model傳遞到View裡面,換言之Supervising Controller裡面的View,提供不在是一個一個的控制項接口,而是可以直接將Model傳遞進去的方法,如下,是Supervising Conroller的View程式碼。

public User user
{
    //直接讓View去繫結Model,
    //所以在架構圖上View和Model有一條線的原因就在這裡。
    set { 
        UserAddressLabel.Text = value.address;
        UserNameLabel.Text = value.name; 
    }
}

而這裡的是Presenter View的程式碼。

public string UserName
{
    set { UserNameLabel.Text = value; }
}

public string UserAddress
{
    set { UserAddressLabel.Text = value; }
}

而在Presenter裡面的差異,Supervising Controller程式碼

//這裡定義一個方法,就是當按下Button時,View會進行此方法的呼叫。
public void OnUserSearched()
{
    if (string.IsNullOrEmpty(_view.SearchUser))
    {
        return;
    }

    User user = _userRepository.GetByName(_view.SearchUser);
    if (user == null)
    {
        return;
    }

    //和Presenter View最大的差異,傳遞給View的不再是User的屬性,
    //而是整個Model
    _view.user = user;

}

另外,這是Presenter View的程式碼,差別就在於Presenter需要一個一個去設定。

//這裡定義一個方法,就是當按下Button時,View會進行此方法的呼叫。
public void OnUserSearched()
{
    if (string.IsNullOrEmpty(_view.SearchUser))
    {
        return;
    }

    User user = _userRepository.GetByName(_view.SearchUser);
    if (user == null)
    {
        return;
    }

    _view.UserName = user.name;
    _view.UserAddress = user.address;

}

換言之,Supervising Controller的程式碼比較簡潔,畢竟,如果今天有好幾個頁面需要塞入幾十個控制項,Presenter View就會顯得複雜,但同樣的,如果今天的頁面邏輯是很複雜的( 例如又要搜尋,又要顯示下一筆,又要顯示客戶的訂單明細等等等 ),那Supervising Controller的處理就變得不好去測試了,所以也有人說Supervising Controller比較適合ASP.NET,而Presenter View比較適合Win Form架構。

MVC 和 MVP

如果是拿Presenter View和MVC比,大概也沒什麼好比的,因為兩者差異性還滿大的,但是如果Supervising Controller和MVC相比,其實MVC是比Supervising Controller更早出來,而Supervising Controller的演進也是由Presenter View而來,目的是為了處理當時的ASP.NET架構( 沒想到最後又回到ASP.NET MVC的MVC架構 ),如果要講,我們可以說Supervising Controller是有狀態的,而MVC是適用於無狀態的;MVC架構下的Controller和View的繫結沒那麼深,以MVC來說,當請求到Controller,並決定哪個Action產出View後,就和Controller沒關係了,直到View送出Post時,會再回到Controller裡面,但是Post後的Controller和第一次的Controller,雖然可能是同一個類別,但兩者卻沒有任何關係( 也就是說沒有狀態,不需要維持與View之間的關係 ),但是如果是Supervising Controller來看,View送出Post後,回到Presenter,那一定是原來的Presenter( 也就是有狀態的關係,會和View進行維持 ),所以ASP.NET想要真的實作MVC是有難度的( 因為一堆ViewState和ControlState,微軟就是要讓ASP.NET有狀態… ),而這也是最大的差異。

最後

終於把三大M家族講得差不多了 ( 好吧,我承認還有近期WPF、Silverlight使用的MVVM..QQ ),雖然這些東西的差異感覺都很小,但本質上卻有很大的差異,如何運用,該用在怎樣的地方,就變成一個學問,也希望大家耐心看完後,能真正的了解M家族的不同。

Sky & Study4.TW