序: 让我们首先通过现实的例子来看看 Model、View、Presenter 应该如何分工吧。View 就像是客服人员(或者留学中介里的顾问),Model 是那些具体的技术支持人员(或者文案,专门处理签证申请材料),Presenter 是组长或部门经理。
View 不需要做太多的具体事情,他们最好相貌好点,声音甜点,对用户友好点,让用户心情舒畅就好,用户的最终问题最终还是由具体的技术人员(技术支持,售后)处理。对于个体户,小作坊,工作室来说,客服、技术可能一人包了,这样效率最高,但后来你发现所有问题都你一个人处理,一个人不可能三头六臂,事必躬亲最终只能做作坊;这是单层结构。
所以你招聘了新的员工让他们负责具体技术问题处理,你专心做客服,这样你能承接的业务量大了,当然代价是工作效率低了成本高了,但这是做大必须要付出的代价;这是两次结构。
后来你的公司规模越来越大,你就发现人员之间关系复杂,相互之间依赖严重,管理起来头绪繁多;并且分工不明确,权责不分明,扯皮时有发生,很多问题接踵而来。现在你才发现,管理不正规化是很难做大做强的,因此,你进行了组织结构调整并建立起专业化的互补团队,他们各司其职,你只要管理好你的经理层,整个企业就能做的很好。每个部门都有自己明确的职责,每个部门内部都有几个相互合作的小组或团队,部门经理的核心工作就是协调,让合适的人(团队)做合适的事情。同时不鼓励培养全能型员工,因为不可能所有的员工都能全能,而依赖于少数全能员工无疑是有风险的,所以尽量分工细化,尽量使员工的工作简单(这样很容易找到合适的替换人员),尽量培养互补的专业化人才。
这样整个公司就比较和谐,代价就是沟通成本更高了,公司仅仅因此付给管理层的薪水就很可观,但没办法,要做世界 500 强,不舍得出血是不可能的。
当然,并不是付了这些成本就可以做世界 500 强,要想做好首先要规划好,应该如何分工应该如何协作,其次要找好管理者协调好各部门各组织的工作,否则不但增加了成本反而会使整个系统更混乱。
同样的道理,能不能做好一个系统与使不使用 MVP 或者 MVC 这些模式其实没有必然的关系,并不会应为使用了 MVP 项目就会成功,同样也不能将项目失败的原因归结到 MVP 或者 MVC 这些模式,而说它们不好。谋事在人,成事也在人。能否领会到 MVP 的优缺点,能否准确的判断你的团队能否驾驭 MVP 才是关键。
一、View 设计参考
View 和 Presenter 的设计应该以用例(Use Case)为基础,以面向对象设计原则为准则。StopLight 和 BankBranchWorkbench 参考实现就是根据用例进行设计的。
根据面向对象的设计原则,对象的职责要明确单一(强内聚),对其他对象的细节了解的越少越好,尽量的只需要关心别人的接口,而不必关心别人的实现。StopLight 的设计可以给我一些启示:
StopLight 的 View 有 StopLightView 是 IStopLightView 组成。
IStopLightView 接口定义了 View 在该用例中具有的能力,IStopLightView 的职责是告诉 Presenter 我能够提供什么服务,你能够从我这获得什么信息:
public interface IStopLightView : INotifyPropertyChanged
{
Color CurrentColor { get; set; }
string GreenDuration { get; set; }
string YellowDuration { get; set; }
string RedDuration { get; set; }
event EventHandler UpdateClicked;
event EventHandler ForceChangeClicked;
void SetError(string propertyName, string errorMessage);
}
也就是说 View 的实现应该允许我们设置并获取当前显示的颜色, 运行我们设置并获取每种颜色的显示时间间隔,运行我们告诉它有错误发生(具体它如何处理该错误信息我们不关心),同时在通供用户接口并且在用户操作后通知我们,具体我们要做什么,View 接口也不关心。
StopLightView 是 IStopLightView 接口的具体实现,它继承自 UserControl ,因此是 WinForm 类型的界面。作为用户控件对象,其核心职责是表现,给用户提供友好的用户接口,包括美观的布局和方便的操作,而不用关心业务逻辑。
在实现 IStopLightView 接口方面,StopLightView 始终恪守本分,不做任何与自己无关的事情,只负责将自身包含的子控件属性的读写(包括 ErrorProvider),提供事件注册接口,并在属性更改时发布通知(利用事件实现发布订阅模式)。
通过阅读 StopLightView 的代码我们看到,虽然 StopLightView 强依赖于 StopLightViewPresenter ,但整个 StopLightView 只使用了 StopLightViewPresenter 的 View 属性和 OnViewReady() 方法,而这两个方法(属性)都是抽象类 Presenter<TView> 已经定义的,也就是说即使我们没有对 StopLightViewPresenter 做任何开发,StopLightView 也是可以编译通过的。
因此 IStopLightView 接口只负责提供属性,事件定义和操作接口,这些和界面相关,是用户接口(UI)的职责范围,但 IStopLightView 对具体的实现形式及页面布局没有任何要求,我们可以使用 Winform 实现,也可以使用 WPF、SilverLight,甚至我们可以使用 ASP.NET 或者 AJAX 来实现,实现者也不会有对界面布局方式,操作方式的约束,只要他们能够实现 IStopLightView 接口就一切 OK 。
【FLYabroad】这就是职责单一!这就是松耦合!这给我们以后维护界面,更改界面带来了极大的方便;同时,对于团队开发,我们完全可以把 UI(UX) 设计与程序业务逻辑设计分开,也因此方便了我们对 UI 及业务逻辑的协作开发和分离测试。
二、Presenter 设计参考
在前一篇中,我们已经简单的看了 Presenter 对 MVP 架构的支持,现在我们再仔细的看看 Presenter<TView> (public abstract class Presenter<TView> : IDisposable)。
所有具体的 SCSF Presenter 都应该继承自抽象基类 Presenter<TView> 。该抽象基类提供了 MVP 模式的基本框架,是 MVP 模式的核心。
Presenter 的主要职责就是用于协调 View 和 Model,在 StopLight 项目中是通过依赖注入与 Model 建立了联系,而在典型的 SCSF 中,Presenter 与 Model 的关系应该通过 WorkItem 建立 。
每个 Presenter 都有一个当前 WorkItem 实例:
[ServiceDependency]
public WorkItem WorkItem
{
get { return _workItem; }
set { _workItem = value; }
}
[ServiceDependency] Attribute 告诉 ObjectBuilder 把环境中的当前WorkItem 实例附给当前 Presenter 的 _workItem 字段。以后任何 Presenter 实例都可以通过当前的 WorkItem 获取需要的资源,包括已注册的 Services,SmartParts,Commands,State,UIExtentions 等。
Presenter<TView> 中还有三个虚方法,两个 public 一个 protected 。
/// <summary>
/// 在具体 View 的 OnLoad 方法中调用
/// </summary>
public virtual void OnViewReady() { }
/// <summary>
/// 在 TView 被设置时调用
/// </summary>
protected virtual void OnViewSet() { }
/// <summary>
/// 在 Dispose 中调用
/// </summary>
public virtual void OnCloseView() { }
具体 Presenter 子类可以重写上面三个方法来做必要的工作(这与 Form 编程中的 OnLoad等方法类似,很容易理解)。
在执行顺序上, OnViewSet() 在 Presenter 类设置 View 属性时最先被调用:
// Presenter<TView> 类中的 View 属性,set 时调用 OnViewSet() 方法
public TView View
{
get { return _view; }
set { _view = value; OnViewSet(); }
}
根据前面讲的,View 被创建时会自动创建对应的 Presenter 并将 View 实例附给该 Presenter,因此 OnViewSet() 在 View 被创建后初始化时调用,这时 View 和 Presenter 都已经创建,可以做进一步初始化。StopLight 项目中的 StopLightViewPresenter 重写了 OnViewSet() 方法,在里面调用了私有方法 wireEvents() :
private void wireEvents()
{
View.PropertyChanged += OnViewPropertyChanged;
View.UpdateClicked += OnViewUpdateClicked;
View.ForceChangeClicked += OnViewForceChangeClicked;
Schedule.ChangeLight += delegate { Stoplight.Next(); };
Stoplight.Changed += OnStoplightChanged;
}
该方法将事件处理器和事件发布者进行了绑定,这是构建松散耦合应用的另一重要方式。
OnViewReady() 方法在具体 View 的 OnLoad 方法中调用:
/// <summary>
/// 在 View 加载时允许 Presenter 注入适当的操作。开发者可以在 Presenter 中重写 OnViewReady() 介入视图加载过程。
/// </summary>
/// <param name="e"></param>
protected override void OnLoad(EventArgs e)
{
_presenter.OnViewReady();
base.OnLoad(e);
}
OnLoad 在第一次显示窗体前调用,这时基本的初始化工作可以完成,子程序可以做进一步的初始化,如分配控件使用的资源。StopLightViewPresenter 重写了 OnViewReady () 方法,在里面调用了私有方法 initViewAndStart():
private void initViewAndStart()
{
View.GreenDuration = "3000";
View.YellowDuration = "500";
View.RedDuration = "5000";
View.CurrentColor = Color.Green;
Schedule.Update(TimeSpan.FromMilliseconds(3000),
TimeSpan.FromMilliseconds(500),
TimeSpan.FromMilliseconds(5000));
Schedule.Start();
}
至此,定时器开始工作,业务流程正式启动。
【FLYabroad】每个用例都对应一组 View 和 Presneter 。 Presenter 是整个用例的大管家,控制协调中心,自己基本上不做具体事情,核心工作都是组织和协调。作为控制器,Presenter了解与该用例相关的所有事情,因此能够将合适的事情指派给合适的人去处理。
三、Model 设计参考
上一篇说过,Model 在程序中是另一个被滥用的名词,很多东西都可以归为 Model ,我们这里把 Model 认为是与实际业务模型对应的程序模型,这些模型是对现实世界业务的建模。在 SCSF 中,Model 通常可以用一系列的 Services 来表示。
模型中对现实世界的实体进行的建模一般是比较稳定的,因此这部分变化小;现实中业务规则还有人们对业务规则的理解往往是变动的,我们通过 Presenter 来应对这种变化,这样 Model、 Prestener、View 都可以通过这种松散耦合来达到开闭原则,从而使我们的系统有步骤的强大。因为开闭原则意味著我们原先做的工作都是有意义的,不会因为局部的改变而是我们推到重来。
获取这种松散耦合的优势最主要得益于面向对象语言的多态特性和里氏代换原则,当然这种优势背后的代价就是使我们多写了不少代码,使程序对于一般人来说似乎变得复杂。
【FLYabroad】Model、View 、Presenter 设计要以用例(Use Case)为基础,以面向对象设计原则为准则。力求职责单一,有选择的面向抽象,构建易测试、易理解的松散耦合系统。
【总结】仔细想想 MVP 模式(或者 MVC)的分工与现实世界的分工原来是如此的一致,如此的和谐。View 就像是前台的客服人员,Model 是实际的技术支持人员,Presenter 是他们的主管,这样势必会增加沟通的成本,但对于大公司这种成本是必须的,否则永远不可能最大做强。