MVP模式

MVP模式定义

简称:MVP 全称:Model-View-Presenter MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。

模式特点

作为一种新的模式,MVPMVC有着一个重大的区别:在MVPView并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVCView会直接从Model中读取数据而不是通过 Controller

MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 在MVC模型里,更关注的Model的改变,而同时有多个对Model的不同显示,即View。所以,在MVC模型里,Model不依赖于View,但是View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。

虽然 MVC 中的 View 的确可以访问 Model,但是我们不建议在 View 中依赖 Model,而是要求尽可能把所有业务逻辑都放在 Controller 中处理,而 View 只和 Controller 交互。

 

 

 

 

实现方式:

MVP里,Presenter完全把ModelView进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,即重用! 不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试--而不需要使用自动化的测试工具。 我们甚至可以在ModelView都没有完成时候,就可以通过编写Mock Object(即实现了ModelView的接口,但没有具体的内容的)来测试Presenter的逻辑。 在MVP里,应用程序的逻辑主要在Presenter里实现,其中的View是很薄的一层。因此就有人提出了Presenter First的设计模式,就是根据User Story来首先设计和开发Presenter。在这个过程中,View是很简单的,能够把信息显示清楚就可以了。在后面,根据需要再随便更改View,而对Presenter没有任何的影响了。 如果要实现的UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在ViewPresenter之间放置一个Adapter。由这个 Adapter来访问ModelView,避免两者之间的关联。而同时,因为Adapter实现了View的接口,从而可以保证与Presenter之间接口的不变。这样就可以保证ViewPresenter之间接口的简洁,又不失去UI的灵活性。 在MVP模式里,View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容,绝不容许直接访问Model--这就是与MVC很大的不同之处。

第一步,解决方法及工程结构

      这个Demo共有三个工程,MVPSimple.ModelMock方式实现的Services,作为ModelMVPSimple.PresentersPresenter工程,其中包括PresenterView InterfaceMVPSimple.WinUIViewWindows 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契约

      如果想实现PresenterView的交互和无缝替换,必须在它们之间规定一个契约。一般来说,每一张界面(注意是界面不是视图)都应该对应一个View接口,不过由于Demo只有一个页面,所以也只有一个View接口。

      这里需要特别强调,View接口必须抽象于任何具体视图而服务于Presenter,所以,View接口中绝不能出现任何与具体视图相关的元素。例如,我们的Demo中是使用Windows Forms作为视图实现,但View接口中绝不可出现与Windows Forms相耦合的元素,如返回一个WinformTextBox。因为如果这样做的话,使用其他技术实现的View就无法实现这个接口了,如使用Web Forms实现,而Web Forms是不可能返回一个WinformTextBox的。

      下面给出视图接口的代码。

      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 FormsWPFSL等技术实现这个接口。

第四步,实现Presenter

      上文说过,一个界面应该对应一个Presenter,这个Demo里只有一个界面,所以只有一个PresenterPresenter仅于视图接口耦合,而并不和具体视图耦合,最好证据就是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。如果朋友们有兴趣,完全可以自己试着用WebWPF实现以下视图,同时可以验证P Logic的可复用性和视图无缝替换,亲身体验一下MVP模式的威力。WinformView代码如下。

      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看效果了。

 

优点

1、模型与视图完全分离,我们可以修改视图而不影响模型

2、可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部

3、我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。

4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

 

缺点

由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。比如说,原本用来呈现HtmlPresenter现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。

posted on 2021-05-10 09:24  FreedomChandler  阅读(432)  评论(0)    收藏  举报