.Net平臺-MVP模式初探(一)


爲何要寫這篇文章

      筆者當前正在負責研究所中一個項目,這個項目基於.NET平臺,初步擬採用C/S部署體系,因此選擇了Windows Forms做爲其UI。通過幾此迭代,咱們發現了一個問題:雖然業務邏輯已經封裝到Services層中,但諸多的UI邏輯仍然瀰漫在各個事件Listener中,使得UI顯得臃腫不堪,而且存在諸多重複性代碼。另外,需求提供方說,根據實際須要,不排除將部署結構改成B/S的可能性,甚至可能會要求此係統同時支持C/S和B/S兩種部署方式。那麼,若是保持目前將UI邏輯編碼到Windows Forms中的方式,到時這些UI邏輯將沒法複用,修改部署方式的代價很大。html

      爲了解決以上兩個問題,筆者和相關人員商量後,決定引入既有成熟模式,從新設計表示層的架構方式,並重構既有代碼。架構

      提到表示層(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的概念。post

      表示層能夠拆分爲兩個部分:User Interface(簡稱UI)和Presentation Logic(簡稱P Logic)。this

      UI是系統與用戶交互的界面性概念,它的職責有兩個——接受用戶的輸入和向用戶展現輸出。UI應該是一個純靜態的概念,自己不該包含任何邏輯,而單純是一個接受輸入和展現輸出的「外殼」。例如,一個不包含邏輯的Windows Form,一張不包含邏輯的頁面,一個不包含邏輯的Flex界面,都屬於UI。編碼

      P Logic是表示層應有的邏輯性內容。例如,某個文本內容不能爲空,當某個事件發生時獲取界面上哪些內容,這都屬於P Logic。應該指出,P Logic應該是抽象於具體UI的,它的本質是邏輯,能夠複用到任何與此邏輯相符的UI。spa

      UI與P Logic之間的聯繫是事件,UI能夠根據用戶的動做觸發各類事件,P Logic響應事件並執行相應的邏輯。P Logic對UI存在約束做用,P Logic規定一套UI契約,UI要根據契約實現,才能被相應的P Logic調用。設計

      下圖展現了UI與P Logic的結構及交互原理。3d

圖一、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模式:

圖二、MVP模式

      圖2清楚地展現了MVP模式的幾個特色:

      一、View和Model徹底解耦,二者不發生直接關聯,經過Presenter進行通訊。

      二、Presenter並非與具體的View耦合,而是和一個抽象的View Interface耦合,View Interface至關於一個契約,抽象出了對應View應實現的方法。只要實現了這個接口,任何View均可以與指定Presenter兼容,從而實現了P Logic的複用性和視圖的無縫替換。

      三、View在MVP裏應該是一個「極瘦」的概念,最多也只能包含維護自身狀態的邏輯,而其它邏輯都應實如今Presenter中。

      總的來講,使用MVP模式能夠獲得如下兩個收益:

      一、將UI和P Logic兩個關注點分離,獲得更乾淨和單一的代碼結構。

      二、實現了P Logic的複用以及View的無縫替換。

在.NET平臺上實現MVP模式

      這一節經過一個示例程序展現在.NET平臺上實現MVP的一種實踐方法。原本想經過我目前負責的實際項目中的代碼片斷做爲Demo,但這樣作存在兩個問題:一是這樣作可能會違反學校的保密守則,二是這個項目應用了許多其餘框架和模式,如經過Unity實現依賴注入,經過PostSharp實現AOP來負責異常處理和事務管理等,經過NHibernate實現的ORM等等,這樣若是讀者不瞭解系統總體架構就很難徹底讀懂代碼片斷,MVP模式不夠突出。所以,我專門爲這篇文章實現了一個Demo,其中的MVP實踐方式與實際項目中是一致的,並且Demo規模小,排除了其餘干擾,使得讀者更容易理解其中的MVP實現方式。

      這個簡單的Demo運行效果以下:

圖三、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:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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方法變得「瘦」了許多。

      完成以上幾步後,就能夠運行這個Demo看效果了。

 

總結

      這篇文章首先討論表示層的組成,說明User Interface和Presentation Logic是表示層的兩個重要組成部分,並分別說明了二者的做用及交互方式。接着討論了MVP模式。最後,經過一個Demo展現了在.NET平臺上實現MVP的一種實踐方式。應該說,MVP很相似簡化了MVC,MVP不但能夠分離關注、使得代碼變得乾淨整潔、並實現P Logic的複用,並且實現起來比MVC在結構上要簡單不少。MVP是一種模式,自己有諸多實現方式,本文只是介紹了筆者使用的一種實踐,朋友們也能夠在此基礎上摸索本身的實踐。


PS:

  本文來講比較通俗易懂,對於理解起來也相對容易,想對MVP有更多的瞭解,請關注個人下一篇文章   <.NET平臺上的 MVP 模式再探(二)>

相關文章
相關標籤/搜索