Model-View-Presenter模式之 Step by Step

拜拜、爱过 提交于 2019-12-29 09:48:14

期以来,一直在C/S的世界里埋头苦干,偶尔会有朋友问我为什么没转向Web。我想,暂时的,除了架构与模式,我还没有额外的时间和精力。但归根结底,无论C/S亦或B/S,Server才是最让我着迷的部分。其中,UI无关的设计一直是分层架构方案的重要组成。它和无关持久化PI(Persistence Ignorance)一样,可以让我们把UI呈现相对独立出来,与应用层和领域模型脱耦。简单地说,就是可以把一个基于WinForm实现的UI换成ASP.NET等Web形式的,或者同时支持窗口与报表两种UI形式,而不影响其他层的实现。

具体到实践中,MVC模式(Model-View-Controller)已经成为了UI无关设计的经典,在企业架构中得到广泛应用。Model维护业务模型与数据,View提供呈现,Controller控制程序流程、传递和处理请求。本质上,这是一种Observer模式的具体实现。但是,MVC如此普及,却并非完美。它也有缺点,其中包括View可以直接访问Model,View中耦合了一部分业务对象转换的逻辑。即便是常见的ASP.NET的MVC,也不是一种纯粹的MVC,因为在ASP.NET的结构里,并没有一个严格的、独立的领域模型。

于是,MVP(Model-View-Presenter)逐渐步入了我们的视野。在领域驱动设计的方式下,作为系统核心的领域模型层,其上会有一个暴露领域模型功能的应用服务层,其下方则是提供数据库访问、事务管理等功能的基础层。应用服务层界定了整个系统的边界。在这种传统方式下,View将直接与Service交互。而在MVP下,将会在二者之间增加一个Presenter。Presenter将负责向Service提交请求,并驱动View的更新。

在MSDN里,有一篇Jean-Paul Boodhoo撰写的《设计模式: Model View Presenter》,可以作为对MVP概念的一个入门。我选择在该文基础上,以类似向导的方式完成一个MVP的示例,作为对该文的一个补充。特别要说明的是,我这个例子是非常粗糙的,而且只是文本描述性的,不是完整的、可运行的。

MVC-MVP

设我们要实现一个叫SomeForm的Windows Form。其中的Combox用于选择不同的SomeEntity,然后下方的TextBox和NumericUpDown显示SomeEntity中的姓名Name与薪水Salary。

SomeForm

一步,是把上面这个Form进行抽象,把我们关心的用于表达UI状态的属性集中到一个接口IViewSomeForm中。在上图可知,SomeForm要维护的状态属性包括三个:对应ComboBox的所有SomeEntity项,对应Name的string,对应Salary的decimal。对ComboBox的抽象,可以理解为一个集合Collection,该集合的每个元素包括两个属性:Text用于显示,Tag储存关联的对象,所以需要另行设计。

public interface IViewSomeForm
{    
    string FullName { get; set; }
    decimal Salary { get; set; }    
}

有了前面对ComboBox的抽象理解,便可以设计一个抽象的IUiComplexItem对应ComboBox的Item,用抽象的IUiComplexItemCollection对应ComboBox的Item集合。于是,先用UiComplexItemCombo实现了我们需要的ComboBox的两个内嵌属性Text与Tag。至于SomeDTO,暂时就理解为SomeEntity的替身,我们把它绑定到ComboBox某项上的对象,以方便通过item.Tag这样的方式提取SomeEntity的信息。

public interface IUiComplexItem
{}

public class UiComplexItemCombo : IUiComplexItem
{
    public UiComplexItemCombo(string text, SomeDTO tag)
    {
        Text = text;
        Tag = tag;
    }

    public string Text { get; private set; }
    public SomeDTO Tag { get; private set; }
}

接下来,由于UiComplexItemCombo只是实现了对ComboBox中Item的一个映射,而ComboBox通常维护的是一个Item的集合,所以我再添加一个接口IUiComplexItemCollection及其实现,用于管理这些Item。完成了这些以后,IUiComplexItemCollection实际成为了ComboBox的一个代理。

public interface IUiComplexItemCollection
{
    void Add(IUiComplexItem complexItem);
    void Clear();
    IUiComplexItem SelectedComplexItem { get; }
}

public class UiComplexItemCollectionProxy : IUiComplexItemCollection
{
    private readonly ComboBox _innerControl;

    public UiComplexItemCollectionProxy(ComboBox innerControl)
    {
        _innerControl = innerControl;
    }

    public void Add(IUiComplexItem complexItem)
    {
        this._innerControl.Items.Add(complexItem);
    }

    public void Clear()
    {
        this._innerControl.Items.Clear();
    }

    public IUiComplexItem SelectedComplexItem
    {
        get { return this._innerControl.SelectedItem as IUiComplexItem; }
    }
}

有了这些准备,我们的IViewSomeForm变成了下面这个样子。其中,原始类型我们提供了getter与setter,而对复杂的控件类型则只提供了IUiComplexItemCollection的getter,这是因为我们将要利用后面出现的Presenter借由IUiComplexItemCollection去绑定和驱动底层的该控件。

public interface IViewSomeForm
{
    // raw type (both getter & setter)
    string FullName { get; set; }
    decimal Salary { get; set; }

    // complex type (only getter)
    IUiComplexItemCollection Combo { get; }
}

二步,为SomeForm添加接口IViewSomeForm并实现之,让SomeForm能被该接口驱动。其中的_textboxName、_updownSalary和_comboBox是布置在SomeForm窗体上的TextBox、NumericUpDown、ComboBox等控件。

public class SomeForm : Form, IViewSomeForm
{
    // for raw type
    private readonly TextBox _textboxName = new TextBox();
    private readonly NumericUpDown _updownSalary = new NumericUpDown();

    // for complex type
    private readonly ComboBox _comboBox = new ComboBox();
    private UiComplexItemCollectionProxy _comboProxy;

    public string FullName
    {
        get { return this._textboxName.Text; }
        set { this._textboxName.Text = value; }
    }

    public decimal Salary
    {
        get { return (decimal) this._updownSalary.Value; }
        set { this._updownSalary.Value = value; }
    }

    public IUiComplexItemCollection Combo
    {
        get { return this._comboProxy; }
    }
}

三步,SomeForm暴露了它的接口,所以开始准备驱动View的表现器Presenter。从前述MVP的结构看,Presenter一头连着View,另一头连着为Presenter提供数据和服务的Service。所以我们使用依赖注入的方式,为Presenter注入IViewSomeForm与IService对象。不过为简单起见,我在例子中只选择了注入IViewSomeForm。

public class Presenter
{
    private readonly IViewSomeForm _view;
    private readonly IService _service;

    public Presenter(IViewSomeForm view)
    {
        _view = view;
        _service = new SomeService();
    }
}

完成注入后,Presenter便可以通过IView暴露的属性,去改变View的呈现。因此,我们先整理Presenter关心的事件,这是非常重要的一步。这些事件中,其中一个是View的加载,Presenter需要帮助SomeView中的ComboBox绑定好Item的集合,以方便用户在下拉列表中选择。另一个事件是ComboBox的选择项变化后,Presenter应该更新Name与Salary的显示值。于是,我们为Presenter添加如下两个方法OnIntialize()与OnSelectedIndexChanged()。

其中的OnInitialize(),是从Service中读取一个SomeDTO的列表,然后利用一个转换子convertor,把SomeDTO列表转换为SomeForm中ComboBox需要的Item的集合,可以简单理解为“从Service获取数据,然后绑定到View中的控件上”。

public class Presenter
{
    private readonly IViewSomeForm _view;
    private readonly IService _service;

    public Presenter(IViewSomeForm view)
    {
        _view = view;
        _service = new SomeService();
    }

    public void OnInitialize()
    {
        var convertor = new ConConvertorDTOToUiComplexItem();
        var dotList = _service.GetDTOList() as IList<SomeDTO>;

        convertor.BindTo(dotList, _view.Combo);
    }

    public void OnSelectedIndexChanged()
    {
        var dto = ((UiComplexItemCombo) _view.Combo.SelectedComplexItem).Tag;

        _view.FullName = dto.Name;
        _view.Salary = dto.Salary;
    }
}

四步,有了Presenter,接下来就需要修改View的实现,让二者可以互动了。这个例子很简单,在SomeForm窗口的Load事件中完成了前面Presenter关心的两件事。一是Presenter初始化,准备好ComboBox中的下拉列表。另一个是当ComboBox的选择改变时,Presenter要及时更新Name与Salary,所以我们将之前Presenter中定义的方法OnSelectedIndexChanged(),利用委托绑定到ComboBox的SelectedIndexChanged事件上。

public class SomeForm : Form, IViewSomeForm
{
    // for raw type
    private readonly TextBox _textboxName = new TextBox();
    private readonly NumericUpDown _updownSalary = new NumericUpDown();

    // for complex type
    private readonly ComboBox _comboBox = new ComboBox();
    private UiComplexItemCollectionProxy _comboProxy;

    public string FullName
    {
        get { return this._textboxName.Text; }
        set { this._textboxName.Text = value; }
    }

    public decimal Salary
    {
        get { return (decimal) this._updownSalary.Value; }
        set { this._updownSalary.Value = value; }
    }

    public IUiComplexItemCollection Combo
    {
        get { return this._comboProxy; }
    }

    private Presenter _presenter;

    private void form_Load(object sender, System.EventArgs e)
    {
        this._presenter = new Presenter(this);
        this._presenter.OnInitialize();

        this._comboProxy = new UiComplexItemCollectionProxy(this._comboBox);
        this._comboBox.SelectedIndexChanged += delegate { this._presenter.OnSelectedIndexChanged(); };
    }
}

此,一个MVP的模型算是基本完成了。当然,作为一个例子,它很粗糙。我们还可以再进一步尝试抽象和解耦,可以增加一些事件接口,提供多线程支持,或者完善依赖注入的方法。比如,根据前述第三步中Presenter关心的事件列表,在IViewSomeForm中定义一些event并暴露给Presenter,从而方便Presenter将自己的事件处理方法挂载上IView,而不是使用上面form_Load()中那种生硬的事件绑定方法。

然,MVP也不是没有缺点。MVP的主要问题是增加了额外的接口和交互,提高了系统的复杂度。同时,某个Presenter定义总是与某一个IView定义存在紧密的联系,这种联系有时会带来额外的耦合问题。

还需要讨论的,就是关于接口所属层的划分与如何保持领域模型封闭性了。从个人的理解而言,将领域对象直接暴露给上层是很危险的、也是不恰当的,这样做容易使领域模型遭到“污染”。另一方面,现代分布式的系统,也对数据传输和表示提出了扁平化的要求,因此可以选择增加一个数据传输层,或者将DTO的相关功能合并入应用服务层,专门用于管理数据传输对象DTO(Data Transfer Object)。而在MVP相关接口的分配上,我个人倾向于将UI呈现相关的所有接口都定义在Presenter中,因为Presenter与IView之间总有一个固定的对应关系。于是,我们得到了如下所示的一张架构图(点击可查看1600x1400大图)。

最后,附上前面引用的Convertor和其他一些相关的示例代码。

public interface IService
{
    IList<DTO> GetDTOList();
}

public class SomeService : IService
{
    public IList<DTO> GetDTOList()
    {            
        return new List<SomeDTO>() as IList<DTO>;
    }
}

public interface IConvertor<T>
{
    void BindTo(IList<T> dtoList, IUiComplexItemCollection uiComplexItemCollection);
}

public class ConvertorDTOToUiComplexItem : IConvertor<SomeDTO>
{
    public void BindTo(IList<SomeDTO> dtoList, IUiComplexItemCollection uiComplexItemCollection)
    {
        uiComplexItemCollection.Clear();

        foreach (var dto in dtoList)
            uiComplexItemCollection.Add(new UiComplexItemCombo(dto.Name, dto));
    }
}

public abstract class DTO
{
    public int Id { get; set; }
}

public class SomeDTO : DTO
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
}

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!