超越 MVP

为企业级应用程序 UI 体系结构扩展 MVP 模式

Haozhe Ma

Model-View-Presenter (MVP) 展现了一种关于 UI 模式的突破性思维方式,并明确了 UI 设计人员应该在应用程序中保持独立。

但是,对 MVP 模式有许多种不同的解释。例如,有些人想当然地认为 MVP 模式明确表示 UI 体系结构模式。这对于企业级应用程序来说,并不完全正确。与其他类型的 UI 应用程序相比,企业级应用程序需要满足许多不同的需求,涉及更多相关方,更加复杂,而且更多地交叉依赖于其他系统(例如服务、其他应用程序等)。这些独有的特征要求企业级应用程序的 UI 体系结构更强调灵活性、易维护性、可重用性和实施一致性,并且要求业务功能与基础技术分离,从而避免依赖于特定的产品和供应商。

如果仅仅采用 MVP 模式本身作为企业级应用程序的 UI 体系结构模式,会出现一些问题。下面列出了部分问题:

典型的企业级应用程序包含许多视图,一个视图中发生的事件可能会影响其他视图。例如,在一个屏幕中单击一个按钮,可能会导致显示一个弹出窗口,同时可能会更新另一个屏幕的数据。谁负责控制这样的屏幕流逻辑?这是否应该由每个视图的配对表示器控制?

在面向服务的体系结构 (SOA) 中,应用程序 UI 通常会通过服务来获取信息。例如,UI 可能需要调用所生成的 WCF 服务客户端代理,以便调用 WCF 服务以获取数据。直接调用此服务客户端代理是不是一种良好的表示器设计呢?如果这些服务采用不同的技术实现,或者如果服务模型被更改,应该如何设计 UI 体系结构,才能使这些变化对 UI 实现的影响降到最低?

根据这种思路,有些实现可能会在整个应用程序中使用所生成的服务客户端代理模型。这样做会不会有什么风险?如果需要专用的 UI 模型,哪个部分负责在服务客户端代理模型与 UI 模型之间提供映射?

这些都已经不是新问题,而且已经引入了许多其他模式来弥补这些漏洞。例如,引入应用程序控制器模式是为了承担控制导航流的责任。

我想,如果能够将这些零零散散的 MVP 扩展讨论收集到一起并绘制 UI 体系结构设计的整体视图,一定会很有帮助。从企业级应用程序的角度来看这个问题,可以帮助 UI 架构师认识到 UI 设计所需的关键部件并定义统一的模式来指导 UI 应用程序的实现。

本文通篇使用了术语“MVP 模式”,但实际上,原始的 MVP 模式已经被它的两种变体所取代:一种是 Passive View 模式,另一种是 Supervising Controller 模式。这两种模式分别适应不同的方案,并且它们各有优缺点。图 1 中描述的 UI 体系结构主要基于 Passive View 模式并对其进行了扩展。这当然不是说不能基于 Supervising Controller 模式来构成 UI 体系结构,而只是我的个人偏好而已。

基于 Passive View 模式的体系结构
图 1 基于 Passive View 模式的 UI 体系结构

在开始讨论之前,让我们先弄清楚是什么扩展了 MVP 模式来构成 UI 体系结构。图 1 显示了从 UI 体系结构的高级视图来看,需要哪些关键部件。在此图表中,介绍了七个主要的部件:视图、表示器、UI 模型、流程控制器、服务代理、服务客户端代理模型和服务客户端代理。

视图

视图基本符合 Passive View 模式中的“视图”角色。从处理 UI 显示布局和特定于表示的逻辑开始,视图承担一系列简单的责任。

视图的第二项责任是向表示器引发事件,它需要一些实现来处理这项责任。首先,视图需要实现 IView 接口。为了确保只有很少的实现会影响表示器逻辑,并且为了提供单元测试功能,表示器应该通过 IView 接口与视图交互。

其次,视图需要定义能够与表示器进行交互的公共属性。在 Passive View 模式中,视图不会向表示器传递数据。而应该由表示器从视图选择它感兴趣的数据。这种实现方式进一步减少了视图与表示器之间的约定绑定,并且更清晰地分割了它们的责任。

但是,有一个问题是,视图属性应该采用何种数据类型。理想情况是,视图仅使用简单类型(例如字符串、整数等)来定义这些属性。但是,在实际实现中,如果视图按这种方式来定义数据,可能会非常繁琐。通过引用复杂类型(例如 UI 模型定义)来公开数据属性是可以接受的。这很好地平衡了体系结构的纯粹性与实施的方便性。

其三,当视图中发生事件时,视图也需要调用表示器操作。视图可以直接调用表示器,而不传递任何数据。在视图与表示器之间没有任何松散耦合设计,因为视图与表示器始终是配对的。使每一个表示器操作都专用于一个视图事件,是一种良好的设计。

视图的最后一项责任是响应属性值的更新。表示器会更新视图属性来指示变化。视图拥有足够的信息来决定如何响应这样的变化。它可能会直接刷新视图以反应数据变化,也可能会决定不执行任何操作。

表示器

表示器本质上符合 Passive View 模式中的“表示器”角色。但是,这里的表示器不决定流程。表示器从视图获取事件请求,并将事件请求发布给控制器,由控制器来决定下一步。由于表示器不处理流程逻辑,它就不知道来自视图的事件请求是否对其他视图有任何影响。因此当表示器从视图收到事件请求时,它会立即发布相应的事件,使控制器能够响应这些事件请求并决定流程的下一步。在控制器发出指令之前,表示器绝不假设它能继续执行任何操作。

控制器决定了是否需要执行表示器操作。当控制器调用表示器操作时,表示器随后就会执行该操作,例如通过服务代理来检索数据。如果表示器需要对服务执行操作,则它通过服务代理来完成此操作。它将向服务代理传递必要的参数,然后从服务代理获取结果。

当表示器准备好向视图通知数据更改时,它将通过更新视图的属性值来实现。然后,就由视图决定如何显示数据更改。如前所述,表示器通过 IView 接口与视图交互,而不直接访问视图对象。由于在启动表示器时,已经将视图实例传递给表示器,表示器已经有一个要处理的视图实例。

最后,表示器可以访问 UI 模型,并且如果以后还需要访问 UI 模型数据,可以将其放到缓存中。

流程控制器

流程控制器近似于应用程序控制器模式。两者的区别是,此处讨论的流程控制器的唯一责任是基于表示器引发的类型化事件来控制流程。流程不仅限于屏幕导航流。它还包括控制与事件请求相关的表示器操作的顺序。

流程控制器订阅表示器发布的事件,而且仅对表示器发布的事件做出响应。这些事件是类型化事件。换句话说,流程控制器不会对一般事件做出响应。

由于流程控制器仅对类型化事件做出响应,因此流程实际上在事件发生之前就已经预先确定了。这简化了流程控制器的逻辑。表示器发布的每个事件都包含流程控制器在启动其他表示器操作时所需的数据。

流程控制器将启动表示器和相关的视图实例(如果它们尚未启动)。由于交叉引用的问题,将需要控制反转 (IoC)。这也在表示器与流程控制器之间提供了松散耦合的设计。

UI 模型

UI 模型基本符合 Passive View 模式中的“模型”角色。在 Passive View 中,模型所承担的责任很少,仅仅提供模型结构定义。而且,如“表示器”部分所述,表示器负责维护模型状态。

我将其称为“UI 模型”而不是直接称为“模型”的目的是为了将其与“服务客户端代理模型”(在本文后面介绍)区分开来。

UI 模型定义了适合 UI 应用程序逻辑处理的模型结构。UI 模型定义看起来可能与服务客户端代理模型完全相同。但是,在有些情况下 - 尤其是在 UI 需要显示来自多个服务来源的数据时 - 需要重组的 UI 模型,此模型就会与服务客户端代理模型不同。

服务代理

服务代理在表示器与服务客户端代理之间扮演中间人角色。它的名字虽然叫服务,但并不一定是 Web 服务。它表示能够提供数据或执行业务逻辑的任何资源。可以是 Web 服务,但也可以是简单的文件 I/O。

服务客户端代理在 Web 服务技术中具有特殊意义。此处,我使用服务客户端代理来表示服务的网关。

服务客户端代理的实现有很强的技术依赖性。从表示器的角度来看,它不需要了解数据传输或提供的方式。这种技术依赖性很强的细节可隐藏在服务代理中。因此服务代理层保护了表示器,使其免受服务实现技术变化、服务版本控制、服务模型变化等的影响。

服务代理提供了可由表示器进行交互的操作。如果需要向这些操作传递复杂类型,您需要在 UI 模型下定义复杂类型。这也适用于操作的返回类型。然后,您就可以将这些操作调用传递给相应的服务客户端代理操作。在有些情况下,一个服务代理操作可能会启动多个服务客户端代理操作调用。

由于服务代理操作接受在 UI 模型下定义的复杂类型,服务代理操作在调用服务客户端代理操作时,需要从 UI 模型映射到服务客户端代理模型。当服务代理操作需要向表示器返回结果时,将在从服务客户端代理操作获得结果之后,将结果从服务客户端代理模型映射到 UI 模型。

这是一项非常繁琐的工作。但是,有一些工具可用来从一个模型结构轻松映射到另一个模型结构,因此这就变得更像一次性设计工作。

服务客户端代理和服务客户端代理模型

Web 服务技术中的服务客户端代理提供了对服务客户端的本地访问,而无论服务是否在远程托管。在本文中,我将服务客户端代理描述为服务的网关。服务客户端代理模型表示服务约定模型定义。

服务客户端代理向服务传递调用并返回服务的响应。如果服务是用 ASP.NET Web 服务 (ASMX) 或 Windows Communication Foundation (WCF) 技术实现的,则可以自动生成服务客户端代理。

服务客户端代理模型将反映服务约定模型结构定义。

实现示例

为了演示图 1 中所述的 UI 体系结构,让我们来看一个能够说明该实现的 Windows Forms 应用程序。这个示例应用程序包含在本文的下载中。

这个示例应用程序首先需要加载一个区域列表。选择区域后,就会显示从属于该区域的客户。选择客户后,将弹出一个时间范围查询窗口。输入开始时间和结束时间后,从属于所选客户的订单列表就会显示在主屏幕的数据网格中。

我使用其中显示区域列表的方案来解释示例应用程序中如何实现图 1 所示的 UI 体系结构。图 2 显示了此方案的调用序列。


图 2 UI 调用序列

在加载主屏幕窗体后,它会先启动表示器接口,并将当前视图实例传递给表示器的构造函数:

private void MainView_Load(

  object sender, EventArgs e) {



  _presenter = new MainPresenter(this);

  ...

}

MainPresenter 实例的启动将使 MainPresenter 的构造函数先将传入的 MainView 实例分配给类型为 IMainView 的私有变量。然后,它会启动控制器实例,并将当前的表示器实例传递给控制器的构造函数:

public MainPresenter(IMainView view) {

  _view = view;

  _controller = new Controller(this);

}

Controller 实例的启动将使该构造函数将传入的 MainPresenter 实例分配给类型为 IMainPresenter 的私有变量。此构造函数还会定义事件处理程序(例如 RetrieveRegions),以便准备对 MainPresenter 发布的事件做出响应:

public Controller(IMainPresenter presenter) {

  _mainPresenter = presenter;

  ...

  _mainPresenter.RetrieveRegions += (OnRetrieveRegions);

}

回到主屏幕窗体的加载事件,在启动表示器对象之后,会调用表示器来检索区域:

Private void MainView_Load(object sender, EventArgs e) {

  ...

  _presenter.OnRetrieveRegionCandidates();

}

当表示器收到此调用时,它首先发布事件 RetrieveRegions,而不是直接检索区域。RetrieveRegions 事件已经在 IMainPresenter 接口中进行了定义,并且在 MainPresenter 中进行了实现:

public event EventHandler<RetrieveRegionsEventArgs> 

  RetrieveRegions;

  ...



public void OnRetrieveRegionCandidates() {

  if (RetrieveRegions != null) {

    RetrieveRegions(this, 

      new RetrieveRegionsEventArgs());

  }

}

在控制器类中,由于 RetrieveEvents 的事件处理程序已经注册,因此它会对 RetrieveRegions 事件做出响应:

private void OnRetrieveRegions(

  object sender, RetrieveRegionsEventArgs e) {



  _mainPresenter.HandleRetrieveRegionsEvent();

}

控制器决定流程应该将控制权返回给 MainPresenter 并要求它继续检索区域。如果控制器需要启动 MainPresenter 之外的表示器,它会利用 Unity Framework 来执行此任务。

在 MainPresenter 的 HandleRetrieveRegionsEvent 操作中,它会调用服务代理来检索区域。为了简化起见,我的示例并未真正实现此服务。它只是编写了一些伪数据来构成应用程序的功能。从服务代理返回结果后,请注意 MainPresenter 并未向 MainView 传递数据,而是更新 MainView 的 RegionCandidates 属性:

public void HandleRetrieveRegionsEvent() {

  RegionAdminServiceAgent agent = 

    new RegionAdminServiceAgent();

  List<Region> regionCandidates = agent.RetriveRegions();

  _view.RegionCandidates = regionCandidates;

}

在 MainView 的 RegionCandidates 属性中,它会处理区域的显示:

public List<UIModel.Region> RegionCandidates {

  set {

    _regionCandidates = value;

    PopulateRegionCandidates();

  }

}

这是检索区域并在 MainView 进行显示的整个序列。它的确涉及了很多步骤,而不仅仅是调用服务代理来获取区域。但是,如果从企业级应用程序的角度想一下,它不仅引入了一个松散耦合的设计,而且推广了一种一致的实现模式。这可以极大地简化维护以及开发团队的知识传递。

对此代码示例,只需再做一个说明:整个序列从第一个 Windows Forms 加载事件开始。更先进的实现可以从控制器开始,并让控制器决定首先加载哪个窗体。

总结

在本文中,我简要介绍了一种基于扩展 MVP 模式的 UI 体系结构设计方法。UI 应用程序可以非常复杂,有很多种不同的 UI 应用程序设计方法。我在本文中演示的技术仅仅代表很多种解决方案中的一种。这种技术在很多情况下都很有用,但在实现之前需要确保它适合您的需求。

市场上已经有了许多 UI 框架,其中很多都基于 MVP、Model-View-Controller 或作为这两者的扩展而开发的模式。第一步最好是看看这些框架实现的主要部件 - 例如,像我在本文中概括出 UI 体系结构一样。不先考虑全局就沉浸到实现细节中,不是一种良好的体系结构思维方式。从广泛理解当前问题的体系结构开始,可确保不仅解决系统体系结构的基本问题,而且会获得一套考虑周全的可重复设计。

最后,在本文的示例中,控制器的实现是用 C# 创建的。更好的方法可能是使用 Windows Workflow Foundation 等流程技术,因为这些技术可提供更灵活的设计和实现。但是,这种技术实现细节不会影响本文所述的 UI 体系结构背后所隐藏的基础原理。

如需进一步讨论 MVP 模式,请参见 2006 年 8 月出版的 MSDN 杂志

Zhe Ma 是 Unum Group(其总部位于缅因州的波特兰市)的企业体系结构技术架构师。可通过 zma@unum.com 与他联系。

衷心感谢以下技术专家对本文的审阅:Don Smith