|
為什么要寫這篇文章
筆者當前正在負責研究所中一個項目,這個項目基于.NET平臺,初步擬采用C/S部署體系,所以選擇了Windows Forms作為其UI。經過幾此迭代,我們發現了一個問題:雖然業務邏輯已經封裝到Services層中,但諸多的UI邏輯仍然彌漫在各個事件Listener中,使得UI顯得臃腫不堪,并且存在諸多重復性代碼。另外,需求提供方說,根據實際需要,不排除將部署結構改為B/S的可能性,甚至可能會要求此系統同時支持C/S和B/S兩種部署方式。那么,如果保持目前將UI邏輯編碼到Windows Forms中的方式,到時這些UI邏輯將無法復用,修改部署方式的代價很大。
為了解決以上兩個問題,筆者和相關人員商量后,決定引入既有成熟模式,重新設計表示層的架構方式,并重構既有代碼。
提到表示層(Presentation Layer)的模式,我想大家腦海中第一個閃過的很可能是經典的MVC(Model-View-Controller)。我最初也準備使用MVC,但經過分析和實驗后,我發現MVC并不適合目前的情況,因為MVC的結構相對復雜,Model和View之間要實現一個Observer模式,并實現雙向通信。這樣重構起來Services層也必須修改。我并不想修改Services層,而且我想將View和Model徹底隔離,因為我個人并不喜歡View和Model直接通信的架構方式。最終,我選擇了MVP(Model-View-Presenter)模式。
經過兩天的重構和驗證,目前已經將MVP正式引入項目的表示層,并且解決了上文提到的兩個問題。在這期間,積累了少許關于在.NET平臺上實踐MVP的經驗,在這里匯集成此文,和朋友們共享。
UI與P Logic
首先,我想先明確一下UI和P Logic的概念。
表示層可以拆分為兩個部分:User Interface(簡稱UI)和Presentation Logic(簡稱P Logic)。
UI是系統與用戶交互的界面性概念,它的職責有兩個——接受用戶的輸入和向用戶展示輸出。UI應該是一個純靜態的概念,本身不應包含任何邏輯,而單純是一個接受輸入和展示輸出的“外殼”。例如,一個不包含邏輯的Windows Form,一張不包含邏輯的頁面,一個不包含邏輯的Flex界面,都屬于UI。
P Logic是表示層應有的邏輯性內容。例如,某個文本內容不能為空,當某個事件發生時獲取界面上哪些內容,這都屬于P Logic。應該指出,P Logic應該是抽象于具體UI的,它的本質是邏輯,可以復用到任何與此邏輯相符的UI。
UI與P Logic之間的聯系是事件,UI可以根據用戶的動作觸發各種事件,P Logic響應事件并執行相應的邏輯。P Logic對UI存在約束作用,P Logic規定一套UI契約,UI要根據契約實現,才能被相應的P Logic調用。
下圖展示了UI與P Logic的結構及交互原理。
圖1、UI與P Logic
Model-View-Presenter模式
MVP模式最早由Taligent的Mike Potel在《MVP: Model-View-Presenter The Taligent Programming Model for C++ and Java》(點擊這里下載)一文中提出。MVP的提出主要是為了解決MVC模式中結構過于復雜和模型-視圖耦合性過高的問題。MVP的核心思想是將UI分離成View,將P Logic分離成Presenter,而業務邏輯和領域相關邏輯都分離到Model中。View和Model完全解除耦合,不再像MVC中實現一個Observer模式,兩者的通信則依靠Presenter進行。Presenter響應View接獲的用戶動作,并調用Model中的業務邏輯,最后將用戶需要的信息返回給View。
下圖直觀表示了MVP模式:
圖2、MVP模式
圖2清楚地展示了MVP模式的幾個特點:
1、View和Model完全解耦,兩者不發生直接關聯,通過Presenter進行通信。
2、Presenter并不是與具體的View耦合,而是和一個抽象的View Interface耦合,View Interface相當于一個契約,抽象出了對應View應實現的方法。只要實現了這個接口,任何View都可以與指定Presenter兼容,從而實現了P Logic的復用性和視圖的無縫替換。
3、View在MVP里應該是一個“極瘦”的概念,最多也只能包含維護自身狀態的邏輯,而其它邏輯都應實現在Presenter中。
總的來說,使用MVP模式可以得到以下兩個收益:
1、將UI和P Logic兩個關注點分離,得到更干凈和單一的代碼結構。
2、實現了P Logic的復用以及View的無縫替換。
在.NET平臺上實現MVP模式
這一節通過一個示例程序展示在.NET平臺上實現MVP的一種實踐方法。本來想通過我目前負責的實際項目中的代碼片段作為Demo,但這樣做存在兩個問題:一是這樣做可能會違反學校的保密守則,二是這個項目應用了許多其他框架和模式,如通過Unity實現依賴注入,通過PostSharp實現AOP來負責異常處理和事務管理等,通過NHibernate實現的ORM等等,這樣如果讀者不了解系統整體架構就很難完全讀懂代碼片段,MVP模式不夠突出。因此,我專門為這篇文章實現了一個Demo,其中的MVP實踐方式與實際項目中是一致的,而且Demo規模小,排除了其他干擾,使得讀者更容易理解其中的MVP實現方式。
這個簡單的Demo運行效果如下:
圖3、Demo界面
這個Demo的功能如下:這是一個簡單的點餐軟件。系統中存有餐廳所有菜品的信息,客戶只需在界面右側輸入菜品名稱和數量,單擊“添加”按鈕,菜品就會被添加到左側點餐列表,并顯示此菜品詳細信息。如果所點菜品不存在則軟件會給出提示。另外,在左側已點餐品列表中右鍵單擊某個條目,在彈出菜單中點擊“刪除”,則可將此菜品從列表刪除。
下面分步驟介紹應用了MVP模式的實現方式。
第一步,解決方法及工程結構
這個Demo共有三個工程,MVPSimple.Model為Mock方式實現的Services,作為Model;MVPSimple.Presenters為Presenter工程,其中包括Presenter和View Interface;MVPSimple.WinUI為View的Windows Forms實現。
第二步,構建Mock方式的Services
因為重點在于表示層,所以這里的Services使用了Mock方式,并沒有包含真正的業務領域邏輯。其中MVPSimple.Model工程里兩個文件的代碼如下:
FoodDto.cs:
using System;namespace MVPSimple.Model{ /// <summary> /// 表示菜品類別的枚舉類型 /// </summary> public enum FoodType { 主菜 = 1, 湯 = 2, 甜品 = 3, } /// <summary> /// 菜品的Data Transfer Object /// </summary> public class FoodDto { /// <summary> /// ID,標識字段 /// </summary> public Int32 ID { get; set; } /// <summary> /// 菜品名稱 /// </summary> public String Name { get; set; } /// <summary> /// 菜品類型 /// </summary> public FoodType Type { get; set; } /// <summary> /// 菜品價格 /// </summary> public Double Price { get; set; } /// <summary> /// 點菜數量 /// </summary> public Int32 Amount { get; set; } }}
FoodServices.cs:
using System;using System.Collections.Generic;namespace MVPSimple.Model{ /// <summary> /// 菜品Services的Mock實現 /// </summary> public class FoodServices { private IList<FoodDto> foodList = new List<FoodDto>(); /// <summary> /// 默認構造函數,初始化各個菜品 /// </summary> public FoodServices() { this.foodList.Add( new FoodDto() { ID = 1, Name = "牛排", Price = 60.00, Type = FoodType.主菜, } ); this.foodList.Add( new FoodDto() { ID = 2, Name = "法式蝸牛", Price = 120.00, Type = FoodType.主菜, } ); this.foodList.Add( new FoodDto() { ID = 3, Name = "水果沙拉", Price = 58.00, Type = FoodType.甜品, } ); this.foodList.Add( new FoodDto() { ID = 4, Name = "奶油紅菜湯", Price = 15.00, Type = FoodType.湯, } ); this.foodList.Add( new FoodDto() { ID = 5, Name = "雜拌湯", Price = 20.00, Type = FoodType.湯, } ); } /// <summary> /// 按照菜品名稱獲取菜品詳細信息 /// </summary> /// <param name="foodName">菜品名稱</param> /// <returns>含有指定菜品信息的DTO</returns> public FoodDto GetFoodDetailByName(String foodName) { foreach (FoodDto f in this.foodList) { if (f.Name.Equals(foodName)) { return f; } } return new FoodDto() { ID = 0 }; } }}
第三步,通過View Interface規定View契約
如果想實現Presenter和View的交互和無縫替換,必須在它們之間規定一個契約。一般來說,每一張界面(注意是界面不是視圖)都應該對應一個View接口,不過由于Demo只有一個頁面,所以也只有一個View接口。
這里需要特別強調,View接口必須抽象于任何具體視圖而服務于Presenter,所以,View接口中絕不能出現任何與具體視圖相關的元素。例如,我們的Demo中是使用Windows Forms作為視圖實現,但View接口中絕不可出現與Windows Forms相耦合的元素,如返回一個Winform的TextBox。因為如果這樣做的話,使用其他技術實現的View就無法實現這個接口了,如使用Web Forms實現,而Web Forms是不可能返回一個Winform的TextBox的。
下面給出視圖接口的代碼。
IMainView.cs:
using System;using System.Collections.Generic;using MVPSimple.Model;namespace MVPSimple.Presenters{ /// <summary> /// MainView的接口,所有MainView必須實現此接口,此接口暴露給Presenter /// </summary> public interface IMainView { /// <summary> /// View上的菜品名稱 /// </summary> String foodName { get; set; } /// <summary> /// View上點菜數量 /// </summary> Int32 Amount { get; set; } /// <summary> /// 判斷某一菜品是否已經存在于點菜列表中 /// </summary> /// <param name="foodName">菜品名稱</param> /// <returns>結果</returns> bool IsExistInList(String foodName); /// <summary> /// 將某一菜品加入點菜列表 /// </summary> /// <param name="food">菜品DTO</param> void AddFoodToList(FoodDto food); /// <summary> /// 將某一已點菜品從列表中移除 /// </summary> /// <param name="foodName">欲移除的菜品名稱</param> void RemoveFoodFromList(String foodName); /// <summary> /// View顯示提示信息給用戶 /// </summary> /// <param name="message">信息內容</param> void ShowMessage(String message); /// <summary> /// View顯示確認信息并返回結果 /// </summary> /// <param name="message">信息內容</param> /// <returns>用戶回答是確定還是取消。True - 確定,False - 取消</returns> bool ShowConfirm(String message); }}
可以看到,IMainView抽象了如圖3所示的界面,但又不包含任何與Windows Forms相耦合的元素,因此如果需要,以后完全可以使用Web Forms、WPF或SL等技術實現這個接口。
第四步,實現Presenter
上文說過,一個界面應該對應一個Presenter,這個Demo里只有一個界面,所以只有一個Presenter。Presenter僅于視圖接口耦合,而并不和具體視圖耦合,最好證據就是Presenter工程根本沒有引用WinUI工程!代碼如下:
MainPresenter.cs:
using System;using System.Collections.Generic;using MVPSimple.Model;namespace MVPSimple.Presenters{ /// <summary> /// MainView的Presenter /// </summary> public class MainPresenter { /// <summary> /// 當前關聯View /// </summary> public IMainView View { get; set; } /// <summary> /// 默認構造函數,初始化View /// </summary> /// <param name="view">MainView對象</param> public MainPresenter(IMainView view) { View = view; } #region Acitons /// <summary> /// Action:將所點菜品增加到點菜列表 /// </summary> public void AddFoodAction() { if (String.IsNullOrEmpty(View.foodName)) { View.ShowMessage("請選輸入菜品名稱"); return; } if (View.Amount <= 0) { View.ShowMessage("點菜的份數至少要是一份"); return; } if (View.IsExistInList(View.foodName)) { View.ShowMessage(String.Format("菜品【{0}】已經在您的菜單中", View.foodName)); return; } FoodServices foodServ = new FoodServices(); FoodDto food = foodServ.GetFoodDetailByName(View.foodName); if (food.ID == 0) { View.ShowMessage(String.Format("抱歉,本餐廳沒有菜品【{0}】",View.foodName)); return; } View.AddFoodToList(food); } /// <summary> /// Action:從點菜列表移除某一菜品 /// </summary> /// <param name="foodName">被移除菜品的名稱</param> public void RemoveFoodAction(String foodName) { if (View.ShowConfirm("確定要刪除嗎?")) { View.RemoveFoodFromList(foodName); } } #endregion }}
第五步,實現View
這里我們使用Windows Forms實現View。如果朋友們有興趣,完全可以自己試著用Web或WPF實現以下視圖,同時可以驗證P Logic的可復用性和視圖無縫替換,親身體驗一下MVP模式的威力。Winform的View代碼如下。
frmMain.cs:
using System;using System.Windows.Forms;using MVPSimple.Model;using MVPSimple.Presenters;namespace MVPSimple.WinUI{ /// <summary> /// MainView的Windows Forms實現 /// </summary> public partial class frmMain : Form, IMainView { /// <summary> /// 相關聯的Presenter /// </summary> private MainPresenter presenter; /// <summary> /// 默認構造函數,初始化Presenter /// </summary> public frmMain() { InitializeComponent(); this.presenter = new MainPresenter(this); } #region IMainView Members /// <summary> /// View上的菜品名稱 /// </summary> public String foodName { get { return this.tbFoodName.Text; } set { this.tbFoodName.Text = value; } } /// <summary> /// View上點菜數量 /// </summary> public Int32 Amount { get { return (Int32)this.tbAmount.Value; } set { this.tbAmount.Value = (Decimal)value; } } /// <summary> /// 判斷某一菜品是否已經存在于點菜列表中 /// </summary> /// <param name="foodName">菜品名稱</param> /// <returns>結果</returns> public bool IsExistInList(String foodName) { foreach (ListViewItem i in this.lvFoods.Items) { if (i.Text == foodName) { return true; } } return false; } /// <summary> /// 將某一菜品加入點菜列表 /// </summary> /// <param name="food">菜品DTO</param> public void AddFoodToList(FoodDto food) { ListViewItem item = new ListViewItem(); Double price = food.Price * (Double)this.tbAmount.Value; item.Text = food.Name; item.SubItems.Add(food.Type.ToString()); item.SubItems.Add(this.tbAmount.Value.ToString()); item.SubItems.Add(price.ToString()); this.lvFoods.Items.Add(item); } /// <summary> /// 將某一已點菜品從列表中移除 /// </summary> /// <param name="foodName">欲移除的菜品名稱</param> public void RemoveFoodFromList(String foodName) { foreach (ListViewItem i in this.lvFoods.Items) { if (i.Text == foodName) { this.lvFoods.Items.Remove(i); } } } /// <summary> /// View顯示提示信息給用戶 /// </summary> /// <param name="message">信息內容</param> public void ShowMessage(String message) { MessageBox.Show(message, "信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); } /// <summary> /// View顯示確認信息并返回結果 /// </summary> /// <param name="message">信息內容</param> /// <returns>用戶回答是確定還是取消。True - 確定,False - 取消</returns> public bool ShowConfirm(String message) { DialogResult result = MessageBox.Show(message, "確認", MessageBoxButtons.OKCancel, MessageBoxIcon.Question); return DialogResult.OK == result; } #endregion #region Event Listeners private void btnAdd_Click(object sender, EventArgs e) { this.presenter.AddFoodAction(); } private void miDeleteFood_Click(object sender, EventArgs e) { if (this.lvFoods.SelectedItems.Count != 0) { String foodName = this.lvFoods.SelectedItems[0].Text; this.presenter.RemoveFoodAction(foodName); } } #endregion }}
可以看到,使用了MVP后,View的代碼變的非常干凈整潔,以前充斥著厚重表示邏輯的事件Listener方法變得“瘦”了許多。
總結
這篇文章首先討論表示層的組成,說明User Interface和Presentation Logic是表示層的兩個重要組成部分,并分別說明了兩者的作用及交互方式。接著討論了MVP模式。最后,通過一個Demo展示了在.NET平臺上實現MVP的一種實踐方式。應該說,MVP很類似簡化了MVC,MVP不但可以分離關注、使得代碼變得干凈整潔、并實現P Logic的復用,而且實現起來比MVC在結構上要簡單很多。MVP是一種模式,本身有諸多實現方式,本文只是介紹了筆者使用的一種實踐,朋友們也可以在此基礎上摸索自己的實踐。
NET技術:.NET平臺上的Model-View-Presenter模式實踐,轉載需保留來源!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。