您的位置:首页 > 其它

MVP设计模式

2015-06-15 19:15 246 查看
维基百科

Model-view-presenter(MVP)使用者接口设计模式的一种,被广范用于便捷自动化单元测试和在呈现逻辑中改良分离关注点(separationofconcerns)。

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。

1MVC和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都没有完成时候,就可以通过编写MockObject(即实现了Model和View的接口,但没有具体的内容的)来测试Presenter的逻辑。在MVP里,应用程序的逻辑主要在Presenter来实现,其中的View是很薄的一层。因此就有人提出了PresenterFirst的设计模式,就是根据UserStory来首先设计和开发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了,那么视图很有可能也需要变更。

微软文档

设计模式:ModelViewPresenter

Jean-PaulBoodhoo

随着UI创建技术(如ASP.NET和Windows®Form)的功能越来越强大,让UI层执行更多功能已成为普遍的做法。由于没有清晰的职责划分,UI层经常成为逻辑层的全能代理,而后者实际上属于应用程序的其他层。ModelViewPresenter(MVP)模式是专门适用于解决此问题的一种设计模式。为了证明我的观点,我将遵循MVP模式为Northwind数据库中的客户创建一个显示屏。

为什么UI层中不应有过多逻辑?如果没有手动运行应用程序,或未能维护自动执行UI组件的高深UI运行程序脚本,则很难测试应用程序UI层中的代码。这本身就是一个麻烦事,而更大的麻烦是应用程序中普通视图间大量的重复代码。当在UI层的不同部分之间复制执行特定业务功能的逻辑时,通常很难发现好的重构候选者。MVP设计模式使得将逻辑和代码从UI层分离更为轻松,从而更易于简化测试可重用代码。

图1显示组成示例应用程序的主要层。请注意UI层和表示层使用不同的软件包。您可能期望它们使用相同的软件包,但实际上一个项目的UI层只应由两种UI元素组成—窗体和控件。在WebForms项目中,通常是ASP.NETWebForms、用户控件和服务器控件的集合。在WindowsForms中,是WindowsForms、用户控件和第三方程序库的集合。此附加层用于分离显示和逻辑。在表示层中可以有实际实现UI行为的对象,如验证显示、UI的集合输入等。

图1应用程序体系结构

遵循MVP

如图2所示,此项目的UI是非常标准的。加载页面时,屏幕将会显示一个填充了Northwind数据库中所有客户的下拉框。如果您从下拉列表中选择一个客户,将会更新页面,以显示该客户的信息。通过遵循MVP设计模式,您可将各种行为从UI层分离,将其置入自身的类中。图3显示一个类图表,表示涉及的不同类之间的关联。

图2客户信息

需要注意的很重要的一点是,表示器并不了解应用程序实际UI层的任何知识。它知道它可以与接口对话,但不知道也不关心接口的具体实现。这就促使了在不同UI技术间表示器的重用。

我将使用测试驱动开发(TDD)来创建客户屏幕功能。图4显示我将使用的第一个测试的详细信息,以说明我期望在页面加载上观察到的行为。TDD使我可以一次将精力集中于一个问题,只编写可使测试通过的足够代码,然后再继续进行。在此测试中,我将利用一个名为NMock2的模拟对象框架来构建接口的模拟实现。

图3MVP类图表

在我的MVP实现中,我决定将表示器作为其将要配合工作的视图的附属。在能使对象立即工作的状态下创建对象总是很好的。在此应用程序中,表示层实际上是依靠服务层来调用域功能的。由于此需求,因此也有必要建立一个带接口的表示器,通过该接口它可以与服务类进行对话。这将确保一旦建立表示器后,它就可以进行所有需要它来完成的工作。我将通过创建两个特定的模拟开始:一个用于服务层,一个用于表示器将要使用的视图。

为什么要创建模拟?单元测试的规则是尽可能的隔离测试,以将精力集中于一个特定的对象。在此测试中,我只关注表示器的预期行为。此时,我并不在意视图接口或服务接口的实际实现,我相信那些接口定义的协议,并相应的设置模拟来表现。这可确保我将测试集中于我所期望的表示器行为,无需考虑其所依赖的对象。调用其初始化方法后,我所期望的表示器行为如下。

首先,表示器应调用ICustomerTask服务层对象上的GetCustomerList方法(在测试中模拟)。请注意您可以使用NMock模仿模拟的行为。而对于服务层,我希望它可将模拟ILookupCollection返回到表示器。然后,在表示器从服务层检索ILookupCollection后,它应调用集合的BindTo方法并将方法传递到ILookupList的实现。通过使用NMockExpect.Once方法,我可以确定如果表示器没有调用该方法一次(且仅一次),则测试将失败。

编写该测试后,我将会处于完全非编辑状态。我将尽可能做最简单的工作来使测试通过。

使第一次测试通过

首先编写测试的好处之一是我现在拥有了一个远景蓝图,可以遵循它来对测试进行编译并最终通过。第一次测试包括两个还不存在的接口。这些接口是正确编译代码的先决条件。我将从IViewCustomerView的代码开始:

publicinterfaceIViewCustomerView

{

ILookupListCustomerList{get;}

}

此接口提供一个属性,该属性可返回一个ILookupList接口实现。对于该问题,我还没有一个ILookupList接口,甚至没有实施工具。为了通过此测试,我不需要明确的实施工具,这样我可以继续创建ILookupList接口:


publicinterfaceILookupList{}

此时,ILookupList接口看起来没什么用处。我的目标是编译并通过测试,而这些接口可以满足测试的需求。现在该将焦点转向我要实际测试的对象-ViewCustomerPresenter了。
此类尚不存在,但回头查看该测试,您可以从中得出两个重要事实:它有一个构造函数,该函数需要视图和服务实现作为依赖,并且有一个空的Initialize方法。图5中的代码显示如何编译测试。

请牢记表示器需要其所有依赖关系,以便富有成效的进行工作;这就是传入视图和服务的原因。我没有实现初始化方法,因此如果运行测试,我将得到NotImplementedException。

如上所述,我没有盲目的编写表示器代码;通过查看测试,我已了解在调用初始化方法后表示器应表现的行为。行为的实现代码如下:

publicvoidInitialize()

{

task.GetCustomerList().BindTo(view.CustomerList);

}

本文附带的源代码中有CustomerTask类(实现了ICustomerTask接口)中GetCustomerList方法的完整实现。虽然从实现和测试表示器的角度看,我还无需了解是否存在工作实现。但正是该抽象级别使我难以通过表示器类的测试。第一个测试现在正处于将要编译和运行的状态。这证明在调用表示器上的Initialize方法时,它将以我在测试中指定的方式与其依赖对象进行交互,并且最终当这些依赖对象的具体实现被插入表示器时,我可以确信结果视图(ASPX页)将被客户列表所填充。

填充DropDownList

到目前为止,我主要处理了接口,抛开实际的实现细节,将精力集中于表示器。现在,该建立一些探测代码了,它最终将允许表示器以一种可测试的方式在Web页面上填充列表。实现此功能的关键是将在LookupCollection类的BindTo方法中发生的交互。如果您看一下图6中LookupCollection类的实现,就会注意到它实现了ILookupCollection接口。本文的源代码带有随附测试,可用于建立LookupCollection类的功能。

BindTo方法的实现特别有趣。请注意在此方法中,集合将重复ILookupDTO实现本身的私有列表。ILookupDTO是一个接口,可很好地与UI层的组合框绑定:

publicinterfaceILookupDTO

{

stringValue{get;}

stringText{get;}

}

图7显示用于测试查找集合的BindTo方法的代码,此方法将会帮助解释LookupCollection与ILookupList之间的预期交互。最后一点特别有趣。在此测试中,我希望在尝试向列表添加项目前,LookupCollection将会调用ILookupList实现中的Clear方法。然后,我希望可以在ILookupList上调用Add10次,而作为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库的引用,这样它就可以实例化DropDownListWeb控件。进一步查看测试,您应了解WebLookupList类将会实现ILookupList接口。它还会将ListControl作为一个依赖对象。System.Web.UI.WebControls命名空间中两个最常见的ListControl实现是DropDownList和ListBox类。图8中测试的主要功能是要确保WebLookupList正确的将实际WebListControl的状态更新为其正在委派责任的状态。图9显示WebLookupList实现中涉及的类的类图表。我可以通过图10中的代码,满足对WebLookupList控件第一次测试的要求。

图9WebLookupList类

请记住,MVP的一个关键是由创建视图接口引入的层的分离。表示器不了解视图的具体实现,以及它要对话的各个ILookupList,它只知道它可以调用这些接口定义的任何方法。最后,WebLookupList类是一个包装并委托至底层ListControl的类(在System.Web.UI.WebControls项目中定义的某些ListControls的基类)。利用这些代码,我可以编译并运行WebLookupList控件测试,现在测试应该顺利通过了。我可以为WebLookupList再添加一个测试,以测试Clear方法的实际行为:

[Test]

publicvoidShouldClearUnderlyingList()

{

ListControlwebList=newDropDownList();

ILookupListlist=newWebLookupList(webList);


webList.Items.Add(newListItem("1","1"));


list.Clear();


Assert.AreEqual(0,webList.Items.Count);

}

另外,我将测试在调用WebLookupList类自身的方法时,它是否会真正更改底层ListControl(DropDownList)的状态。WebLookupList现在可以完成填充WebForm中DropDownList的功能。现在可将所有程序绑定在一起,就可获得已填充客户列表的Web页面下拉列表。

实现视图接口

由于我在建立WebForm前端,因此IViewCustomerView接口的实现程序必须是WebForm或用户控件。出于此列的原因,我将其设为WebForm。页面的常规外观已经创建,如图2所示。现在我只需要实现视图接口。切换到ViewCustomers.aspx页的源代码,我可以添加以下代码,表示需要此页来实现IViewCustomersView接口:

publicpartialclassViewCustomers: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:DropDownListid="customerDropDownList"AutoPostBack="true"

runat="server"Width="308px"></asp:DropDownList></td>

</tr>

利用这些已有代码,我可以快速的继续实现满足IViewCustomerView接口实现所需的代码:

publicILookupListCustomerList

{

get{returnnewWebLookupList(this.customerDropDownList);}

}

我现在需要调用表示器上的Initialize方法,以触发该方法实际执行一些操作。因此,视图需要能够实例化表示器,这样就可以调用它的方法了。如果回头查看一下表示器,您会记得它需要视图和服务与之配合使用。ICustomerTask接口表示位于应用程序服务层的接口。服务层通常负责协调域对象之间的交互,并将这些交互的结果转换为“数据传输对象”(DataTransferObjects,DTO),然后将其从服务层传递到表示层,再到UI层。但是此处有一个问题:我已规定表示器需要与视图和服务实现一同构造。

表示器的实际实例化将在Web页的源代码中进行。这是一个问题,因为UI项目没有引用任何服务层项目。但是,表示项目却引用了服务层项目。通过将一个重载构造函数添加到ViewCustomerPresenterClass中,可以解决此问题:

publicViewCustomerPresenter(IViewCustomerViewview):

this(view,newCustomerTask()){}

这一新的构造函数同时满足了表示器视图和服务的实现要求,同时还可从服务层维护UI层的分离。现在完成源代码的后续代码就很简单了:

protectedoverridevoidOnInit(EventArgse)

{

base.OnInit(e);

presenter=newViewCustomerPresenter(this);

}


protectedvoidPage_Load(objectsender,EventArgse)

{

if(!IsPostBack)presenter.Initialize();

}

请注意,表示器实例化的关键是:我将利用新建的构造函数重载,并且WebForm会将其自身作为实现视图接口的对象传入。

利用实现的源代码中的代码,我可以立即创建并运行应用程序。现在不需要源代码中的任何数据绑定代码,就可以使用客户名称列表来填充Web页上的DropDownList。另外,已在最终一起工作的所有代码段上运行了测试分数,这可确保表示层体系结构将按预期运转。

现在我准备展示一下在DropDownList中显示选定客户信息所需的步骤,以此来总结我对MVP的讨论。再次重申,我将首先编写一个测试,来描述我所希望观察到的行为。(请参阅图11)。

如上所述,我将利用NMock程序库来创建任务和视图接口的模拟。此特定测试将通过向服务层请求表示特定客户的DTO来验证表示器的行为。表示器从服务层检索到DTO后,它将直接更新视图上的属性,这样视图就不必了解任何有关如何正确显示对象信息的知识。简便起见,我将不再讨论WebLookupList控件上SelectedItem属性的实现;相反,我会将它留给您去检查源代码,以了解实现的详细信息。此测试真正展示的是在表示器从服务层检索CustomerDTO后,表示器和视图之间发生的交互。如果现在尝试运行测试,我将面临一个严重的失败,因为视图接口上的许多属性都还不存在。因此,我将继续进行并为IViewCustomerView接口添加必要的成员,如图12所示。

这些接口成员添加完成之后,我的WebForm也许会抱怨,因为它不再满足接口协议了,所以我必须返回WebForm的源代码并实现其余的成员。如上所述,Web页的整个标记已经创建,同时表格单元格已被标记为"runat=server"属性,并且已根据其应显示的信息进行了命名。这样就可以使结果代码非常轻松的实现接口成员:

publicstringCompanyName

{

set{this.companyNameLabel.InnerText=value;}

}

publicstringContactName

{

set{this.contactNameLabel.InnerText=value;}

}

...

随着setter属性的实现,现在只剩下最后一件事要完成。我需要一种方法来告诉表示器显示选定客户的信息。回头看看测试,您会发现此行为的实现位于表示器的DisplayCustomerDetails方法中。但是,此方法不带有任何参数。调用时,表示器将返回视图,从中提取其所需的任何信息(使用ILookupList检索),然后使用该信息检索选定客户的详细信息。从UI角度看,我需要做的就是将DropDownList的AutoPostBack属性设置为true,我还需要将以下事件处理程序挂钩代码添加到页面的OnInit方法中:

protectedoverridevoidOnInit(EventArgse)

{

base.OnInit(e);

presenter=newViewCustomerPresenter(this);

this.customerDropDownList.SelectedIndexChanged+=delegate

{

presenter.DisplayCustomerDetails();

};

}

此事件处理程序可确保在下拉列表中选择新客户时,视图将请求表示器显示该客户的详细信息。

重要的是注意这是典型行为。当视图请求表示器执行操作时,它不会给予任何特定的详细信息,并且将由表示器来决定是否返回视图,并使用视图接口来获取其所需的任何信息。图13显示实现表示器中所需行为的代码。

希望您现在可以了解添加表示器层的价值了。表示器负责尝试检索需要显示其详细信息的客户ID。这就是通常在源代码中执行的代码,但是它现在位于类中,我可以在任何表示层技术以外对其进行完全的测试和实践。

如果表示器能够从视图中检索有效的客户ID,则它将转向服务层并请求表示该客户详细信息的DTO。表示器获得DTO后,它将使用DTO中包含的信息更新视图。要注意的关键一点是视图接口的简单性,除ILookupList接口以外,视图接口完全由字符串DataTypes组成。表示器的最终职责是正确地转换和格式化从DTO中检索的信息,这样它就可以作为字符串,实际被传递到视图。虽然未在此例中说明,但表示器还可负责从视图中读取信息,并将其转换为服务层所期待的必要类型。

完成所有代码段后,我现在就可以运行应用程序了。首次加载页面时,我会获得一个客户列表,并且在DropDownList中显示(未选中)第一个客户。如果我选择一个客户,则会出现回发,视图与表示器之间发生交互,并且会使用相关的客户信息更新Web页面。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: