• [转]MVP 模式实例解析


    【转】MVP 模式实例解析

    来自: http://www.tracefact.net/Software-Design/MVP-Pattern-Explained.aspx
    作者: 张子阳

    引言

    可能有的朋友已经看过我翻译的Jean-Paul Boodhoo的 模型-视图-提供器 模式 一文了(如果没有,建议你先看下再看这篇文章,毕竟这两篇是紧密联系的)。在那篇文章中,作者为了说明 MVP 的优点之一,易测性,引入了单元测试和NMock框架。可能有的朋友对这部分不够熟悉,也因为本人翻译水平有限,导致看后感觉不够明朗,所以我就补写了这篇文章,对作者给出的范例程序作了些许简化和整理,让我们一步步地来实现一个符合MVP模式的Web页面。

    开始前的准备

    译文中,作者使用了Northwind数据库的Customer表来作为范例,这个表包含了太多的字段,而且字段类型缺乏变化,只有一个自定义的Country类型,其余均为String类型。这样容易让大家忽视掉MVP模式需要注意的一点,或者说是优势之一:视图部分,通常也就是一个Aspx页面,向用户显示的数据类型只有一种可能,就是字符串。即便你想向用户显示一个数字,比如金额,在显示之前,也会要么显式、要么隐式地转换为了字符串类型;而对象的字段类型却可能是多种多样的。所以,View的接口定义只包含String类型的Set属性,而实际将各种类型向String类型转换的工作,全部在提供器中完成。通过这样的方式,页面的CodeBehind将进一步简洁,连格式转换都移到了单独的提供器类中了。如果上面的加粗的字体你一时不能领悟也不要紧,一点点看下去你自然会明白。

    本文中,我们使用一个Book类作为我们的领域对象,它将包含 字符串、日期、数字三种类型,后面我们会看到它的代码。本文的范例依然是以一个通过选择Book列表的下拉框,来显示Book的详细信息 的Web窗体页面来作说明。

    现在创建一个新的空解决方案,起名为 MVP-Pattern,我们开始吧。

    Model(Service)层的实现

    大家可能对译文的图1和图3有点混淆,实际上图1的Service层和图3的Model层是同一个事物,它们的工作都是一样的:实际的从数据库(或者存储文件)中获取数据、填充对象,然后返回给提供器。

     

    MVP.DTO 项目

    我们先在解决方案下创建类库项目 MVP.DTO,DTO代表着Data Transfer Object(数据传输对象),这个项目和通常三层、四层构架的业务对象(Business Object)很类似,注意DTO项目实际上不应该属于Model层,它不会引用任何项目,但是因为各个层的项目都会引用它,所以我们在这里先创建它:

    这个项目包含这样几个类,首先是BookDTO,它代表着我们的Book对象,它的代码如下:

    public class BookDTO {
        private int id;              // 索引
        private string title;    // 标题
        private DateTime pubDate; // 出版日期
        private decimal price;       // 价格
        // 构造函数 及 Get属性略...
    }

    接下来它还包含三个接口,这三个接口定义了 传送 给页面上下拉框(DropDownList)的数据,以及如何为下拉框 送数据。可能正是因为它们的目的是 数据传送 ,而不仅仅是将数据库表映射成业务对象,所以才会称之为DTO,而非Business Object吧。我们一个个来看下:

    首先,我们想一想DropDownList的每个列表项ListItem需要什么数据?当然是一个Text,一个Value了,所以定义第一个接口 ILookupDTO,它代表了ListItem所需的数据,只定义了这两个属性:

    public interface ILookupDTO {
        string Value { get; }    // 获取值
        string Text { get; }     // 获取文本
    }

    接着,给出了一个它的简单实现 SimpleLookupDTO :

    public class SimpleLookupDTO : ILookupDTO {
        private string value;
        private string text;

        public SimpleLookupDTO(string value, string text) {
           this.value = value;
           this.text = text;
        }

        public string Value {
           get { return value; }
        }
        public string Text {
           get { return text; }
        }
    }

    NOTE:如果是我,我会将之命名为IListItemDTO,但是这篇文章和译文联系甚密,所以我尽量保持和译文一样的命名

    接下来,我们还要要为页面上的DropDownList传送数据,所以再定义接口ILookupList:

    public interface ILookupList{
        void Add(ILookupDTO dto);       // 添加项目
        void Clear();                   // 清除所有项目
        ILookupDTO SelectedItem{get;}       // 获得选中项目
    }

    在 MVP.DTO 项目中只定义了这个接口,但没有给出它的实现,因为它的实现显然和UI层很靠近,所以它的实现我们将它放到后面的 MVP.WebControls 项目(UI层)中。

    最后是ILookupCollection接口及其实现。这里,我不得不批判一下这个接口的命名,它很容易让人困惑:因为List是一个集合,Collection也是一个集合,所以第一眼感觉就是ILookupCollection和 ILookupList应该是同一个事物,但是这里同时出现,让人摸不着头脑。实际上它们是完全不同的:

    • ILookupList 更多的是描述了一个事物,即是页面上的DropDownList,它定义的方法也是对其本身进行操作的。
    • ILookupCollection 描述的是一个行为,它仅包含一个方法,BindTo(),方法接收的参数正是ILookupList,意为将ILookupCollection的数据绑定到 ILookupList上。而ILookupCollection包含的数据,是ILookupDTO的集合(IList<ILookupDTO>,由类型外部通过构造函数传入)。

    public interface ILookupCollection {
        void BindTo(ILookupList list);
    }

    public class LookupCollection : ILookupCollection {
        private IList<ILookupDTO> items;

        public LookupCollection(IEnumerable<ILookupDTO> items) {
           this.items = new List<ILookupDTO>(items);  // 根据传递进来的items创建新的列表
        }

        public int Count {
           get { return items.Count; }     // 获取项目数
        }

        // 将项目绑定到列表
        public void BindTo(ILookupList list) {
           list.Clear();                           // 先清空列表
           foreach (ILookupDTO dto in items) {     // 遍历集合,绑定到列表中
               list.Add(dto);
           }
        }
    }

    到这里 MVP.DTO 项目就结束了,我们再来看一下大家都熟悉的数据访问层,MVP.DataAccess。

    MVP.DataAccess 项目

    这一是和数据最接近的一层,用来获取来自数据库(或者其它存储)的数据。因为本文的目的是讲述MVP模式的构架,我们不需要把注意力集中在数据访问上,所以这一层我直接HardCode了,而非从数据库中获取。

    这一层定义了一个接口 IBookMapper:

    public interface IBookMapper {
        IList<BookDTO> GetAllBooks();           // 获取所有Book
        BookDTO FindById(int bookId);           // 获取某一Id的Book
    }

    以及一个实现了此接口的BookMapper类:

    public class BookMapper :IBookMapper {
        private readonly IList<BookDTO> list;

        public BookMapper() {
           list = new List<BookDTO>();

           BookDTO book;
           book = new BookDTO(1, "Head First Design Patterns"new DateTime(2007, 9, 12), 67.5M);
           list.Add(book);
           // 略... 共添加了若干个
        }

        public IList<BookDTO> GetAllBooks() {
           return new List<BookDTO>(this.list);
        }

        public BookDTO FindById(int bookId) {
           foreach (BookDTO book in list) {
               if (book.Id == bookId)
                  return new BookDTO(book.Id, book.Title, book.PubDate, book.Price);
           }

           return null;      // 没有找到则返回Null
        }
    }

    NOTE:这里有一个技巧,在GetAllBooks()和FindById()方法中,我没有直接返回list列表,或者是list中的book项目,而是对它们进行了深度复制,返回了它们的副本。这样是为了避免在类型外部通过引用类型变量访问类型内部成员。更多内容可以参考我之前写的 创建常量、原子性的值类型 一文(Effective C#的笔记)。

    MVP.Task 项目

    MVP.Task 项目是Model层的核心,之前创建的两个项目都是为这个项目进行服务的。它包含一个接口 IBookTask,这个接口定义了Task的两个主要工作:1、返回所有的Book列表(用于绑定DropDownList列表);2、根据某一个Book的Id返回该Book的详细信息。

    public interface IBookTask {
        ILookupCollection GetBookList();        // 返回图书列表
        BookDTO GetDetailsForBook(int bookId);     // 返回某一图书
    }

    我觉得这个接口的定义是MVP模式的精华所在之一,GetDetailsForBook()方法很容易理解,我们几乎现在就可以猜到它会把工作委托给MVP.DataAccess项目的BookMapper去处理,因为BookMapper已经包含了类似的方法FindById()。关键就在于 GetBookList()方法,注意它返回的是ILookupCollection,而非一个IList<BookDTO>。这样我们在后面将介绍的提供器中,只需要在获取到的ILookupCollection上调用BindTo方法,然后传递列表对象,就可以绑定列表了,实现了Web页面和CodeBehind逻辑的分离(MVP模式的精要所在);而如果这里我们仅仅返回IList<Book>,那么绑定列表的工作势必要移交给上一层去处理。

    接下来我们面临了一个问题:MVP.DataAccess 项目中的 BookMapper.GetAllBook()方法返回的是 IList<Book>,而这里需要的是一个ILookupCollection。回头看一下ILookupCollection的实现,它内部维护的是一个IList<ILookupDTO>,ILookupDTO是业务无关的,它包含了Text和Value属性用于向页面上的DropDownList的列表项提供数据。在本例中,ILookupDTO的Text应该为书名,而Value应该为书的Id。这样,我们最好能创建一个Converter类,能够进行由BookDTO到ILookupDTO,进而由IList<BookDTO> 到 IList<ILookupDTO>的转换。最后将转换好的IList<ILookupDTO>作为参数传递给ILookupCollection的构造函数,从而得到一个ILookupCollection。

    注意到ILookupDTO是业务无关的,所以我们定义接口名称,为ObjectToLookupConverter,而非BookToLookupConverter。另外,以后我们可能创建其他的类型,比如Customer(客户)也能转换为LookupDTO,我们定义一个泛型接口(使得Converter类不限于BookDTO才能使用):

    public interface IObjectToLookupConverter<T> {
        // 将 T类型的对象obj 转换为 ILookupDTO类型
        ILookupDTO ConvertFrom(T obj);

        // 将 IList<T> 类型的对象列表 转换为 IList<ILookupDTO> 类型
        IList<ILookupDTO> ConvertAllFrom(IList<T> obj);
    }

    再定义一个抽象基类实现这个接口,抽象类实现接口的ConvertAllFrom()方法,并将其中中实际的转换工作委托给 ConvertFrom() 方法:

    public abstract class ObjectToLookupConverter<T> : IObjectToLookupConverter<T> {
        public abstract ILookupDTO ConvertFrom(T obj);

        public IList<ILookupDTO> ConvertAllFrom(IList<T> objList) {
           List<T> list = new List<T>(objList);
           
           return list.ConvertAll<ILookupDTO>(delegate(T obj) {
               return ConvertFrom(obj);     // 将实际的转换委托给 ConvertFrom()方法
           });
        }
    }

    最后,到了实际的将 Book 转换为 LookupDTO 的部分了,非常的简单:

    public sealed class BookToLookupConverter : ObjectToLookupConverter<BookDTO> {
        public override ILookupDTO ConvertFrom(BookDTO book) {
           return new SimpleLookupDTO(book.Id.ToString(), book.Title);
        }
    }

    好了,有了这些准备工作,我们实现 IBookTask接口就变得轻易的多了。现在,创建MVP.Task项目的最后一个类,BookTask。注意GetBookList()方法的实现过程,和我们上面的分析一模一样:

    public class BookTask : IBookTask {
        private readonly IBookMapper bookMapper;

        public BookTask()
           : this(new BookMapper()) {
        }

        public BookTask(IBookMapper bookMapper) {
           this.bookMapper = bookMapper;
        }

        // 获取图书列表
        public ILookupCollection GetBookList() {

           IList<BookDTO> bookList = bookMapper.GetAllBooks();// 获取IList<BookDTO>
           IList<ILookupDTO> list = // 转换为 IList<ILookupDTO>
               new BookToLookupConverter().ConvertAllFrom(bookList);

           // 构建ILookupCollection
           ILookupCollection collection = new LookupCollection(list);

           return collection;
        }

        // 获取某一图书的详细信息
        public BookDTO GetDetailsForBook(int bookId) {
           BookDTO book = bookMapper.FindById(bookId);
           return book;
        }
    }

    至此,Model层或者叫Service服务层的所有项目都已经结束了,我们接下来看MVP的V(View层)是如何构建的。

    View 层的实现

    Web 站点项目 和 MVP.WebControl 项目

    你可能会奇怪为什么现在就讲述View层,而不是Presenter提供器层?这是因为Presenter是View 和 Model的一个协调者,从下面幅图就可以看出来。所以,我们需要先看下View层如何实现,进而才能去讨论Pesenter层。

    View层包含两个项目,一个是站点项目,一个是MVP.WebControl项目,我们先看站点项目。它仅包含一个页面:Default.aspx,内容也是简单之极,我们先看页面部分的HTML代码:

    <h1>MVP 模式范例</h1>
    选择图书<asp:DropDownList runat="server" ID="ddlBook"></asp:DropDownList>
    <br /><br />
    <div style="line-height:140%;">
        <strong>书名:</strong><asp:Literal ID="ltrTitle" runat="server"></asp:Literal><br />
        <strong>出版日期:</strong><asp:Literal ID="ltrPubDate" runat="server"></asp:Literal><br />
        <strong>价格:</strong><asp:Literal ID="ltrPrice" runat="server"></asp:Literal>
    </div>

    非常的简单,是吧?然后我们再看一下后置代码,通常情况下,我们会在后置代码中写DropDownList的PostBack事件,并且设置根据得到的数据填充三个Literal控件的Text属性。而在MVP模式中,这部分的工作将会交由提供器来完成,所以,我们只需要为这些控件建立Set访问器,并且将页面的引用传给提供器就可以了(如何传递页面引用给提供器后面会讨论)。我们现在在页面的后置代码中添加一组Set属性,分别去为页面的三个Literal控件赋值:

    public string Title {
        set { ltrTitle.Text = value; }
    }

    public string Price {
        set { ltrPrice.Text = value; }
    }

    public string PubDate {
        set { ltrPubDate.Text = value; }
    }

    通常情况下DropDownList的填充也是在后置代码中完成的,而为了能让提供器对DropDownList的数据进行填充,我们需要让这个DropDownList能够与ILookupList联系起来,并进一步通过调用来自MVP.Task中的 ILookupCollection的BindTo()方法,来对列表进行绑定。

    记得到现在为止我们都没有实现 ILookupList接口,现在是时候实现它了,新建一个项目MVP.WebControl,添加对MVP.DTO的引用,然后创建ILookupList接口的实现WebLookupList。在对ILookupList接口的实现中,对DropDownList进行包装,为了更好的代码重用,我们传递DropDownList的基类ListControl,而非DropDownList本身:

    public class WebLookupList : ILookupList {
        private ListControl underlyingList;

        public WebLookupList(ListControl underlyingList) {
           this.underlyingList = underlyingList;
        }

        public void Add(ILookupDTO dto) {
           underlyingList.Items.Add(new ListItem(dto.Text, dto.Value));
        }

        public void Clear() {
           underlyingList.Items.Clear();
        }

        public ILookupDTO SelectedItem {
           get {
               ListItem item = underlyingList.SelectedItem;
               return new SimpleLookupDTO(item.Value, item.Text);
           }
        }
    }

    可以看到我们实际上将对这个接口实现的具体工作都委托给了 ListControl,这样,当我们在ILookupList上调用Add()方法添加列表项时,便会添加到页面的DropDownList上。

    记住:我们期望能让提供器送数据的所有Web页面上的控件,都应该为提供器提供一个入口。在前面,我们为三个Literal空间提供的入口是Set属性。这里我们一样需要提供一个Get属性,来让提供器能够获得一个ILookupList。在Default页面的后置代码中添加下面代码:

    public ILookupList BookList {
        get { return new WebLookupList(ddlBook); }
    }

    Presenter 层的实现

    实现Presenter(提供器)之前我们先考虑它的作用是什么:从Task中获取数据,然后送到View层(Aspx页面)中这就暗示 提供器必须包含 Task和 View层的引用。但是如果我们是无法让提供器引用站点项目的,因为站点项目不会生成单独的dll文件(基于每个页面生成dll)。但是站点却可以引用提供器,所以我们只要在提供器项目中定义一个接口,然后让页面去实现这个接口,我们通过这个接口去为页面送数据(调用接口的Set访问器)

    MVP.Presentation 项目

    现在你可以将页面上的三个Literal和一个DropDownList与这个View接口联系起来了。创建MVP.Presentation项目,然后我们定义Default页面需要实现的IViewBookView接口:

    public interface IViewBookView {
        ILookupList BookList { get; }
        string Title { set; }
        string PubDate { set; }
        string Price { set; }
    }

    这个接口的定义完全是基于Web页面的,你需要为页面提供哪些数据,或者为哪个控件送数据,那么就定义哪些属性。然后我们让Web项目引用MVP.Presentation项目,在修改页面的后置代码文件Default.aspx.cs,让它去实现这个接口(因为页面已经包含了这个接口的所有定义,所以这里只是起到一个向提供器传递窗体的作用)。

    public partial class _Default : System.Web.UI.Page, IViewBookView

    下一步,我们要实现提供器,我们在项目中再添加一个文件 ViewBookPresenter.cs,添加下面代码:

    public class ViewBookPresenter {
        private readonly IViewBookView view;
        private readonly IBookTask task;

        public ViewBookPresenter(IViewBookView view) : this(view, new BookTask()) { }

        public ViewBookPresenter(IViewBookView view, IBookTask task) {
           this.view = view;
           this.task = task;
        }

        // 初始化方法,绑定列表
        public void Initialize() {
           ILookupCollection collection = task.GetBookList(); // 获取图书列表
           collection.BindTo(view.BookList);   // 绑定到列表
           DisplayBookDetails();               // 显示图书信息
        }

        // 获取选中的图书的Id
        private int? SelectedBookId {
           get {
               string selectedId = view.BookList.SelectedItem.Value;

               if (String.IsNullOrEmpty(selectedId)) return null;

               int? id = null;

               try {
                  id = int.Parse(selectedId.Trim());
               } catch (FormatException) { }

               return id;
           }
        }

        // 显示特定图书的详细信息
        public void DisplayBookDetails() {
           int? bookId = SelectedBookId;

           if (bookId.HasValue) {
               BookDTO book = task.GetDetailsForBook(bookId.Value);
               UpdateViewFrom(book);
           }
        }

        // 更新页面的信息,在这里进行格式化
        private void UpdateViewFrom(BookDTO book) {
           view.Price = book.Price.ToString("c");         
           view.PubDate = String.Format(new DateFomatter(), "{0}", book.PubDate);
           view.Title = book.Title;
        }

        // 格式日期,作为示范,所有格式化工作都放到 Presenter中
        private class DateFomatter : ICustomFormatter, IFormatProvider {

           public string Format(string format, object arg, IFormatProvider formatProvider) {
               DateTime date = (DateTime)arg;
               return string.Format("{0}年{1}月{2}日", date.Year, date.Month, date.Day);        
           }
           
           public object GetFormat(Type formatType) {
               return this;
           }
        }
    }

    上面的代码是很直白的,只有一个主题思想:从task中获取数据,然后调用view接口的属性,或者从view接口获得DropDownList的引用(通过ILookupList),然后通过 BindTo()方法为列表填充数据。注意到Initialize()方法,它为列表填充数据,这个应该在页面加载之前就被调用;还有DisplayBookDetails()方法,它应该在列表的SelectedIndexChanged事件被触发时调用,所以我们还有最后一部没有做,再次修改Default.aspx.cs文件,设置这些方法的触发时机。

    最后一步,再次修改Default.aspx.cs文件

    在后置代码类中添加如下代码,完成上一小节说明的所有内容:

    private ViewBookPresenter presenter;

    protected override void OnInit(EventArgs e) {
        base.OnInit(e);
        presenter = new ViewBookPresenter(this);       // 创建Presenter的实例

        // 为DropDownList绑定事件处理方法
        ddlBook.SelectedIndexChanged += delegate {
           presenter.DisplayBookDetails();
        };
    }

    protected void Page_Load(object sender, EventArgs e) {
        if (!IsPostBack) {
           presenter.Initialize();      // 绑定列表
        }
    }

    这里值得注意的是 ViewBookPresenter 对象的创建,它通过this关键字,将页面本身传递了进去,而页面本身实现了IViewBookView接口,满足构造函数的签名,这样提供器通过IViewBookView便可以访问页面上的属性和列表,并为之提供数据。

    总结

    这篇文章是对 模型-视图-提供器 模式 一文范例程序的一个刨析和说明。在本文中,我们创建了一个包含多个项目的完整的符合MVP模式的Web页面。我们先创建了基础项目 MVP.DTO,用于传送数据、MVP.DataAccess,用于数据访问;接着分别创建了 Model层、View层、Presenter层,并讲述了它们之间的调用关系,以及使用的要点。通过这则范例,希望大家能对MVP模式有了一定的认识和了解。

    不一定项目的每个页面,都去采用MVP模式来构建。但如果运用的好的话,可以将多个页面共同的的某一部分(或者叫功能)抽象出来,使用同一个提供器,可以很大程度上实现代码重用。另外也可以一个Page实现多个IView,将页面功能分离成多个部分,需要使用哪个功能,就实现哪个IView,并使用相应的IViewPresenter进行初始化。

    感谢阅读,希望这篇文章能给你带来帮助!

  • 相关阅读:
    递归方程(续)
    递推方程
    协方差简单介绍
    排列组合
    牛顿法
    jquery常用方法总结
    RegExp的test()方法
    localStorage用法总结
    正则表达式
    登录页面按回车键实现登陆效果
  • 原文地址:https://www.cnblogs.com/zhouyinhui/p/1297866.html
Copyright © 2020-2023  润新知