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好了… ) ,以下是他的圖。
如果要說MVP的Passive View,最簡單的講法就是Model與View是完全沒溝通的,也就是說Presenter有絕對的控制權( 控制控阿!! )。
正式開始
自從上次講了MVC架構後,發現與其一開始說那麼多,大家還是聽不懂,還不如直接先給範例,就如比古清十郎所說的,從實戰中學習吧!劍心~。
假設今天我們要用ASP.NET寫一個查詢程式,非常非常簡單的查詢,只要輸入,名子,就會尋找到指定的資料,並且顯示於下方,畫面大致上是這樣。
啥!?很醜!!,範例嘛,就不要太苛求了,總之,就是在TextBox輸入名子,按下Button後,會在下面兩個Label顯示姓名和地址…
程式碼大概長這樣。
我們會先有一個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圖
就如前面所說,Model ( User ) 完全和View沒有任何關係,取得的Model ( User ),由Presenter塞到View ( _Default2 )裡面去,而從View ( _Default2 )進來的事件,也由Present ( Default2Presenter )來處理,所以Present和Model可以很輕鬆的進行測試,不再需要開啟頁面按下Button,然後看看答案對不對,各單位之間的耦合也沒那麼強烈,它們之間的關係,變得更好維護了!!
後記
這個範例,我忘記把_default2改成比較好的名子,所以一堆相關的命名都是這種沒有意義的Default2,這是不好的習慣喔!!大家不要學習喔!!。