MVP模式最佳实践(1)—MVP模式简介
虽然MVP是与任何具体的框架或技术无关的,但本系列文章仅以DotNet相关的技术或框架来来说明问题,请读者谅解。
.Net的盛行
.Net的出现无疑是一场变革,早期开发需要用C、C++、pascal等编写CGI程序,开发WEB应用,要求的技术门槛极高(当然,我听老一辈说那时的工资也高),使用纯Asp.Net开发WEB程序,门槛低,甚至不用编写任何代码就用实现一个简单的应用(当然,个人觉得这种应用没有什么太大的价值),这给广大的WEB开发者带来了福音,许多人纷纷转向.Net的怀抱,我记得那时在学校,还装模作样的安装了一个Visual Studio(我记着是Beta 2),写了两行代码,我记着还得手工建立虚拟目录,按F5,网页出来了,当时甭提多高兴了。
微软确实为我们打造了一个简单、易用且功能强大的开发平台,它节省了开发时间,提高了工作效率,赢得无数开发者的青睐,但是它在简单易用的同时,也带来了一些小"问题"(其它平台也可能存在同样的问题)。
WebForm的问题
既然我们都是搞程序写代码的,那就从一段代码谈起,请看:
{
SqlDataAdapter adapter = new SqlDataAdapter("select * from Table1", "server=.;database=db;uid=sa;pwd=password");
DataSet ds = new DataSet("ds1");
adapter.Fill(ds);
this.GridView1.DataSource = ds;
this.GridView1.DataBind();
}
上面的代码是标准的Asp.Net后台代码,它没有什么逻辑错误,运行结果也正确,但是它有一个问题,一个很严重的问题,我们都是博客园的人,我们都是有一定开发思路的人,我们都是有身份(证)的人,我们肯定知道"分层"这个东西吧,没错,上面的代码处在UI层,但是干了不该干的事(它明明是一只公鸡,但它却下蛋了),造成职责混乱,它把业务逻辑,数据访问,界面展现逻辑全部放在一段代码中,即不利于代码的阅读、维护、升级、也不利用测试,当然如果你的项目特别小的例外。
当然以上只是从分层的角度来考虑,指出Asp.Net的一个小小的问题,它没有约束开发人员在编写程序时注意分层,Asp.Net优势和其它的缺陷在这里不作重点说明。
MVP模式
当意识到这个问题的时候,似乎离好的解决方案的出现也不远了,Castle开源组织想到了在Asp.Net中应用MVC模式,开发出了MonoRails,第一个asp.net版的MVC框架,应该是MVC的Model2模型,MVC模型是一个很好的分层模型,它有好多变种,其中Model2就是一个相当不错的变种,也有大量开发人员在自己的项目里手工实现了这一模型,Asp.net MVC Framework的出现彻底普及了.Net界的以MVC模型思想为基础的框架,大量的开发人员开始学习研究ASP.Net MVC Framework,我记得我的好朋友重典就是其中一位,它并且应用到他的开源项目CHSNS中。
MVP是MVC模式的另一个变种,MVP即可以应用到WEB项目中, 也可以应用到Winform项目中,它的方便的测试机制为大型复杂的企业级应用带来了福音,下图是基于我的理解,画的MVP模式的层次图。
从图中可知道,MVP有Model-Presenter-View三个层次,下面是时序图
Controller层是负责状态保存和页面流转的有时根据需要,也需要Controller的参与
由以上几幅图综合分析可知,Presenter相当于中介者的作用,它负责接收视图发送来的请求,调用Model服务接口,Presenter再把处理结果反映到View,Presenter可以不对View层作强引用,可以接口引用,这样任何一个Presenter都不会依赖于任何一个具体的视图,而是依赖于具体的接口,一个视图接口可以有好几个的实现,大部分展现逻辑都有Presenter层中,它是一个具体的类,可测试性级高;同时Presenter与WEB或是Win无关,这就更增加了MVP的可测试性。View的具体实现有对Presenter的强引用,在测试时我们只要制作一个Mock的VIEW,实现View即实现接口IView,就可以测试了。所以说复杂的企业级应用我觉得用MVP模式比较合适。
View层即一系列视图规约(视图接口类)和视图的具体实现,视图规约定了可以从该视图取得哪些信息和可以调用哪些接口更新视图。以下代码是我正在开发的,正准备开源的一个利用MVP架构写的SNS网站的发送及编辑日记的源代码:
/// <summary>
/// SNS网站中日记发送及日记编辑的视图接口
/// </summary>
public interface ISendView
{
/// <summary>
/// 日记类别ID
/// </summary>
string CatId
{
get;
}
/// <summary>
/// 日记标题
/// </summary>
string DiaryTitle
{
get;
}
/// <summary>
/// 日记内容
/// </summary>
string Content
{
get;
}
/// <summary>
/// 日记中提到了好友ID,用逗号分隔
/// </summary>
string AboutUserIds
{
get;
}
/// <summary>
/// 是否为私密日记
/// </summary>
bool IsPrivate
{
get;
}
/// <summary>
/// 日记ID
/// </summary>
string Did
{
get;
}
/// <summary>
/// 绑定日记类别,此日记类别为用户可选的日记类别
/// </summary>
/// <param name="infos">类别信息列表</param>
void BindCategories(List<DiaryCategory> infos);
/// <summary>
/// 绑定日记内容,在编辑已经写好的日记时使用
/// </summary>
/// <param name="diary">日记实体</param>
void BindDiary(Spiderweb.Models.Diary diary);
}
发送及编辑日记的视图实现类:
public partial class Send : Microsoft.Practices.CompositeWeb.Web.UI.Page, ISendView
{
private SendPresenter _presenter;
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
this._presenter.OnViewInitialized();
}
this._presenter.OnViewLoaded();
}
[CreateNew]
public SendPresenter Presenter
{
get
{
return this._presenter;
}
set
{
if (value == null)
throw new ArgumentNullException("value");
this._presenter = value;
this._presenter.View = this;
}
}
protected void BtnSend_Click(object sender, EventArgs e)
{
Presenter.OnSend();
}
#region ISendView 成员
public string CatId
{
get { return Request.Form[this.ddlCats.UniqueID]; }
}
public string Content
{
get { return this.example_textarea.Text; }
}
public string AboutUserIds
{
get { return this.SelectFriendsUserControl1.UserIds; }
}
public bool IsPrivate
{
get { return chkPrivacy.Checked; }
}
public void BindCategories(System.Collections.Generic.List<Spiderweb.Models.DiaryCategory> infos)
{
this.ddlCats.DataSource = infos;
this.ddlCats.DataBind();
}
public string DiaryTitle
{
get { return this.txtTitle.Text; }
}
public string Did
{
get { return Request["did"]; }
}
public void BindDiary(Spiderweb.Models.Diary diary)
{
this.txtTitle.Text = diary.Title;
this.example_textarea.Text = diary.Content;
this.chkPrivacy.Checked = diary.IsPrivate.Value;
this.ddlCats.SelectedValue = diary.CatId.Value.ToString();
}
#endregion
}
视图的实现类基类为一个Asp.Net的Page,它实现了ISendView 接口,为Presenter的获取视图信息及刷新视图提供了方便。
Presenter类的源代码:
public class SendPresenter : Presenter<ISendView>
{
private IDiaryController _controller;
IDiaryService _DiaryService;
public SendPresenter([CreateNew] IDiaryController controller, [ServiceDependency]IDiaryService diaryService)
{
_controller = controller;
_DiaryService = diaryService;
}
public override void OnViewLoaded()
{
}
public override void OnViewInitialized()
{
//调用服务,然后初始化用户界面
View.BindCategories(_DiaryService.GetCategories(_controller.CurrentUserId));
if (!string.IsNullOrEmpty(View.Did))
{
Spiderweb.Models.Diary diary = _DiaryService.GetDiaryByDid(_controller.CurrentUserId, View.Did);
View.BindDiary(diary);
}
}
public void OnSend()
{
//Controller负责发送日记,页面转向
_controller.Send(View.Did, View.CatId, View.DiaryTitle, View.Content, View.IsPrivate, View.AboutUserIds);
}
}
从代码中可知,Presenter类与Controller(IDiaryController )、Model层(IDiaryService )通讯,但都是接口依赖,所以测试起来非常方便。
Presenter与对View层的是约定是强制约定,也就是必须实现ISendView,这在一定程度上了避免了有些错误的发生,不管是开发时,还是运行时。下面是测试时所用的视图实现类的代码:
public class TestSendDiary : ISendView
{
#region ISendView 成员
public string CatId
{
get { return "001"; }
}
public string DiaryTitle
{
get { return "日记标题"; }
}
public string Content
{
get { return "日记内容"; }
}
public string AboutUserIds
{
get { return "001,002,003"; }
}
public bool IsPrivate
{
get { return false; }
}
public string Did
{
get { return "001"; }
}
public void BindCategories(List<Spiderweb.Models.DiaryCategory> infos)
{
}
public void BindDiary(Spiderweb.Models.Diary diary)
{
Console.WriteLine(diary.Title);
}
#endregion
}
在执行测试时,调用类似于如下的代码:
SendPresenter presenter = new SendPresenter();
presenter.View = new TestSendDiary();
//发表日记
presenter.OnSend();
我们可以在winform或控制台应用程序里进行MVP的测试,非常方便,当然也可以用MS提供的测试工具进行测试。
总之,MVP模式有它无可替代的优秀,表示逻辑可以独立于UI平台的存在而存在,它也可以处理实现相同规约的不同视图。它的方便的测试性为复杂企业应用开发带来了很多便利,当然,如果你的应用特别简单,我建议还是不要用MVP,因为它未免有些浪费。