• ModelViewPresenter模式之 Step by Step


    期以来,一直在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; }
    }

    转载请注明出处及作者,谢谢!
  • 相关阅读:
    面向对象的测试用例设计有几种方法?如何实现?
    html5直接调用手机相机照相/录像
    关于ionic2在IOS上点击延迟的问题
    vue项目使用html5+ barcode扫码在苹果遇到的问题以及自己的解决方法
    vue设置多个入口
    把项目中的vant UI组件升级
    记录axios在IOS上不能发送的问题
    getElementsByClassName兼容 封装
    记录vue用 html5+做移动APP 用barcode做扫一扫功能时安卓 的bug(黑屏、错位等等)和解决方法
    JS的事件委托
  • 原文地址:https://www.cnblogs.com/Abbey/p/2472030.html
Copyright © 2020-2023  润新知