305
技術社區[雲棲]
MVC、MVP以及Model2[下篇]
[上篇]通過采用MVC模式,我們可以將可視化UI元素的呈現、UI處理邏輯和業務邏輯分別定義在View、Controller和Model中,但是對於三者之間的交互,MVC並沒有進行嚴格的限製。最為典型的就是允許View和Model繞開Controller進行直接交互,View不僅僅可以通過調用Model獲取需要呈現給用戶的數據,Model也可以直接通知View讓其感知到狀態的變化。當真正地將MVC應用於具體的項目開發中,不論是基於GUI的桌麵應用還是基於Web UI的Web應用,如果不對Model、View和Controller之間的交互進行更為嚴格的限製,我們編寫的程序可以比自治視圖更為難以維護。
今天我們將MVC視為一種模式(Pattern),但是作為MVC最初提出者的Trygve M. H. Reenskau實際是將MVC視為一種範例(Paradigm),這可以從它在《Applications Programming in Smalltalk-80(TM): How to use Model-View-Controller (MVC)》中對MVC的描述可以看出來:In the MVC paradigm the user input, the modeling of the external world, and the visual feedback to the user are explicitly separated and handled by three types of object, each specialized for its task.
模式和範例的區別在於前者可以直接應用到具體的應用上,而後者則僅僅提供一些基本的指導方針。在我看來MVC是一個很寬泛的概念,任何基於Model、View和Controller對UI應用進行分解的設計都可以成為MVC。當我們采用MVC的思想來設計UI應用的時候,應該根據應用框架(比如Windows Forms、WPF和Web Forms)的特點對Model、View和Controller的界限以及相互之間的交互設置一個更為嚴格的規則。在軟件設計的發展曆程中出現了一些MVC的變體(Varation),它們遵循定義在MVC中的基本原理。我們現在來簡單地討論一些一個常用的MVC變體。
目錄
MVP
PV與SoC
View和Presenter交互的規則(針對SoC模式)
實例演示:SoC模式的應用
Model 2ASP.NETMVC與Model2
MVP是一種廣泛使用的基於架構模式,使用與基於事件驅動的應用框架,比如ASP.NET Web Forms和Windows Forms應用。MVP中的M和V對應中MVC的Model和View,而P(Presenter)則自然代替了MVC中的Controller。但是MVP並非僅僅體現在從Controller到Presenter的轉換,更對地體現在Model、View和Presenter之間的交互上。
MVC模式中元素之間混亂的交互隻要體現在允許View和Model繞開Controller進行單獨“交流”,這在MVP模式中得到了徹底地解決。如下圖所示,能夠與Model直接進行交互的僅限於Presenter,View隻能間接地通過Presenter調用Model。Model的獨立性在這裏得到了真正的體現,它不僅僅與可視化元素的呈現無關(View)和與UI處理邏輯(Presenter)無關。使用MVP的應用是用戶驅動的而非Model驅動的,所以Model不需要主動通知View以提醒狀態發生了改變。
MVP不僅僅避免了View和Model之間的耦合,更進一步地降低Presenter對View的依賴。如圖1-2所示,Presenter依賴的是一個抽象化的View,即View實現的接口IView。這帶來的最直接的好處就是使定義在Presenter中的UI處理邏輯變得易於測試。由於Presenter對View的依賴行為定義在接口IView中,我們隻需要Mock一個實現了該接口的View就能對Presenter進行測試。
構成MVP三要素之間的交互體現在兩個方麵,即View/Presenter和Presenter/Model。Presenter和Model之間的交互很清晰,僅僅體現在Presenter對Model的單向調用。而View和Presenter之間該采用怎樣的交互方式是整個MVP的核心,MVP針對關注點分離的初衷能否體現在具體的應用中很大程度上取決於兩者之間的交互方式是否正確。按照View和Presenter之間的交互方式以及View本身的職責範圍,Martin Folwer將MVP可分為PV(Passive View)和SoC(Supervising Controller)兩種模式。
PV與SoC
解決View難以測試的最好的辦法就是讓它無須測試,如果View不需要測試,其先決條件就是讓它盡可能不涉及到UI處理邏輯,而這就是PV模式目的所在。顧名思義,PV(Passive View)是一個被動的View,針對包含其中的UI元素(比如控件)的操作不是由View自身來操作,而交給Presenter來操控。
如果我們純粹地采用PV模式來設計View,意味著我們需要將定義View中的UI元素通過屬性的形式暴露出來。具體來說,當我們在為View定義接口的時候,需要定義基於UI元素的屬性以使Presenter可以對View進行細粒度地操作,但這並不是意味著我們直接將View上的控件暴露出來。舉個簡單的例子,我們開發的HR係統 中具有如下圖所示的Web頁麵用於根據部門獲取員工列表。
現在通過ASP.NET Web Form應用來涉及這個頁麵,我們來討論一下如果采用PV模式View的接口該如何定義。對於Presenter來說,View供它操作的控件有兩個,一個是包含所有部門列表的DropDownList,另一個則是顯示員工列表的GridView。在頁麵加載的時候,Presenter將部門列表綁定在DropDownList上,與此同時包含所有員工的列表被綁定到GridView。當用戶選擇某個部門並點擊“查詢”按鈕後,View將包含篩選部門在內的查詢請求轉發給Presenter,後者篩選出相應的員工列表之後將其綁定到GridView。
如果我們為該View定義一個接口IEmployeeSearchView,我們不能像如下的代碼所示將上述這兩個控件直接以屬性的形式暴露出來。針對數據綁定對控件類型的選擇屬於View的內部細節(比如說針對部門列表的顯示,我們可以選擇DropDownList也可以選擇ListBox),不能體現在表示用於抽象View的接口中。在另一方麵,理想情況下定義在Presenter中的UI處理邏輯應該是與具體的技術平台無關的,如果在接口中涉及到了控件類型,這無疑將Presenter也具體的技術平台綁定在了一起。
1: public interface IEmployeeSearchView
2: {
3: DropDownList Departments { get;}
4: GridView Employees { get; }
5: }
正確的接口和實現該接口的View(一個Web頁麵)應該采用如下的定義方式。Presenter通過屬性Departments和Employees進行賦值進而實現對DropDownList和GridView進行綁定,通過屬性SelectedDepartment得到用戶選擇的篩選部門。為了盡可能讓接口隻暴露必須的信息,我們特意將對屬性的讀寫作了控製。
1: public interface IEmployeeSearchView
2: {
3: IEnumerable<string> Departments { set; }
4: string SelectedDepartment { get; }
5: IEnumerable<Employee> Employees { set; }
6: }
7:
8: public partial class EmployeeSearchView: Page, IEmployeeSearchView
9: {
10: //其他成員
11: public IEnumerable<string> Departments
12: {
13: set
14: {
15: this.DropDownListDepartments.DataSource = value;
16: this.DropDownListDepartments.DataBind();
17: }
18: }
19: public string SelectedDepartment
20: {
21: get { return this.DropDownListDepartments.SelectedValue;}
22: }
23: public IEnumerable<Employee> Employees
24: {
25: set
26: {
27: this.GridViewEmployees.DataSource = value;
28: this.GridViewEmployees.DataBind();
29: }
30: }
31: }
雖然從可測試性的角度來說PV模式是一種不錯的選擇,因為所有的UI處理邏輯全部定義在Presenter上,意味著所有的UI處理邏輯都可以被測試。但是我們需要將View可供操作的UI元素定義在對應的接口中,對於一些複雜的富客戶端(Rich Client)View來說,接口成員將會變得很多,這無疑會提升編程所需的代碼量。從另一方講,由於Presenter需要在控件級別對View進行細粒度的控製,這無疑會提供Presenter本身的複雜度,往往會使原本簡單的邏輯複雜化,在這種情況下我們往往采用SoC模式。
在SoC(Supervising Controller)模式下,為了降低Presenter的複雜度,我們將諸如數據綁定和格式化這樣簡單的UI處理邏輯邏輯轉移到View中,這些處理邏輯會體現在View實現的接口中。盡管View從Presenter中接管了部分UI處理邏輯,但是Presenter依然是整個三角關係的驅動者,View被動的地位依然沒有改變。對於用戶作用在View上的交互操作,View本身並不進行響應,而是直接將交互請求轉發給Presenter,後者在獨立完成相應的處理流程(可能涉及到針對Model的調用)之後會驅動View或者創建新的View作為對用戶交互操作的響應。
View和Presenter交互的規則(針對SoC模式)
View和Presenter之間的交互是整個MVP的核心,能夠正確地應用MVP模式來架構我們的應用極大地取決於能夠正確地處理View和Presenter兩者之間的關係。在由Model、View和Presenter組成的三角關係的核心不是View而是Presenter,Presenter不是View調用Model的中介,而是最終決定如何響應用戶交互行為的決策者。
打個比方,View是Presenter委派到前端的客戶代理,而作為客戶的自然就是最終的用戶。對於以鼠標/鍵盤操作體現的交互請求應該如何處理,作為代理的View並沒有決策權,所以它會將請求匯報給委托人Presenter。View向Presenter發送用戶交互請求應該采用這樣的口吻:“我現在將用戶交互請求發送給你,你看著辦,需要我的時候我會協助你”,而不應該是這樣:“我現在處理用戶交互請求了,我知道該怎麼辦,但是我需要你的支持,因為實現業務邏輯的Model隻信任你”。
對於Presenter處理用戶交互請求的流程,如果中間環節需要涉及到Model,它會直接發起對Model的調用。如果需要View的參與(比如需要將Model最新的狀態反應在View上),Presenter會驅動View完成相應的工作。
對於綁定到View上的數據,不應該是View從Presenter上“拉”回來的,應該是Presenter主動“推”給View的。從消息流(或者消息交換模式)的角度來講,不論是View向Presenter完成針對用戶交互請求的同誌,還是Presenter在進行交互請求處理過程中驅動View完成相應的UI操作,都是單向(One-Way)的。反應在 應用編程接口的定義上就意味著不論是定義在Presenter中被View調用的方法,還是定義在IView接口中被Presenter調用的方法最好都是沒有返回值得。如果不采用方法調用的形式,我們也可以通過事件注冊的方式實現View和Presenter的交互,事件機製體現的消息流無疑是單向的。
View本身僅僅實現單純的、獨立的UI處理邏輯,它處理的數據應該是Presenter實時推送給它的,所以View盡可能不維護數據狀態。定義在IView的接口最好隻包含方法,而避免屬性的定義,Presenter所需的關於View的狀態應該在接收到View發送的用戶交互請求的時候一次得到,而不需要通過View的屬性去獲取。
實例演示:SoC模式的應用
為了讓讀者對MVP模式,尤其是該模式下的View和Presenter之間的交互方式具有一個深刻的認識,我們現在來進行一個簡單的實例演示。本實例采用上麵提及的關於員工查詢的場景,並且采用ASP.NET Web Form來建立這個簡單的應用,最終呈現出來的效果如上圖所示。前麵我們已經演示了采用PV模式下的IView應該如何定義,現在我們來看看SoC模式下的IView有何不同。先來看看表示員工信息的數據類型如何定義,我們通過具有如下定義的數據類型Employee來表示一個員工。簡單起見,我們僅僅定義了表示員工基本信息(ID、姓名、性別、出生日期和部門)的5個屬性。
1: public class Employee
2: {
3: public string Id { get; private set; }
4: public string Name { get; private set; }
5: public string Gender { get; private set; }
6: public DateTime BirthDate { get; private set; }
7: public string Department { get; private set; }
8:
9: public Employee(string id, string name, string gender, DateTime birthDate, string department)
10: {
11: this.Id = id;
12: this.Name = name;
13: this.Gender = gender;
14: this.BirthDate = birthDate;
15: this.Department = department;
16: }
17: }
作為包含應用狀態和狀態操作行為的Model通過如下一個簡單的EmployeeRepository類型還體現。如代碼所示,表示所有員工列表的數據通過一個靜態字段來維護,而GetEmployees返回指定部門的員工列表。如果沒有指定篩選部門或者指定的部門字符為空,則直接返回所有的員工列表。
1: public class EmployeeRepository
2: {
3: private static IList<Employee> employees;
4: static EmployeeRepository()
5: {
6: employees = new List<Employee>();
7: employees.Add(new Employee("001", "張三", "男", new DateTime(1981, 8, 24), "銷售部"));
8: employees.Add(new Employee("002", "李四", "女", new DateTime(1982, 7, 10), "人事部"));
9: employees.Add(new Employee("003", "王五", "男", new DateTime(1981, 9, 21), "人事部"));
10: }
11: public IEnumerable<Employee> GetEmployees(string department = "")
12: {
13: if (string.IsNullOrEmpty(department))
14: {
15: return employees;
16: }
17: return employees.Where(e => e.Department == department).ToArray();
18: }
19: }
接下來我們來看作為View接口的IEmployeeSearchView的定義。如下麵的代碼片斷所示,該接口定義了BindEmployees和BindDepartments兩個方法,分別用於綁定基於部門列表的DropDownList和基於員工列表的DataView。除此之外,IEmployeeSearchView接口還定義了一個事件DepartmentSelected,該事件會在用戶選擇了篩選部門後點擊“查詢”按鈕時觸發。DepartmentSelected事件參數類型為自定義的DepartmentSelectedEventArgs,屬性Department表示用戶選擇部門。
1: public interface IEmployeeSearchView
2: {
3: void BindEmployees(IEnumerable<Employee> employees);
4: void BindDepartments(IEnumerable<string> departments);
5: event EventHandler<DepartmentSelectedEventArgs> DepartmentSelected;
6: }
7:
8: public class DepartmentSelectedEventArgs : EventArgs
9: {
10: public string Department { get; private set; }
11: public DepartmentSelectedEventArgs(string department)
12: {
13: Guard.ArgumentNotNullOrEmpty(department, "department");
14: this.Department = department;
15: }
16: }
作為MVP三角關係核心的Presenter通過具有如下定義的EmployeeSearchPresenter表示。如下麵的代碼片斷所示,表示View的隻讀屬性類型為IEmployeeSearchView接口,而另一個隻讀屬性Repository則表示作為Model的EmployeeRepository對象,兩個屬性均在構造函數中初始化。
1: public class EmployeeSearchPresenter
2: {
3: public IEmployeeSearchView View { get; private set; }
4: public EmployeeRepository Repository { get; private set; }
5:
6: public EmployeeSearchPresenter(IEmployeeSearchView view)
7: {
8: this.View = view;
9: this.Repository = new EmployeeRepository();
10: this.View.DepartmentSelected += OnDepartmentSelected;
11: }
12: public void Initialize()
13: {
14: IEnumerable<Employee> employees = this.Repository.GetEmployees();
15: this.View.BindEmployees(employees);
16: string[] departments = new string[] { "銷售部", "采購部", "人事部", "IT部" };
17: this.View.BindDepartments(departments);
18: }
19: protected void OnDepartmentSelected(object sender, DepartmentSelectedEventArgs args)
20: {
21: string department = args.Department;
22: var employees = this.Repository.GetEmployees(department);
23: this.View.BindEmployees(employees);
24: }
25: }
在構造函數中我們注冊了View的DepartmentSelected事件,作為事件處理器的OnDepartmentSelected方法通過調用Repository(即Model)實現了針對所選部門的篩選,而返回的員工列表通過調用View的BindEmployees方法實現了在View上的數據綁定。在Initialize方法中,我們通過調用Repository獲取了表示所有員工的列表,並通過View的BindEmployees方法顯示在界麵上;通過調用View的BindDepartments方法將作為篩選條件的部門列表綁定在View上。
最後我們來看看作為View的Web頁麵如何定義,如下所示的是作為頁麵主體部分的HTML,核心部分之包括一個用於綁定篩選部門列表的DropDownList和一個綁定員工列表的GridView。
1: <html xmlns="https://www.w3.org/1999/xhtml">
2: <head runat="server">
3: ...
4: </head>
5: <body>
6: <form id="form1" runat="server">
7: <div id="page">
8: <div class="top">
9: 選擇查詢部門:
10: <asp:DropDownList ID="DropDownListDepartments" runat="server" />
11: <asp:Button ID="ButtonSearch" runat="server" Text="查詢" OnClick="ButtonSearch_Click" />
12: </div>
13: <asp:GridView ID="GridViewEmployees" runat="server" AutoGenerateColumns="false" Width="100%">
14: <Columns>
15: <asp:BoundField DataField="Name" HeaderText="姓名" />
16: <asp:BoundField DataField="Gender" HeaderText="性別" />
17: <asp:BoundField DataField="BirthDate" HeaderText="出生日期" DataFormatString="{0:dd/MM/yyyy}" />
18: <asp:BoundField DataField="Department" HeaderText="部門"/>
19: </Columns>
20: </asp:GridView>
21: </div>
22: </form>
23: </body>
24: </html>
如下所示的是該Web頁麵的後台代碼的定義。它實現了定義在IEmployeeSearchView接口的兩個方法(BindEmployees和BindDepartments)和一個事件(DepartmentSelected)。表示Presenter的同名屬性在構造函數中被初始化。在頁麵加載的時候(Page_Load方法)Presenter的Initialize方法被調用,而在“查詢”按鈕被點擊的時候(ButtonSearch_Click)事件DepartmentSelected被觸發。
1: public partial class Default : Page, IEmployeeSearchView
2: {
3: public EmployeeSearchPresenter Presenter { get; private set; }
4: public event EventHandler<DepartmentSelectedEventArgs> DepartmentSelected;
5: public Default()
6: {
7: this.Presenter = new EmployeeSearchPresenter(this);
8: }
9: protected void Page_Load(object sender, EventArgs e)
10: {
11: if (!this.IsPostBack)
12: {
13: this.Presenter.Initialize();
14: }
15: }
16: public void BindEmployees(IEnumerable<Employee> employees)
17: {
18: this.GridViewEmployees.DataSource = employees;
19: this.GridViewEmployees.DataBind();
20: }
21: public void BindDepartments(IEnumerable<string> departments)
22: {
23: this.DropDownListDepartments.DataSource = departments;
24: this.DropDownListDepartments.DataBind();
25: }
26: protected void ButtonSearch_Click(object sender, EventArgs e)
27: {
28: string department = this.DropDownListDepartments.SelectedValue;
29: DepartmentSelectedEventArgs eventArgs = new DepartmentSelectedEventArgs(department);
30: if (null != DepartmentSelected)
31: {
32: DepartmentSelected(this, eventArgs);
33: }
34: }
35: }
Trygve M. H. Reenskau當初提出的MVC是作為基於GUI的桌麵應用的架構模式,並不太適合Web本身的特性。雖然MVC/MVP也可以直接用於ASP.NET Web Form應用,但這是因為微軟基於桌麵應用的編程模式 來設計基於Web Form的ASP.NET應用框架的。Web應用不同於GUI桌麵應用在於用戶是通過瀏覽器與應用進行交互,交互請求和相應是通過HTTP請求和回複來完成的。
為了讓MVC能夠Web應用提供原生的支持,另一個被稱為Model2 的MVC變體被提出來,Model2來源於基於Java的Web應用架構模式。Java Web應用具有兩種基本的架構模式,分別被稱為Model1和Model2。Model1類似於我們前麵提及的自治試圖模式,它將數據的可視化呈現和用戶交互操作的處理邏輯合並在一起。Model1使用於那些比較簡單的Web應用,對於相對複雜的應用應該采用Model 2。
為了讓開發者采用相應的編程模式進行GUI桌麵應用和Web應用的開發,微軟通過ViewState和Postback對背後的HTTP請求和回複機製進行了封裝,使我們能夠像編寫Windows Forms應用一樣采用事件驅動的方式進行ASP.NET Web Forms應用的編程。而Model 2采用完全不同的設計,它讓開發者直接麵向Web,讓他們關注HTTP的請求和回複流程,所以Model 2提供對Web應用原生的支持。
對於Web應用來說,和用戶直接交互的UI界麵由瀏覽器來提供,接下來我們詳細討論作為MVC的三要素是如何相互協作對從瀏覽器發出的用戶交互請求的響應的,下圖所示的序列圖體現了整個流程的全過程。
Model 2種一個HTTP請求的目標是Controller中的某個Action,後者體現為定義在Controller類型中的某個方法,所以對請求的處理最終體現在對Controller對象的激活和對Action方法的執行。一般來說,Controller、Action以及作為Action方法的部分參數(針對HTTP-GET)可以直接通過請求的URL解析出來。
如上圖所示,我們通過一個攔截器(Interceptor)對抵達Web服務器的HTTP請求進行攔截。一般的Web應用框架都提供了針對這樣一種攔截機製,對於ASP.NET來說,我們可以以HttpModule的形式來定義這麼一個攔截器。攔截器根據請求解析出目標Controller和對應的Action,Controller被激活之後Action方法被執行。對於需要傳入Action方法的輸入參數,則來源於請求地址或/和Post的數據。
在Controller的Action方法被執行過程中,它可以調用Model獲取或者改變其狀態。在Action方法執行的最後階段會選擇相應的View,綁定在View上的數據來源Model或者基於顯示要求進行得簡單邏輯計算,我們有時候它們成為VM(View Model),即基於View的Model(MVC中的Model是與UI無關的)。生成的View最終寫入HTTP回複並最終呈現在用戶的瀏覽器中。
和MVP一樣,Model 2完全隔斷了View和Model之間的聯係。Controller作為支配者地位在Model 2體現尤為明顯,用戶交互請求不再由View報告給Controller(Presenter),而是由攔截器直接轉發給Controller。Controller不僅僅決定著Model的調用,還決定了View的選擇和生成。ASP.NET MVC就是基於Model 2模式設計的。
憑著讀者對ASP.NET MVC的了解,通過上麵對Model2模式的介紹,應該很清楚地認識到ASP.NET MVC就是根據Model2模式設計的。基於HTTP請求的攔截機製是通過一個自定義的HttpModule和一個自定義HttpHandler來實現的,在本章的最後我們會通過一個例子來模擬ASP.NET MVC的工作原理。
在上麵我們多次強調MVC的Model是維持應用狀態提供業務功能的領域模型,或者是多層架構中進入業務層的入口或者業務服務的代理,但是ASP.NET MVC中的Model還是這個Model嗎?稍微了解ASP.NET MVC的讀者都知道ASP.NET MVC的Model僅僅是綁定到View上的數據而已,它和MVC模式中的Model並不是一回事。由於ASP.NET MVC中的Model是基於View的,我們可以將其稱為View Model。
由於ASP.NET MVC隻有View Model,所以ASP.NET MVC應用框架本社實際上僅僅關於View和Controller,真正的Model以及Model和Controller之間的交互體現在我們如何來設計Controller。我個人覺得將用於構建ASP.NET MVC的MVC模式成為M(Model)-V(View)-VM(View Model)-C(Controller)也許更為準確。
參考資料:
1、Dino Esposito,Andrea Saltarello《Micorsoft .NET Architecting Applications for Enterprise》
2、Adam Freeman, Steven Sanderson《Pro ASP.NET MVC 3 Framework》
3、Martin Fowler 《GUI Architectures》: https://martinfowler.com/eaaDev/uiArchs.html
4、Martin Fowler 《Passive View》:https://martinfowler.com/eaaDev/PassiveScreen.html
5、Martin Fowler 《Supervising Controller》:https://martinfowler.com/eaaDev/SupervisingPresenter.html
6、Mike Potel,VP & CTO,Taligent, Inc. 《MVP: Model-View-Presenter The Taligent Programming Model for C++ and Java》
7、Model View Controller- Wikipedia, the free encyclopedia:https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
8、Model View Presenter- Wikipedia, the free encyclopedia:https://en.wikipedia.org/wiki/Model_View_Presenter
9、Steve Burbeck, Ph.D. 《Applications Programming in Smalltalk-80(TM):How to use Model-View-Controller (MVC)》:https://st-www.cs.illinois.edu/users/smarch/st-docs/mvc.html
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 12:04:48