2011年9月28日

UI的設計模式MVP模式 - Passive View

最近我朋友問了我一個問題,MVP和MVC架構有甚麼不一樣,我只很簡單的回答,MVP比較運用於Win Form、ASP.NET這種有狀態的程式,而MVC運用於無狀態的應用程式;事過幾天後,我還是覺得,這個問題很不錯,但因沒有好好地回答感到遺憾XDD,雖然現在網路上也有很多的"VS”了,但我還是決定把這一些模式敘說一次。

為什麼要有MVP模式?

其實MVP模式是MVC模式演進而來的,也可能MVC剛出來的時代,網路還沒開始發展,所以MVC不適用於Win Form這類程式架構,而衍生出MVP模式;但不管怎樣,他們要解決的問題,原因都相同,都是原本的架構不易測試,耦合太高,關聯太複雜,所以使用這些模式來解決這些問題,總之,測試是很重要的=w=。

斯斯有兩種,MVP也有兩種

老實說,我也不是考古團隊,我也沒有仔細的去翻歷史,但據我所知,MVP設計模式是有分兩種,最先提出來的是Martin Fowler,最後於2006年將MVP分成Supervising Controller和Passive View;基本上這兩個的概念是相同的,但最大的差別只在於那個"P" ( 也就是Presenter ),Presenter能控制的東西程度不同罷了,而這次的主題就是其中一個MVP模式Passive View。

Passive View

首先,既然是MVP還是要說明一下這三個字的縮寫,分別為Model、View、Presenter,翻成中文,大概也就是模型、視圖、主講者!? ( 好吧,我還是用英文Presenter好了… ) ,以下是他的圖。

image

如果要說MVP的Passive View,最簡單的講法就是Model與View是完全沒溝通的,也就是說Presenter有絕對的控制權( 控制控阿!! )。

正式開始

自從上次講了MVC架構後,發現與其一開始說那麼多,大家還是聽不懂,還不如直接先給範例,就如比古清十郎所說的,從實戰中學習吧!劍心~。

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

image

啥!?很醜!!,範例嘛,就不要太苛求了,總之,就是在TextBox輸入名子,按下Button後,會在下面兩個Label顯示姓名和地址…

image

 

程式碼大概長這樣。

我們會先有一個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的Passive View試試看。

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

( Repository模式和MVP Passive View模式,兩者之間並不是必須,也就是說,今天我實作MVP Passive View,是可以不用做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在哪裡?,以我這邊的範例,頁面是Default2.aspx,所以,View就是Default2.aspx和Default2.cs檔。

而在準備View之前,也必須先準備好Interface,"IDefault2DetialView”,然後裡面定義了SearchUser、UserNmae、UserAddress三個必須實作的方法,其實這三個方法,對應的就是SearchUserTextBox、UserNameLabel、UserAddressLabel這三個控制項,目的是希望未來透過此方法來存取這三個控制項,也因為我們的程式只需要取得SearchUserTextBox的Text值,和設定UserNameLabel和UserAddressLabel的值,所以裡面也只分別設定get和set。

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

namespace WebAppMvpDemo
{
    public interface IDefault2DetialView
    {
        string SearchUser { get; }
        string UserName { set; }
        string UserAddress { set; }
    }
}

ok~接下來我們要處理Default2.cs了,雖然程式很簡單,但還是很長,而且有很多地方需要注意的,首先,我們會讓此類別( _Default2 )去實作此介面,目的也是一樣,為了降低耦合與測試。而我們也會在這個類別裡面去實作Presenter,目前我們還沒寫到Presenter,但這邊會用到Presenter的原因其實很簡單,因為是由Presenter來控制,如最下面的Button Click所看到,我們沒有將程式寫在_Default2裡面了,而是去呼叫Presenter的OnUserSearched方法;接下來往回看一點,有三個存取控制項的方法,這些方法會回傳或是設定控制項,最後,最關鍵的是new Presenter(this)這段,這段也就是說把_Default2塞到Presenter裡面去,讓Presenter能參照到_Default2,這代表甚麼呢?也就是說,Presenter可以控制到_Default2,所以Presenter裡面不會再有SearchUserTextBox.Text這種和控制項有關的程式碼,而通通都是透過_Default2所定義的方法來取得控制項的資訊,換言之,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 _Default2 : System.Web.UI.Page ,IDefault2DetialView
    {
        //目前還沒寫到此類別。也就是MVP的P
        private Default2Presenter _presenter;

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

        //此類別必須實作IDefault2DetialView定義的方法。
        public string UserName
        {
                //可以看到,這裡就是用來設定控制項。
                set { UserNameLabel.Text = value; }
        }


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

        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方法,其實和之前的程式沒有多大改變,就只是進行了資料撈取的動作,只是不會直接寫到控制項裡面去了,而是會透過實作IDefault2DetialView的物件來進行存取的動作,好處就如之前說的,隔離了控制項的部分,讓耦合降更低。

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

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

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

        public Default2Presenter(IDefault2DetialView 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;
            }

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

        }
    }
}

結論

最後,看完了有甚麼感覺,恩,就是程式碼變的很多XDD,我們來重新看一下MVP的Passive View圖

image

就如前面所說,Model ( User ) 完全和View沒有任何關係,取得的Model ( User ),由Presenter塞到View ( _Default2  )裡面去,而從View ( _Default2 )進來的事件,也由Present ( Default2Presenter )來處理,所以Present和Model可以很輕鬆的進行測試,不再需要開啟頁面按下Button,然後看看答案對不對,各單位之間的耦合也沒那麼強烈,它們之間的關係,變得更好維護了!!

後記

這個範例,我忘記把_default2改成比較好的名子,所以一堆相關的命名都是這種沒有意義的Default2,這是不好的習慣喔!!大家不要學習喔!!。

沒有留言:

張貼留言