长期以来,一直在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的示例,作为对该文的一个补充。特别要说明的是,我这个例子是非常粗糙的,而且只是文本描述性的,不是完整的、可运行的。
假设我们要实现一个叫SomeForm的Windows Form。其中的Combox用于选择不同的SomeEntity,然后下方的TextBox和NumericUpDown显示SomeEntity中的姓名Name与薪水Salary。
第一步,是把上面这个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; } }