显示标签为“MVP”的博文。显示所有博文
显示标签为“MVP”的博文。显示所有博文

2007年4月4日星期三

MVP: modify data only one time

Martin Fowler提到MVP的两种实现方式,Supervising Controller 和Passive View,它们的主要区别在于View和Model之间的关系,如果View对Model有一定的了解并且可以直接操纵Model,那么就是Supervising Contrller的形式;如果View对Model一无所知,就是Passive View的形式,“MVP vs MVC”中提到的其实是后面这种。

视图的Getter/Setter主要说的是当视图操作DomainModel(或者与DomainModel对应的DTO-Data Transfer Object)的读取操作,如果使用的Passive View的方式就不用考虑这个问题了,因为这时候的View不存在对Model的操作。

Getter/setter指的什么


为什么在使用Supervising Controller的时候需要考虑?回答这个问题之前需要先说明这里说的Getter/setter分别是什么。比如我们定义一个MyView:

public class MyView{
……
public MyEntity{
get{
return myEntity;
}
set{
myEntity=value;
}
……
}

Getter是指由View提供的对某个实体(Domain Model 或 DTO)对象的读操作,presenter通过getter从view中读取实体的数据然后进行保存;Setter当然就是对应的写操作了,presenter将从model从取出的实体数据传给view就通过setter访问器。所以的这里说的getter和setter都是站在从view的外部看view的这个角度,比如说presenter对view的操作就是这种情况。

存在什么问题?


回到前面的问题,如果一个view同时有getter和setter访问器,presenter把entity通过setter传入view之后,view利用UI的数据对entity进行“更新”。注意这里的更新有两种做法,一种是由view将修改过的entity传给presenter,由后者进行更新,一种是view直接调用model中相应的方法进行更新,不要忘记了Supervising Contrller中view对model有一定的了解,它可以操作model。当采用第二种方式更新的时候view对model的了解太多,会导致view和数据访问层的联系过于紧密。初此之外,还有一个问题同时存在于这两种更新方式之中,就是对entity的修改可能既存在于view也存在于presenter,散布在多处的修改很容易导致不一致性,而且难以理解和维护。

DTO的好坏


使用DTO可以解决上面的问题。DTO是用于在层与层之间传递数据的一种中间数据形式,它的数据来自domain object,它的使用使得view不再依赖于具体的domain model,因为这时候的view只需要知道与presenter之间通讯的数据形式即可。使用DTO之后,view端得到和修改的都是DTO,presenter从view获得DTO之后进行一次DTO-Domain Object的转换,最后将转换得到的domain object存入数据库。这样一来view和presenter的分工明确,而且view对dto的修改不会影响到最终数据,因为真正的数据修改实际上只在presenter内部发生,dto只是做了临时存储之用。

使用dto也有它的坏处,如果系统很大,domain object很多,那就意味着DTO需要很多,对domain 的修改都要相应地在dto上实施,使得维护工作量变大。

用Update Method 替代Getter


这是Billy McCafferty在他的blog中提出来的方法。他提出保留view的setter访问器而“去掉getter访问器,其而代之的是由view提供一个Update方法”。这样view既可以从presenter处得到domain model的数据,又可以利用自己的Update方法更新来修改这些数据以反应用户的输入。注意这里的“Update”并不是写回了数据库,而只是是修改了Domain object的数据,还存在于内存中。以保存操作为例,基本步骤如下(Presenter:表示都是发生在presenter内部):

Presenter: view.Update(myEntity)
Presenter: presenter.Validate(myEntity)
Presenter: customerDao.SaveOrUpdate(myEntity)
可以看出对数据的修改发生在view,而presenter内部只是进行逻辑控制和数据验证。

总结


实际上DTO方式和Update Method 方式的目的都是为了控制对数据的修改,严格限制数据的修改只发生在一个地方不至于散布在各处,让view和presenter分工明确,理解和维护起来都容易。

我比较喜欢Passive View方式,因为View和Model完全分开,但是有时候Supervising的方式也很不错,比如在winform中要使用数据绑定,在asp.net中要使用Object Data Source,用后者就很方便。总之没有绝对的好坏,视情况而定。

2007年3月16日星期五

MVP实践之:单元测试

MVP模式的应用中很重要一点是“方便了测试”。MVP中的Model层不与界面发生任何关系,按照一般的测试方法进行测试即可。但是Presenter需要view,至少需要一个View的契约,也就是它多少需要对view有一些了解,那么到底如何在没有界面的情况下测试呢?这里借用上一篇中讲到的winform的例子,以测试SearchPictOfComp为目标来实践一下。这里假设Model层的代码都经过了测试。

我们编写一个测试用例,如下:
[Test]
public void SearchArtifactOfCompTest()
{
MockViewListDescPict view;
view=测试用的View;
view.ComponentId = "265f1e5d-2c75-4368-9a3a-5da5d82270c5";
ListDescPictPresenter presenter = new ListDescPictPresenter(view);
view.AttachPresenter(presenter);
view执行click button的动作;
Assert.IsTrue(((IList)view.descPictList).Count == 6);
}
先解释一下这个测试用例的意思。首先创建一个测试用的view,然后用它初始化presenter,当view执行一个点击button的动作之后,presenter会去响应这个操作,将取出的数据传给view的列表控件显示,如果成功则列表控件的数据源中应该有6条纪录。
现在运行这个测试用例肯定是失败的,因为标记为红色的代码都是没有实现的伪代码。为了让测试通过,我们尝试实现红色标记处所要求的功能。
  • view=测试用的View
    这里实际上是要求一个满足契约IViewListDescPict的view,我们虽然没有真正的view,但是我们可以编写一个模拟的view(mock view)——可以联想在模块化测试时编写桩(stub)模块的情形。MockViewListDescPict是一个模拟的view,它实现了IViewListDescPict接口。那么一个问题就解决了:
    view=new MockViewListDescPict();
  • view执行click button的动作
    上一篇中讲到presenter是采用观察者模式,它监听button的事件。既然只要事件,那我们只要模拟一个事件就行了,并不需要真的用一个button。搞清楚button的click是产生一个普通的event,那我们定义一个:
    public EventHandler searchArtsOfComp_Event;
    事件的触发很简单:searchArtsOfComp_Event.Invoke(null, null);
    我们把触发的代码放在一个方法中,以便在测试的时候模拟:
    public void MockClickSearchButton()
    {
    searchArtsOfComp_Event.Invoke(null, null);
    }
    接下来我们要让presenter注册对该事件的监听,以便在invoke触发事件的时候能被presenter捕捉到,我们把这一步放在AttachPresener中进行:
    public void AttachPresenter(ListDescPictPresenter presenter)
    {
    this.presenter = presenter;
    searchArtsOfComp_Event = presenter.SearchPictOfComp_EventHandler;
    }
    其中presenter.SearchPictOfComp_EventHandler是一个指向方法SearchPictOfComp(见应用篇)的委托。
    到此,第二个问题也解决了:
    view.MockClickSearchButton();
现在运行一下测试用例,pass了自然高兴,可是“(IList)view.descPictList”是怎么回事?其实这是为了测试读取的数据是不是正确地传给了view的列表控件,在解释测试用例的意思已经说过了。但是我们现在使用的view都是模拟(Mock)的,button事件也是假的,那么列表也同样可是假的了。我们并不关心列表控件是不是正确的显示了数据,那是控件自己的事情,我们只关心是不是传递了正确的数据,因此这里可以模拟一个接受数据的行为。控件的datasource是object的,那我们就在mock view的内部定义一个objcect的对象来获得对传递数据的引用:
public object descPictList;
public void SetPictListDataSource(IList pictList)
{
descPictList = pictList;
}
当我们模拟了button的click事件之后,presenter根据它的内部逻辑会去调用view的
SetPictListDataSource方法,将数据传递给我们设定的接收对象descPictList,现在我们只需要检查descPictList所获得的数据是否正确就行了。如果你愿意的话可以逐条检查descPictList中的项是否正确,但其实我们只需简单地检查项的个数就可以了,因为在presenter内部未对数据做修改,只要Model层能保证取出的数据无误,这里就不会有错。

总结一下对presenter的测试,一共有三点:
1、不需要真的view,可以使用类似Mock Object这样的技术模拟一个view,只要模拟的对象满足IView的契约即可。
2、如果要从view中获得用户的数据,比如要从textbox中取得用户输入的查询条件,又或者需要将处理的结果数据发回view显示,比如将一个list的集合发送到ListBox显示,这两类问题我们都可以使用对字段或者属性的存取操作来模拟。因为我们不关心显示,只要数据正确传送即可,将显示的任务交给view,可是使得presenter的功能独立于显示层。
3、对于像用户点击button或者选择chechbox这样的交互行为,我们不需要真的用控件来测试。.net允许我们手动触发事件,比如EventHandler.Invoke(),因此我们可以用手动触发事件的方式来模拟用户的交互行为。

2007年3月15日星期四

MVP实践之:Winform应用

上面的界面显示了程序最终运行的状态,主要完成两个功能:
  1. 输入一个构件的ID号,根据Id号到ftp服务器上查找该构件所有相关的图片,并且将这些图片的名称显示在一个ListBox中。
  2. 改变选中的图片(列表中选图片名)时将图片显示在右边的PictureBox中。
上面说的那两个功能,基本上可以说是这个程序的需求了,根据要求先画出界面来。注意这里画界面仅仅只是为了明确所需要的功能,既然是MVP模式的实践,我们就必须摆脱双击控件然后直接编写代码的习惯。将MVP的三要素从这个程序剥离出来,分析清楚它们各自的角色和职责,以及如何进行交互才是最首要的工作。
三要素:
View:是一个Form,包含了一些控件
Model:这里的功能就是访问ftp取得图片信息
Presenter:从界面获得构件id,根据id从model取得图片名称列表,然后传给界面显示;从界面获得图片名,根据图片名从model取得图片,将图片传给界面显示。

右边的图是实现该程序的类图,有两点需要先说明一下:
  1. Model在哪里?
    前面的文章“MVP vs MVC”中讲到过,MVP中的Model并非DomainModel,其中甚至包含了业务逻辑,就如在本程序中充当Model角色的是一个Service类,它负责从数据源取得数据并向外提供,需要的时候会先对数据做一定的处理。这里的数据源是一个ftp服务器。
  2. IView***是干什么的?
    使用MVP的一个很重要的原因是为了方便测试,将界面与数据和控制逻辑分离之后,可以在不依赖于具体的界面的情况下对View以下的逻辑层(V、P层)的代码进行测试。但是如果没有了具体的View,presenter与View的通信如何完成呢?引入“契约”可以解决这个问题。契约的基本思想是:一个对象它保证为它的使用者(调用它的对象)提供它所承诺的功能,而承诺的功能就是契约中所规定的功能。比如有这样一个规定:
    string SelectedPictName { get;}
    意思是所有的使用者都能够得到一个PictName,而所有遵守这个契约的对象都保证提供一个PictName。
    那么,本例中的IViewListDescPict就是这样一个契约,它规定了所有实现了这一接口的View都必须保证提供接口中所规定的功能(类实现接口,就必须实现接口中所有的功能)。这样presenter就不需要一个具体的View了,它只需要一个view的契约,通过调用契约中的功能来帮助自己完成任务。
以根据id查找构件所有的图片为例,看一下是如何工作的:

步骤1-4都是一些初始化工作,窗体作为视图负责初始化自己,然后创建一个它需要使用的presenter并且将自己与presenter邦定(实际上就是获得一个对presenter的引用)。
private void FormListDescPict_Load(object sender, EventArgs e)
{
presenter =new ListDescPictPresenter(this);
AttachPresenter(presenter);//this.presenter=presenter;
}
步骤5中view接受一个用户响应,但是它自己并不做处理而是在步骤6中将处理委托给presenter。
//register the presenter as a observer
btnSearch.Click +=presenter.SearchPictOfComp_EventHandler;
步骤6-11是一套控制逻辑,全部在presenter内部处理。presenter监视view中的按钮(观察者模式),当检测到用户点击事件之后,先从view取得componentid,从service取得数据,然后刷新view的数据显示。
private void SearchPictOfComp(Object sender,EventArgs e)
{
try
{
IList descPicts = pictService.GetPictsOfComp(view.ComponentId);
view.SetPictListDataSource(descPicts);
}
catch (Exception ex)
{
//View will show the error msg to the end user
view.Message = ex.Message;
}
}
我们可以看到view也就是这里的窗体form仅仅只是将用户的输入向下层传递,它甚至不需要知道自己什么时候该刷新,只是定义了刷新的方法,真正的刷新命令由presenter发出。view中没有逻辑处理,这样可以使得view尽量的简化。复杂的控制逻辑都在presenter层处理,因此这些控制也就变得可以复用了,意味着我们可以将这一套逻辑处理用于多个view,理想的情况下可以同时支持winform的和asp.net的程序。

最后要提一下异常处理问题。由于view没有处理逻辑,使得我们在view中没有捕获异常的机会,因此只能在presenter层捕捉了。那么如何将捕获的异常消息通知view以显示给用户呢?本例中是在IView这个契约里定义了一个Message属性,presenter将捕获的异常消息写入这个属性中,只要实现IView接口的view都可以获取这个消息,并进行处理。
public string Message
{
get
{
return msg;
}
set
{
if (!string.IsNullOrEmpty(value))
{
msg = value;
//you can use the msg in the way you like
MessageBox.Show(msg);
}
}
}
上面的方法虽然是对于错误处理提出来的,但是同样可以用于其他的用途,比如我希望返回一个操作成功的提示消息。

2007年3月12日星期一

A MVP Framework for .net

Today, I find a project about MVP pattern on CodePlex.com. That project is named NMVP which means a Model-View-Presenter Framework for .Net. Here is the description from the NMVP Home:
"NMVP is an acronym for .Net Model-View-Presenter Framework.
The NMVP Framework is a framework for helping in building Model-View-Presenter (MVP) based UI architectures.

Even though it´s possible to create very simple MVP Architectures using NMVP, it really shines when used to build Composite MVP Architectures. If you don´t know MVP yet, please check out our Links page. There are several good articles about it there."
I think it's a very interesting and useful framework.Click here to know more about it .

2007年3月8日星期四

MVP vs MVC

这两天一直在看MVP相关的东西,什么是MVP,怎么实现MVP可以参考下面两篇文章:
http://msdn.microsoft.com/msdnmag/issues/06/08/DesignPatterns/default.aspx
(中文版:http://www.microsoft.com/china/msdn/library/architecture/architecture/architecturetopic/MVP.mspx?mfr=true)
http://www.codeproject.com/useritems/ModelViewPresenter.asp
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。Alex在他的blog中对于这两者之间的比较很直观也比较清楚,原文可以下面的地址找到:
http://ameleta.spaces.live.com/blog/cns!5F6316345A821420!163.entry


【译文】:

Model View Presenter vs Model View Controller

简介

在我工作中经常需要处理一些由于开发人员没能很清楚地理解MVC和MVP模式的区别的情况下使用它们而产生的问题。在这篇文章中我将会阐述一下我对两者之间区别的一些理解。
在N层体系结构中MVC/P模式仅仅只是用于表示层(presentation layer),理解这一点很重要。这两个模式并不是关于怎么构建数据层(data layer)和服务层(service layer)的,而是关于怎么将数据(data)从用户接口(view)中分离出来,以及用户接口如何与数据进行交互的。这些模式的使用让解除你的程序中表示层对对数据和控制逻辑的依赖,从而可以自由的变更表示层。

这两种模式中三个部分的一般理解

1、模型(Model)表示数据模型和业务逻辑(business logic)。模型并不总是DataSet,DataTable之类的东西,它代表着一类组件(components)或类(class),这些组件或类可以向外部提供数据,同时也能从外部获取数据并将这些数据存储在某个地方。简单的理解,可以把模型想象成“外观类(facade class)”。【译注:这里的外观是指“外观模式”中所说的外观。外观的一般作用是为一个复杂的子系统提供高层次的简单易用的访问接口,可以参看下面的图来理解它的原理:

】
2、视图(View)将数据层现给用户。一般的视图都只是包含用户界面(UI),而不包含界面逻辑。比如,Asp.net中包含控件的页面(page)就是一个视图。视图可以从模型中读取数据,但是不能修改或更新模型。
3、层现器(Presenter)/控制器(Controller)包含了根据用户在视图中的行为去更新模型的逻辑。视图仅仅只是将用户的行为告知控制器,而控制器负责从视图中取得数据然后发送给模型。

MVC/P模式的核心是为了将模型从视图/控制器中分离出来,从而使得模型独立于它们,因此模型不包含对视图和控制的引用。

什么是MVC(Model View Presenter)模式?

1、为了使得视图接口可以与模型和控制器进行交互,控制器执行一些初始化事件
2、用户通过视图(用户接口)执行一些操作
3、控制器处理用户行为(可以用观察着模式实现)并通知模型进行更新
4、模型引发一些事件,以便将改变发告知视图
5、视图处理模型变更的事件,然后显示新的模型数据
6、用户接口等待用户的进一步操作

这一模式的有一下几个要点:
1、视图并不使用控制器去更新模型。控制器负责处理从视图发送过来的用户操作并通过与模型的交互进行数据的更新
2、控制器可以和视图融合在一块。Visual Studion中对Windows Forms的默认处理方式就是这样的。【译注:比如我们双击一个Button,然后在它的事件里写处理逻辑,然后将处理的数据写回模型中。这里处理逻辑时间应该是控制器的功能,但是我们并没有专门写一个控制器来做这件事情而是接受了VS的默认处理方式,将它写在Form的代码中,而这里的Form在MVC中它就是一个View。所以这说vs默认的处理方式是将把控制器和视图融合在一起的。】
3、控制器不包含对视图的渲染逻辑(rendering logic)

“主动—MVC”模式,也是通常意义下的MVC模式

【译注:为什么说是主动的?View不是等Controller通知它Model更新了然后才从Model取数据并更新显示,而是自己监视Model的更新(如果用观察者模式)或主动询问Model是否更新。前面那种等待Controller通知的方式是下面所介绍的“被动—MVC”的实现方式。】

“被动—MVC”模式
与主动MVC的区别在于:
1、模型对视图和控制器一无所知,它仅仅是被它们使用
2、控制器使用视图,并通知它更新数据显示
3、视图仅仅是在控制器通知它去模型取数据的时候它才这么做(视图并不会订阅或监视模型的更新)
4、控制器负责处理模型数据的变化
5、控制器可以包含对视图的渲染逻辑

MVP模式

与“被动—MVC模式”很接近,区别在于“视图并不使用模型”。在MVP模式中视图和模型是完全分离的,他们通过Presenter进行交互。
Presenter与控制器非常相似,但是它们也有一些的区别:
1、Presenter处理视图发送过来的用户操作(在MVC中视图自己处理了这些操作)
2、它用更新过的数据去更新模型(在被动MVC中控制器只是通知视图去更新过的模型中去取新的数据,而主动MVC中模型通知视图去更新显示,控制器不需要做工作)
3、检查模型的更新(与被动MVC一样)
4、(与MVC的主要区别)从模型中取数据然后将它们发送到视图中
5、(与MVC的主要区别)将所做的更新告知视图
6、(与MVC的区别)用Presenter渲染视图

MVP的优势

1、模型与视图完全分离,我们可以修改视图而不影响模型
2、可以更高效地使用模型,因为所以的交互都发生在一个地方——Presenter内部
3、我们可以将一个Presener用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

MVP的问题

由于对视图的渲染放在了Presenter中,所以视图和Persenter的交互会过于频繁。

还有一点你需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。比如说,原本用来呈现Html的Presenter现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。