[上篇]通過采用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
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: }