维基百科
Model-view-presenter (MVP) 是使用者接口设计模式的一种,被广范用于便捷自动化单元测试和在呈现逻辑中改良分离关注点(separation of concerns)。
- Model 定义使用者接口所需要被显示的资料模型,一个模型包含着相关的商业逻辑。
- View 视图为呈现使用者接口的终端,用以表现来自 Model 的资料,和使用者命令路由再经过 Presenter 对事件处理后的资料。
- Presenter 包含着元件的事件处理,负责检索 Model 取得资料,和将取得的资料经过格式转换与 View 进行沟通。
MVP 设计模式通常会再加上 Controller 做为整体应用程序的后端程序工作。
百度百科
mvp 的全称为Model-View-Presenter,Model提供数据,View负责显示,Controller/Presenter负责逻辑的处理。 MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。
1 MVC和MVP
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负 责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。
在MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些 业务逻辑。 在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,及View。所以,在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。[1]
2解决MVC问题
在 MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的 View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,即重用! 不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试--而不需要使用自动化的测试工具。 我们甚至可以在Model和View都没有完成时候,就可以通过编写Mock Object(即实现了Model和View的接口,但没有具体的内容的)来测试Presenter的逻辑。 在MVP里,应用程序的逻辑主要在Presenter来实现,其中的View是很薄的一层。因此就有人提出了Presenter First的设计模式,就是根据User Story来首先设计和开发Presenter。在这个过程中,View是很简单的,能够把信息显示清楚就可以了。在后面,根据需要再随便更改View, 而对Presenter没有任何的影响了。 如果要实现的UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在View和Presenter之间放置一个Adapter。由这个 Adapter来访问Model和View,避免两者之间的关联。而同时,因为Adapter实现了View的接口,从而可以保证与Presenter之 间接口的不变。这样就可以保证View和Presenter之间接口的简洁,又不失去UI的灵活性。 在MVP模式里,View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容,绝不容许直接访问 Model--这就是与MVC很大的不同之处。[1]
3优点
1、模型与视图完全分离,我们可以修改视图而不影响模型
2、可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部
3、我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)[1]
4缺点
由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视 图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。比如说,原本用来呈现Html的 Presenter现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。
微软文档
设计模式:Model View Presenter
Jean-Paul Boodhoo
随 着 UI 创建技术(如 ASP.NET 和 Windows® Form)的功能越来越强大,让 UI 层执行更多功能已成为普遍的做法。由于没有清晰的职责划分,UI 层经常成为逻辑层的全能代理,而后者实际上属于应用程序的其他层。Model View Presenter (MVP) 模式是专门适用于解决此问题的一种设计模式。为了证明我的观点,我将遵循 MVP 模式为 Northwind 数据库中的客户创建一个显示屏。
为什么 UI 层中不应有过多逻辑?如果没有手动运行应用程序,或未能维护自动执行 UI 组件的高深 UI 运行程序脚本,则很难测试应用程序 UI 层中的代码。这本身就是一个麻烦事,而更大的麻烦是应用程序中普通视图间大量的重复代码。当在 UI 层的不同部分之间复制执行特定业务功能的逻辑时,通常很难发现好的重构候选者。MVP 设计模式使得将逻辑和代码从 UI 层分离更为轻松,从而更易于简化测试可重用代码。
图 1 显示组成示例应用程序的主要层。请注意 UI 层和表示层使用不同的软件包。您可能期望它们使用相同的软件包,但实际上一个项目的 UI 层只应由两种 UI 元素组成 — 窗体和控件。在 Web Forms 项目中,通常是 ASP.NET Web Forms、用户控件和服务器控件的集合。在 Windows Forms 中,是 Windows Forms、用户控件和第三方程序库的集合。此附加层用于分离显示和逻辑。在表示层中可以有实际实现 UI 行为的对象,如验证显示、UI 的集合输入等。
图 1 应用程序体系结构
遵循 MVP
如 图 2 所示,此项目的 UI 是非常标准的。加载页面时,屏幕将会显示一个填充了 Northwind 数据库中所有客户的下拉框。如果您从下拉列表中选择一个客户,将会更新页面,以显示该客户的信息。通过遵循 MVP 设计模式,您可将各种行为从 UI 层分离,将其置入自身的类中。图 3 显示一个类图表,表示涉及的不同类之间的关联。
图 2 客户信息
需要注意的很重要的一点是,表示器并不了解应用程序实际 UI 层的任何知识。它知道它可以与接口对话,但不知道也不关心接口的具体实现。这就促使了在不同 UI 技术间表示器的重用。
我将使用测试驱动开发 (TDD) 来创建客户屏幕功能。图 4 显示我将使用的第一个测试的详细信息,以说明我期望在页面加载上观察到的行为。TDD 使我可以一次将精力集中于一个问题,只编写可使测试通过的足够代码,然后再继续进行。在此测试中,我将利用一个名为 NMock2 的模拟对象框架来构建接口的模拟实现。
图 3 MVP 类图表
在 我的 MVP 实现中,我决定将表示器作为其将要配合工作的视图的附属。在能使对象立即工作的状态下创建对象总是很好的。在此应用程序中,表示层实际上是依靠服务层来调 用域功能的。由于此需求,因此也有必要建立一个带接口的表示器,通过该接口它可以与服务类进行对话。这将确保一旦建立表示器后,它就可以进行所有需要它来 完成的工作。我将通过创建两个特定的模拟开始:一个用于服务层,一个用于表示器将要使用的视图。
为什么要创建模拟?单元测试的规则是尽可能 的隔离测试,以将精力集中于一个特定的对象。在此测试中,我只关注表示器的预期行为。此时,我并不在意视图接口或服务接口的实际实现,我相信那些接口定义 的协议,并相应的设置模拟来表现。这可确保我将测试集中于我所期望的表示器行为,无需考虑其所依赖的对象。调用其初始化方法后,我所期望的表示器行为如 下。
首先,表示器应调用 ICustomerTask 服务层对象上的 GetCustomerList 方法(在测试中模拟)。请注意您可以使用 NMock 模仿模拟的行为。而对于服务层,我希望它可将模拟 ILookupCollection 返回到表示器。然后,在表示器从服务层检索 ILookupCollection 后,它应调用集合的 BindTo 方法并将方法传递到 ILookupList 的实现。通过使用 NMockExpect.Once 方法,我可以确定如果表示器没有调用该方法一次(且仅一次),则测试将失败。
编写该测试后,我将会处于完全非编辑状态。我将尽可能做最简单的工作来使测试通过。
使第一次测试通过
首先编写测试的好处之一是我现在拥有了一个远景蓝图,可以遵循它来对测试进行编译并最终通过。第一次测试包括两个还不存在的接口。这些接口是正确编译代码的先决条件。我将从 IViewCustomerView 的代码开始:
public interface IViewCustomerView
{
ILookupList CustomerList { get; }
}
此接口提供一个属性,该属性可返回一个 ILookupList 接口实现。对于该问题,我还没有一个 ILookupList 接口,甚至没有实施工具。为了通过此测试,我不需要明确的实施工具,这样我可以继续创建 ILookupList 接口:?
public interface ILookupList { }
此时,ILookupList 接口看起来没什么用处。我的目标是编译并通过测试,而这些接口可以满足测试的需求。现在该将焦点转向我要实际测试的对象 - ViewCustomerPresenter 了。?此类尚不存在,但回头查看该测试,您可以从中得出两个重要事实:它有一个构造函数,该函数需要视图和服务实现作为依赖,并且有一个空的 Initialize 方法。图 5 中的代码显示如何编译测试。
请牢记表示器需要其所有依赖关系,以便富有成效的进行工作;这就是传入视图和服务的原因。我没有实现初始化方法,因此如果运行测试,我将得到 NotImplementedException。
如上所述,我没有盲目的编写表示器代码;通过查看测试,我已了解在调用初始化方法后表示器应表现的行为。行为的实现代码如下:
public void Initialize()
{
task.GetCustomerList().BindTo(view.CustomerList);
}
本文附带的源代码中有 CustomerTask 类(实现了 ICustomerTask 接口)中 GetCustomerList 方法的完整实现。虽然从实现和测试表示器的角度看,我还无需了解是否存在工作实现。但正是该抽象级别使我难以通过表示器类的测试。第一个测试现在正处于将 要编译和运行的状态。这证明在调用表示器上的 Initialize 方法时,它将以我在测试中指定的方式与其依赖对象进行交互,并且最终当这些依赖对象的具体实现被插入表示器时,我可以确信结果视图(ASPX 页)将被客户列表所填充。
填充 DropDownList
到 目前为止,我主要处理了接口,抛开实际的实现细节,将精力集中于表示器。现在,该建立一些探测代码了,它最终将允许表示器以一种可测试的方式在 Web 页面上填充列表。实现此功能的关键是将在 LookupCollection 类的 BindTo 方法中发生的交互。如果您看一下图 6 中 LookupCollection 类的实现,就会注意到它实现了 ILookupCollection 接口。本文的源代码带有随附测试,可用于建立 LookupCollection 类的功能。
BindTo 方法的实现特别有趣。请注意在此方法中,集合将重复 ILookupDTO 实现本身的私有列表。ILookupDTO 是一个接口,可很好地与 UI 层的组合框绑定:
public interface ILookupDTO
{
string Value { get; }
string Text { get; }
}
图 7 显示用于测试查找集合的 BindTo 方法的代码,此方法将会帮助解释 LookupCollection 与 ILookupList 之间的预期交互。最后一点特别有趣。在此测试中,我希望在尝试向列表添加项目前,LookupCollection 将会调用 ILookupList 实现中的 Clear 方法。然后,我希望可以在 ILookupList 上调用 Add 10 次,而作为 Add 方法的参数,LookupCollection 将在实现 ILookupDTO 接口的对象中传递。若要使其与 Web 项目中的控件(例如下拉列表框)配合使用,则您需要创建一个 ILookupList 实现,该实现知道如何与 Web 项目中的控件配合使用。
本 文附带的源代码包含一个名为 MVP.Web.Controls 的项目。该项目包含我选择用于创建完整解决方案的所有 Web 特定控件或类。为什么我将代码放在此项目中,而不是放在 APP_CODE 目录或 Web 项目中?回答是可测试性。在没有手动运行应用程序或没有使用某种测试程序自动执行 UI 测试的情况下,很难直接测试 Web 项目中的任何控件。MVP 模式使我可在不必手动运行应用程序的情况下考虑更高的抽象级别,并测试核心接口(ILookupList 和 ILookupCollection)的实现。我打算向 Web.Controls 项目中添加一个新类:WebLookupList 控件。图 8 显示此类的第一次测试。
某些事项在图 8 所示的测试中比较突出。显然,测试项目需要一个到 System.Web 库的引用,这样它就可以实例化 DropDownList Web 控件。进一步查看测试,您应了解 WebLookupList 类将会实现 ILookupList 接口。它还会将 ListControl 作为一个依赖对象。System.Web.UI.WebControls 命名空间中两个最常见的 ListControl 实现是 DropDownList 和 ListBox 类。图 8 中测试的主要功能是要确保 WebLookupList 正确的将实际 Web ListControl 的状态更新为其正在委派责任的状态。图 9 显示 WebLookupList 实现中涉及的类的类图表。我可以通过图 10 中的代码,满足对 WebLookupList 控件第一次测试的要求。
图 9 WebLookupList 类
请 记住,MVP 的一个关键是由创建视图接口引入的层的分离。表示器不了解视图的具体实现,以及它要对话的各个 ILookupList,它只知道它可以调用这些接口定义的任何方法。最后,WebLookupList 类是一个包装并委托至底层 ListControl 的类(在 System.Web.UI.WebControls 项目中定义的某些 ListControls 的基类)。利用这些代码,我可以编译并运行 WebLookupList 控件测试,现在测试应该顺利通过了。我可以为 WebLookupList 再添加一个测试,以测试 Clear 方法的实际行为:
[Test]
public void ShouldClearUnderlyingList()
{
ListControl webList = new DropDownList();
ILookupList list = new WebLookupList(webList);
webList.Items.Add(new ListItem("1", "1"));
list.Clear();
Assert.AreEqual(0, webList.Items.Count);
}
另外,我将测试在调用 WebLookupList 类自身的方法时,它是否会真正更改底层 ListControl (DropDownList) 的状态。WebLookupList 现在可以完成填充 Web Form 中 DropDownList 的功能。现在可将所有程序绑定在一起,就可获得已填充客户列表的 Web 页面下拉列表。
实现视图接口
由 于我在建立 Web Form 前端,因此 IViewCustomerView 接口的实现程序必须是 Web Form 或用户控件。出于此列的原因,我将其设为 Web Form。页面的常规外观已经创建,如图 2 所示。现在我只需要实现视图接口。切换到 ViewCustomers.aspx 页的源代码,我可以添加以下代码,表示需要此页来实现 IViewCustomersView 接口:
public partial class ViewCustomers :Page,IViewCustomerView
如果观察示例代码,您将会发现 Web 项目和 Presentation 是两个完全不同的程序集。而且,Presentation 项目没有引用任何 Web.UI 项目,这样可进一步维护分离层。另一方面,Web.UI 项目必须引用 Presentation 项目,因为视图接口和表示器都位于该项目中。
通过选择实现 IViewCustomerView 接口,现在我们的 Web 页面可以实现由该接口定义的任何方法或属性。当前 IViewCustomerView 接口上只有一个属性,是一个可返回 ILookupList 接口任何实现的 getter。我已向 Web.Controls 项目中添加了引用,这样就可以实例化 WebLookupListControl。我这样做是因为 WebLookupListControl 实现了 ILookupList 接口,并且它知道如何委托给 ASP.NET 中的实际 WebControls。请查看 ViewCustomer 页面的 ASPX,您将会发现客户列表只是一个 asp:DropDownList 控件:
<td>Customers:</td>
<td><asp:DropDownList id="customerDropDownList" AutoPostBack="true"
runat="server" Width="308px"></asp:DropDownList></td>
</tr>
利用这些已有代码,我可以快速的继续实现满足 IViewCustomerView 接口实现所需的代码:
public ILookupList CustomerList
{
get { return new WebLookupList(this.customerDropDownList);}
}
我现在需要调用表示器上的 Initialize 方法,以触发该方法实际执行一些操作。因此,视图需要能够实例化表示器,这样就可以调用它的方法了。如果回头查看一下表示器,您会记得它需要视图和服务与 之配合使用。ICustomerTask 接口表示位于应用程序服务层的接口。服务层通常负责协调域对象之间的交互,并将这些交互的结果转换为“数据传输对象”(Data Transfer Objects, DTO),然后将其从服务层传递到表示层,再到 UI 层。但是此处有一个问题:我已规定表示器需要与视图和服务实现一同构造。
表示器的实际实例化将在 Web 页的源代码中进行。这是一个问题,因为 UI 项目没有引用任何服务层项目。但是,表示项目却引用了服务层项目。通过将一个重载构造函数添加到 ViewCustomerPresenterClass 中,可以解决此问题:
public ViewCustomerPresenter(IViewCustomerView view) :
this(view, new CustomerTask()) {}
这一新的构造函数同时满足了表示器视图和服务的实现要求,同时还可从服务层维护 UI 层的分离。现在完成源代码的后续代码就很简单了:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
presenter = new ViewCustomerPresenter(this);
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) presenter.Initialize();
}
请注意,表示器实例化的关键是:我将利用新建的构造函数重载,并且 Web Form 会将其自身作为实现视图接口的对象传入。
利用实现的源代码中的代码,我可以立即创建并运行应用程序。现在不需要源代码中的任何数据绑定代码,就可以使用客户名称列表来填充 Web 页上的 DropDownList。另外,已在最终一起工作的所有代码段上运行了测试分数,这可确保表示层体系结构将按预期运转。
现在我准备展示一下在 DropDownList 中显示选定客户信息所需的步骤,以此来总结我对 MVP 的讨论。再次重申,我将首先编写一个测试,来描述我所希望观察到的行为。(请参阅图 11)。
如 上所述,我将利用 NMock 程序库来创建任务和视图接口的模拟。此特定测试将通过向服务层请求表示特定客户的 DTO 来验证表示器的行为。表示器从服务层检索到 DTO 后,它将直接更新视图上的属性,这样视图就不必了解任何有关如何正确显示对象信息的知识。简便起见,我将不再讨论 WebLookupList 控件上 SelectedItem 属性的实现;相反,我会将它留给您去检查源代码,以了解实现的详细信息。此测试真正展示的是在表示器从服务层检索 CustomerDTO 后,表示器和视图之间发生的交互。如果现在尝试运行测试,我将面临一个严重的失败,因为视图接口上的许多属性都还不存在。因此,我将继续进行并为 IViewCustomerView 接口添加必要的成员,如图 12 所示。
这 些接口成员添加完成之后,我的 Web Form 也许会抱怨,因为它不再满足接口协议了,所以我必须返回 Web Form 的源代码并实现其余的成员。如上所述,Web 页的整个标记已经创建,同时表格单元格已被标记为 "runat=server" 属性,并且已根据其应显示的信息进行了命名。这样就可以使结果代码非常轻松的实现接口成员:
public string CompanyName
{
set { this.companyNameLabel.InnerText = value; }
}
public string ContactName
{
set { this.contactNameLabel.InnerText = value; }
}
...
随着 setter 属性的实现,现在只剩下最后一件事要完成。我需要一种方法来告诉表示器显示选定客户的信息。回头看看测试,您会发现此行为的实现位于表示器的 DisplayCustomerDetails 方法中。但是,此方法不带有任何参数。调用时,表示器将返回视图,从中提取其所需的任何信息(使用 ILookupList 检索),然后使用该信息检索选定客户的详细信息。从 UI 角度看,我需要做的就是将 DropDownList 的 AutoPostBack 属性设置为 true,我还需要将以下事件处理程序挂钩代码添加到页面的 OnInit 方法中:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
presenter = new ViewCustomerPresenter(this);
this.customerDropDownList.SelectedIndexChanged += delegate
{
presenter.DisplayCustomerDetails();
};
}
此事件处理程序可确保在下拉列表中选择新客户时,视图将请求表示器显示该客户的详细信息。
重要的是注意这是典型行为。当视图请求表示器执行操作时,它不会给予任何特定的详细信息,并且将由表示器来决定是否返回视图,并使用视图接口来获取其所需的任何信息。图 13 显示实现表示器中所需行为的代码。
希望您现在可以了解添加表示器层的价值了。表示器负责尝试检索需要显示其详细信息的客户 ID。这就是通常在源代码中执行的代码,但是它现在位于类中,我可以在任何表示层技术以外对其进行完全的测试和实践。
如 果表示器能够从视图中检索有效的客户 ID,则它将转向服务层并请求表示该客户详细信息的 DTO。表示器获得 DTO 后,它将使用 DTO 中包含的信息更新视图。要注意的关键一点是视图接口的简单性,除 ILookupList 接口以外,视图接口完全由字符串 DataTypes 组成。表示器的最终职责是正确地转换和格式化从 DTO 中检索的信息,这样它就可以作为字符串,实际被传递到视图。虽然未在此例中说明,但表示器还可负责从视图中读取信息,并将其转换为服务层所期待的必要类 型。
完成所有代码段后,我现在就可以运行应用程序了。首次加载页面时,我会获得一个客户列表,并且在 DropDownList 中显示(未选中)第一个客户。如果我选择一个客户,则会出现回发,视图与表示器之间发生交互,并且会使用相关的客户信息更新 Web 页面。