UI 架构小史1(GUI Architectures)

96
作者 ntop
2015.11.23 16:06* 字数 10812

写GUI的同学(比如现在主流的移动平台,Android/iOS)都会面临GUI-App架构的问题,好的架构容易扩展易于测试便于维护,但是对于架构的议题网上的讨论并不是够深入,这篇文章是06年左右的时候MartinFowler写的,读完之后收获颇深。

此文介绍了几种主流(分不同时期)架构的历史和实现方式,总结了各自的特点和优缺点。

译文:
对于一个富客户端的系统有很多种方式去组织代码。这里我选了一些感觉比较主流的,介绍了他们和设计模式的关系(and introduce how they relate to the patterns)

无论对于用户还是开发者,在软件设计中图形用户界面已经变成令人熟悉的一部分。从设计的角度来看,他们代表了系统设计中一系列特殊的问题——这些问题衍生了几种不同但是略有相似的解决方案。

我感兴趣的是找出共同并且有用的模式,以便App开发者可以在富客户端(rich-client)开发中使用。我在做项目review的时候看到过各种设计并且也在写代码的时候用过很多种设计。拿 Model-View-Controller 为例,它经常被看做一种模式,但是我发现把它看做一种模式不会非常有用,因为它包含几种完全不同的理解。不同的人在不同的地方理解MVC的时候会产生不同的见解,并称之为“MVC”。如果这还没有产生足够的混淆,你会在开发一个类似“Chinese whispers” (传话游戏)的系统中感受到误解MVC的后果。

在这篇文章中,我会探索几种有趣的架构并根据我的理解描述其中最有趣的特色。我希望这样会给接下来我描述的模式提供一个上下文。

在一定程度上,你可以把这篇文章看做是通过介绍过去这些年产生的各种架构来追溯各种UI设计思想的知识史。然而我必须提醒,理解这些架构并不容易,特别是因为他们中的很多都已经改变了或者死掉了。追溯这些思想的传播会更困难,因为人们会从同一种架构中读出不同的理解。尤其是,我也没有对我写的这几种架构做彻底的调查。如果描述有所出入,我也会完全不知道,所以别把我所说的当做权威。另外,我故意漏掉了一些东西或者对我认为不太相关的东西做了简化。记住我的要旨是底层的模式,而不是这些模式的历史。

(这是一个意外,我真的找到了一个还在运行的Smalltalk80系统去了解它的MVC实现。虽然我还是不能说我的研究是彻底的,但是它还是揭示了一些常见的描述错误,这使我对接下来要描述的其他架构变得更加小心。如果你对其中的某种架构比较熟悉,并发现我对一些重要的点得描述是错误的或者漏掉的,我很希望知道这些。我觉得对这领域做一个彻底的研究是一个很好的学术课题。)

Forms and Controls


我会从一个既简单又熟悉的架构开始。它没有一个通用的名字,在本文中我称之为“Forms and Controls”。它是一个熟悉的架构因为它是在90年代 client-server 开发环境(比如VisualBasic,Delphi,Powerbuilder)中诞生的一种。它被广泛的使用,尽管经常被像我这样的设计Geeks所鄙视。

为了研究它和其他的框架,我会用一个常见的案例。在NewEngland,我住的地方,有一个政府软件用来监控大气中的冰淇淋微粒含量。如果浓度太低,表明我们没有吃掉足够的冰淇淋,这意味着严重的经济和公共消费的风险。(我喜欢用这种不太现实的例子,你在本书中可以找到很多。)

为了监视冰淇淋健康,政府开始在NewEngland设置了很多监测站。使用了复杂的大气模型,政府部门给每个监测站都设置了目标(target)。职员偶尔会走出去到各种监测站做一些评估并记录真实的冰淇淋微粒浓度。这个界面可以让他们选择一个站点,输入日期(date)和真实的值(actual)。这个系统会计算并显示和目标值得差值(variance)。系统会在差值大于10%的时候显示成红色,大于5%的时候显示成绿色。


Figure 1: The UI I'll use as an example.

在我们看到这个界面的时候,把不同元素组合在一起会发现一个重要的分层。form根据我们的程序而定(译注:这是一个WinForm程序,Form一般写一些自有逻辑,Controls专指WinForm的可复用控件),但是它使用的controls是通用的。大部分GUI环境都会提供很多可以在App中直接使用的通用controls。我们也可以新建controls,有时候这么做是非常好的想法,但是可复用的controls和专用的forms还是有很多差别的。尤其是写好的controls可以在不同的forms中复用。

Form包含两个主要的职责:

  • 屏幕布局:定义controls在屏幕的布局和各自之间的分层结构
  • Form逻辑:实现那些不能在controls中简单实现的其他行为

大部分GUI开发环境允许开发者使用图形化界面拖拽屏幕布局,这样可以很好地处理form的布局。这样可以简单的给Form建立一个不错的布局(尽管这不是最好的方式,稍后会说)。

Controls显示数据 - 在这个例子指的是 reading(The controls display data - in this case about the reading. )。这些数据很可能来自其他地方,本例中假设来自一个SQL数据库。在大部分场景下会引入三种数据拷贝。

  • 一部分拷贝存在数据库中。这份拷贝是数据最后的记录,我称之为记录状态(record state)。记录状态经常被共享可以被多人通过不同的方式看到。
  • 第二份拷贝存是在于内存中的记录集合(Record Sets)。大部分的client-server环境都会提供方便的工具来实现它。这些数据仅和应用和数据库之间的一次特定会话(session)有关,所以我称之为会话状态(session State)。实际上,它提供了原始数据的临时版本,用户可以操作这些数据直到用户保存或者提交回数据库——这时,它会和记录状态合并。这里我并不担心协调记录状态和会话状态的数据的问题:我在 [P of EAA] 中谈到了很多技巧。
  • 最后的一份拷贝存在于GUI组件中。这部分,严格的说是用户在屏幕上看到的部分,因此我称之为屏幕状态(screen state)。在有UI的App中同步屏幕状态和会话状态的数据是非常重要的。

保持屏幕状态和会话状态的同步是一项重要的任务。一个能使之变得简单的工具是数据绑定( Data Binding )。这个想法是:任何改动无论是controls的数据还是底层记录集合(record set)会被立刻同步到对方。所以,如果我改变屏幕上actual reading(alter the actual reading on the screen),text field控件会立刻在底层数据集中更新正确的列。

一般情况下,数据绑定会变得棘手,因为必须避免循环——一次controls的改变,会改变数据集,接着又会改变controls,然后又改变数据集... 具体的操作流程会帮助解决这个问题,当界面展示的时候,把数据从会话状态加载到屏幕,在此之后的任何屏幕状态的改动都同步到会话状态。在屏幕已经显示的时候,把会话状态同步到屏幕状态的场景并不常见。所以如此,数据绑定并不是完全双向的-仅仅限制为初始化的时候加载数据然后把controls的改变同步回会话状态。

数据绑定可以完美的处理client-server程序的大部分场景。如果我改变了actual value (译注:指图形界面上的那个文本框),底层数据的对应列就会更新,甚至改变选择的站点(station,指图形界面上那个列表)会改变底层数据集中当前选择的行,还会导致其他的controls自动刷新。

这些行为都被框架内建了,框架关注通用的需求,并使之容易实现。它通过controls提供的叫做属性的设置方法实现( In particular this is done by setting values, usually called properties, on the controls)。Controls通过一个简单的属性编辑器设置列的名称从而绑定到数据集中相应的列。

使用数据绑定时,正确的参数化方式可以带你走很远,但是它不能带你完成整个路程-总是有一些逻辑是无法使用参数化的方案的。在这个例子中,计算差值(variance)就是无法使用框架内建的行为实现-因为它是程序特定的逻辑通常会在form中实现。

为了实现这种逻辑,form必须在actual field 改变的时候得到通知,这就需要这种通用的文本框去调用form中的某个特定方法。这比起拿到一个类库,通过调用它来使用它略复杂,因为控制反转被引入了。

有很多种方式能完成这种工作 - 对于client-server程序常用的一种是事件通知机制。每个控件都有一套它能触发的事件列表。任何外部的类都可以告诉控件它感兴趣的事件 - 这样当事件被触发的时候,控件可以通知外部的类。实际上,这是观察者模式( Observer )的一种重新措辞,在这里form观察着控件。框架通常会提供一种机制,这样开发者可以写一段代码,然后事件发生的时候被调用。至于事件和代码之间的联系如何实现在不同的平台上是不一样的并且对于本文这并不重要 - 要点在于有一种机制会使之工作。

一旦在form中写的那段代码被执行,它就可以做任何它想做的事情。它可以执行特殊的行为然后根据需要改变控件的状态,并依赖于数据绑定来把任何变化传递给回话状态。

这是必要的,因为数据绑定并不总是存在。在windows的控件库中存在很多,他们中并不是每一个都可以做数据绑定的。如果数据绑定不存在,那么就要依赖于form中的代码来完成数据同步。这可以通过在开始时从数据集中拉取数据设置在控件上,然后在点击保存按钮的时候把改变的数据再保存到数据集来实现。

让我们看下编辑actual value,假设这里的数据绑定是存在的。form对象直接持有controls的引用。每个控件都会有一个引用,但是此处为了简单,只写了这三个控件:actual(真实数值),variance(差值),target(目标值)。


Figure 2: Class diagram for forms and controls

文本框控件声明了一个文本变化的事件,当form初始化屏幕的时候会订阅这个事件,绑定到一个方法 - 这里是 actual_textChanged 方法。


Figure 3: Sequence diagram for changing a genre with forms and controls.

当用户改变actual value,文本控件会触发一个事件然后通过框架触发 actual_textChanged方法运行。这个方法拿到真实数值和目标数值,计算差值,然后设置到差值控件。它同时计算出差值控件的颜色并设置给控件设置合适的颜色。

用几段话来总结这个框架:

  • 开发者使用通用控件在form中完成App特有逻辑
  • form描述了控件的布局
  • form观察控件,通过处理方法来回应感兴趣的事件
  • 简单的数据编辑使用数据绑定来完成
  • 复杂的变化使用form的事件处理方法来完成

Model View Controller


或许,在UI开发中被最广泛的引用的一种模式是 ModelViewController(MVC)- 也是被最多的错误引用的。我已经忘记有多少次看到一些被称为 MVC 的设计,实际上一点也不像。坦白的说,导致如此的很多原因是经典MVC设计的部分实现对于现在富客户端(rich-client)来说毫无意义。但是现在我们会看一下它的原来面貌。

当我们看MVC的时候,需要记得这是第一次去尝试做一个认真的可以适应任何扩展的UI框架。在70年代图形用户界面并不是很常见。前面提到的”Form and Controls“框架是在MVC之后出现的 - 第一个讲它是因为它比较简单,但这并不是一种好的方式。同样,我也会用上面的评估案例讲一下Smalltalk80 的MVC - 但是注意我会随意的使用Smalltalk80的一些真实细节来描述 - 从一个单色的系统开始吧。

MVC的核心 ,一种对后来的框架影响深远的思想,我称之为 Separated Presentation。隐藏在 Separated Presentation 背后的思想是对领域对象(domain object)和表现对象(presentation object)的分层,前者是对真实世界的感知建模,后者是那些我们在屏幕上看到的GUI元素。领域对象是完全自包含的可以不引用表现层的情况下工作。他们也能够支持多种表现方式(可能是同时的)。这种方式也是Unix文化中重要的一部分,直到现在还能够让很多App同时通过图形界面和命令行界面操作。

在MVC中,领域元素被称作model。Model对象对UI元素是无知的。在开始讨论咱们的评估示例(译注:前面的冰淇淋评估示例)之前,我们会把model看成是reading对象,它包含所有我们感兴趣的字段(正如稍后可见,listbox的存在会让“what is the model”的问题变得更复杂,所以暂时会忽略它)。

在MVC中我会假定 Domain Model 是普通的对象,而不是前面 “Form and Controller” 章节中提到的 Record Set 。这反映了在这种设计背后一般化的假设。“Form and Controller” 模式中认为人们希望简单的操作关系型数据库中得数据,MVC模式假设我们想去操作普通的Smalltalk对象。

MVC的表现层部分是由剩下的两种元素组成的:view and controller(译注:这里的Controller和前面的controls不是同一个东西,前者是控制器后者是UI控件)。Controller的工作是拿到用户输入,然后决定去做什么。

从这个角度来说,我应该强调不是只有一个view和controller,而是对应界面上的每一个元素,每一个控件都有一对view-controller,而整个界面是一个整体。所以处理用户输入的第一个部分是各种在一起协作的controller去处理谁被编辑了。在这个例子中是 actuals text field,所以文本框的controller会处理接下来的事情。


Figure 4: Essential dependencies between model, view, and controller. (I call this essential because in fact the view and controller do link to each other directly, but developers mostly don't use this fact.

就像后来的编程环境,Smalltalk发现开发人员希望可以复用通用UI组件。这个例子中,这些组件是view-controller对。他们都是普通的类,需要被组织起来才能完成App特有的行为。应该有一个 assessment view 代表整个界面,并且定义其它低层控件的布局,从这个角度来看它和 “Form and Controller” 模式是有点相似的。不同的是,MVC框架中不会在 assessment controller 中实现低层UI组件的事件处理。


Figure 5: Classes for an MVC version of an ice-cream monitor display

text field 需要持有对应model的引用,reading 对象,并且告诉它当文本发生变化的时候应该调用哪个方法。在屏幕初始化的时候它被设置为 #actual: (在Smalltalk语言中#开头的表示变量名或者字符串)。text field controller 然后会接着调用reading对象的这个方法来做出改变。实际上,这个机制和数据绑定的机制是一样的,控件被绑定到低层对象(row),然后被告知操作哪个方法(column)。


Figure 6: Changing the actual value for MVC.

所以,没有一个全局的对象观察所有的低层控件,相反,低层控件观察数据模型,然后自己做出本来由form做出的决定。在这种情况下,当它要计算差值(variance)的时候,reading 对象本身就是一个很自然的地方来做这件事。

观察者模式确实在MVC中使用了,实际上这也是MVC的理念之一。在这个示例中,所有的views和controller都会观察model。当model改变的时候,view就会做出反应。在这个例子中,actual text field 被通知 reading 对象发生了改变,然后调用了 text field 委托的方法,在这个例子中就是 #actual 方法(设置文本颜色类似,接下来会讲到)。

你会注意到 text field controller 没有调用view的方法自己设置值,它更新 model 然后留给观察者机制去处理更新。这和“Form and Controls”中通过form更新控件然后依赖数据绑定来更新底层记录的方式颇为不同。这两种风格的同步方式我称之为:Flow SynchronizationObserver Synchronization。这两种模式描述了两种可以互相替换的同步屏幕状态和会话状态的方式。“Forms and Controls” 通过应用本身直接操作各种需要更新的控件来实现。MVC通过更新model然后依赖于观察者机制来更新观察此model的view。

在数据绑定模式不存在的时候,Flow Synchronization 被使用的更明显。如果App需要同步数据,那么它经常会在App流程中一些重要时刻来实现,比如打开新的页面或者点击保存按钮。

Observer Synchronization模式中一个额外的好处是,当用户操作一个特定的widget时,Controller可以完全不知道其他需要更新的widget的存在。但是form需要保持其它控件的引用,以便使整个屏幕状态在发生变化时保持一致,这在复杂的屏幕布局中会变得非常棘手。但是使用Observer Synchronization模式的Controller可以完全忽略这些。

如果有多个屏幕同时观察着一个model的时候,这种忽视变得非常便利。经典的MVC示例是一个类似电子表单的程序,它包含几个窗口,使用一个数据源分别展示几种不同的图例。电子表单的窗口不需要知道其它的窗口是否开着,它只需要改变model,Observer Synchronization 机制会处理剩下的事情。但是如果使用Flow Synchronization模式,每个窗口都需要知道其它窗口是否打开着,数据是否需要刷新。

虽然Observer Synchronization用起来很优美,但是它也有缺点。Observer Synchronization 的问题在于观察者模式本身 - 你不能通过阅读代码来获知究竟发生了什么。当我尝试去找出Smalltalk80的某些屏幕是如何工作的时候,我不由的想起了这些。我可以通过阅读代码走到这,但是一旦使用了观察者模式我只能通过调试器和跟踪状态的方式来弄清发生了什么。观察者模式难于理解和排错,因为它是隐式的行为。

通过查看时序图可以发现,不同的数据同步方式是非常明显的,但最重要的、也是最具影响的不同是MVC中SeparatedPresentation理念的应用。计算actualtarget之间的variance是领域行为,它和图形界面一点关系也没有。遵从 Separated Presentation 的结果意味着我们应该把这个逻辑放在系统的领域层 - 正巧是 reading 对象所代表的。当我回看reading对象的时候,variance在不需要用户界面搀和的情况下也变得非常有意义。

此时我们该看一些复杂的情况。现在有两个被略过的情况摆在面前(在MVC理论中略显尴尬)。第一个是处理variance的颜色设置问题。这不应该通过领域对象来做,因为我们展示的颜色值明显不是领域模型的一部分。我这儿所做的是对variance变量的一个定性的声明,我们可以定义为good(小于5%),bad(大于10%),normal(剩下的)。做出这个评估是领域层的事情,把它映射到颜色并改变variance field的值是view层的逻辑。现在的问题是我们应该把view层的逻辑放在哪儿 - 它不是标准 text field的一部分。

这种问题在早期的Smalltalk程序员中也经常面对,他们想出一个方案。上面我使用的一种方案是比较脏的一种 - 牺牲了领域层代码的单一性来完成工作。必须承认这次意外,但是我不会把它变成一种习惯。

我们完全可以照着 “Form and Controls” 模式来一遍 - 使 assessment screen 的 view 观察 variance field 的view,当variance field变化的时候,assessment screen 可以做出反应然后设置variance field的文本颜色。但是问题在于同样使用了更多的观察者机制 - 潜在的影响是这种机制用的越多,程序越复杂 - 并且导致不同view之间也相互耦合。

我比较喜欢的一种方式是,创建一个新的UI控件。实际上我们需要的是一个UI控件,它需要的领域层传来一个定性的值,它会和一张内置的value-color映射表做比对,然后设置合适的颜色。表和需要从领域拿到的值都需要在assessment view初始化的时候设置。就像它给 text field设置委托。如果可以方便的创建text field的子类添加额外的行为,那么这种方式就可以很好的工作。但是这明显依赖于平台的组件是否可以方便的继承 - Smalltalk 可以方便的实现 - 其他的环境可能会困难一些。


Figure 7: Using a special subclass of text field that can be configured to determine the color.

最后一步是写一个新的model类,它是面向screen的,但是依然是相对于widget无关的。他是专门为screen设计的model。方法和reading对象的方法是一样的,它仅仅是reading对象的代理,但是它会添加一些只和UI有关的逻辑,比如设置text color


Figure 8: Using an intermediate Presentation Model to handle view logic.

最后一种方案在好多种案例中都可以很好的工作,如我们所见,它成为了Smalltalk程序员中遵循的通用规则 - 我称之为 PresentationModel 是因为它真的是为了表现层而设计的一种model所以是表现层的一部分。

PresentationModel也可以很好的解决另外一个表现层逻辑问题 - PresentationState。基本的MVC概念认为所有的view状态都是可以从model中获取的,但是在这个例子中,我们怎么能从model中知道哪一行在listbox中被选取了呢?PresentationModel通过提供一个处理这种状态的地方解决了这个问题。还有一个类似问题,如果我们有一个按钮,它仅仅在数据发生变化的时候才能使用 - 同样,它是我们和底层model交互的时候产生的问题,而不是model自身。

所以现在是该总结一下MVC的时候了.

  • 明确的分开表现层(view&controller)和领域层(model)- Separated Presentation
  • 把 GUI widgets 分成 controller(用来处理用户输入) 和 view (用来展示model的状态)。Controller 和 view不应该(大部分情况下)直接的通信而是通过model。
  • 让views(and controller)观察model 以便允许多个widget都可以更新而不需要直接通信 - Observer Synchronization

VisualWorks Application Model


就像前面所说的一样,Smalltalk80的MVC影响深远并且有很多优秀的特性,但是依然有缺陷(译注:MVC假设可以从model中拿到view的所有状态,为了解决这个问题,前面采用了PresentationModel来解决之)。随时Smalltalk在80、90年代的发展,原有的经典MVC架构发生了很大变化。事实上,如果你认为view/controller分离是MVC中比较重要的部分的话(which the name does imply),也可以说MVC已经消失了。

MVC中依然可以明确的工作的特性是 Separated PresentationObserver Synchronization。所以随着Smalltalk的发展,这些特性依然存在 - 事实上对于大部分人来说这才是MVC的关键。

这些年以来Smalltalk语言也开始碎片化。Smalltalk语言的基本思想,包括语言的设计理念(最小化的)依然存在,但是我们看到很多Smalltalk使用不同的库开发。从UI的视角来看这变得很重要,因为很多库开始使用原生组件 - 即在 “Form and Controls” 模式中使用的控件。

Smalltalk最开始是在施乐帕克实验室(XeroxParc)开发的,现在分离出一个新的公司 - ParcPlace - 来专门开发和拓展Smalltalk语言。ParcPlace版本的Smalltalk被称作 VisualWorks, 特点是可以跨平台开发。早在Java出现之前,你就可以用一段在windows上写的Smalltalk程序运行在Solaris系统上。导致的一个结果是VisualWorks不能使用原生的UI组件,在语言内部维护一套自己的GUI库。

在我讨论MVC的时候,我解决了一些MVC中存在的问题 - 特别是如何处理 view 层的逻辑和状态。VisualWorks 重构了它的架构以便可以很好的处理这种问题,它引入了一种叫做应用模型(ApplicationModel)的结构 - 一种倾向于Presentation Model 的结构。在VisualWorks中使用 Presentation Model 的理念并不新鲜 - 和原有的Smalltalk80的代码浏览器(code browser)非常像,但是VisualWorks的应用模型把这种理念嵌入了框架里面。

这种Smalltalk的关键点是把属性(properties)改成了对象(objects)。一般我们对对象和属性的认知是如果有一个对象是Person,那么它可以有nameaddress 属性。这些属性可以是字段(fields)但是也可以是其他的东西。常见的访问属性的约定是:在Java中 temp = aPerson.getName()aPerson.setName("martin"),在C#中 temp = aPerson.nameaPerson.name = "martin"

属性对象(PropertyObject)的概念通过返回一个封装了真实值的对象改变了这种认知。所以在VisualWorks中当要获取name的时候会返回一个对象。然后会从封装它的对象中拿到真实的值。所以访问Person的name属性变成了 temp = aPerson name valueaPerson name value: 'martin'

属性对象使UI组件(widget)和model之间的映射关系变得非常简单。只需要告诉UI组件去发送什么样的消息可以拿到对应的属性,然后UI组件(widget)知道如何通过 valuevalue:来访问合适的值。VisualWorks的属性对象也允许通过onChangeSend: aMessage to: anObserver 消息创建一个观察者。

在VisualWorks中实际上是找不到一个叫属性对象的类(class)的。而是很多类都会遵循 value/value:/onChangeSend:协议。最简单的例子是 ValueHolder - 一个只封装了真实值的类。和当前讨论更相关的是 AspectAdapterAspectAdapter允许一个属性对象完全的把另一个对象封装成属性。如此,你就可以在 PersonUI 类中定义一个属性对象,然后把 Person 类通过下面的代码封装成一个属性:

adaptor := AspectAdaptor subject: person
adaptor forAspect: #name
adaptor onChangeSend: #redisplay to: self

看一下应用模型(ApplicationModel)是如何和当前例子结合的。


Figure 9: Class diagram for visual works application model on the running example

使用应用模型(ApplicationModel)和MVC的主要不同是在领域模型(domain model)类(Reader)和UI组件(widget)之间多了一个协调类 - 这个类就是应用模型(ApplicationModel)。UI组件不会直接访问领域的对象 - 他们对应的对象是应用模型(ApplicationModel)。UI组件依然被划分成views和controllers,但是除非你创建新的UI组件否则这个区别变得不重要了。

当你组织UI的时候,会在UI painter中完成,在这里你可以给每个UI组件设置 aspect。每个 aspect都对应一个在应用模型(ApplicationModel)中的方法,它会返回一个属性对象。


Figure 10: Sequence diagram showing how updating the actual value updates the variance text.

上图展示了基本的更新时序。当我改变text field的值的时候,text field会更新应用模型中属性对象的值。接着会更新底层领域对象,再更新真实的值。

这时观察者模式进入了。应该先建立起关系,这样改变 actual 的值的时候会引起 reading 对象发布一个改变事件。我们通过在 actual的修改器(modifier)中放一个回调(a call)来告诉 reading 对象它改变了 - 特别是 variance aspect 发生了变化。当给 variance对象设置 aspect-adapter的时候,可以方便的让它去观察 reading对象。所以它会收到一个更新消息然后发送给对应的text fieldtext field 然后通过 aspect adapter拿到最新的值。

使用应用模型(ApplicationModel)和属性对象(property-objects)帮助我们把更新逻辑衔接起来而不用写很多的代码。它也能提供细粒度的同步(但是我不认为是好事)。

应用模型(ApplicationModel)允许我们把UI层的行为和状态从领域逻辑中分离出来。对于我之前提到的一个问题 - 如何持有当前选择的行 - 可以通过提供一个 aspect adapter 封装领域模型的数据集并且保存当前选择的行来实现。

唯一的限制是,对于更复杂的行为,你需要创建特殊的UI组件和属性对象。比如之前的例子中,领域模型中并没有提供文本颜色(text-color)和variance变化程度之间的关联。分离应用模型和领域模型确实可以用正确的方式分开决策,但是为了让UI组件(widgets)观察 aspect adapter 需要定义新的类。但是这么做往往需要写更多的代码,为了简单,可以通过让应用模型直接访问UI组件来实现,如图:


Figure 11: Application Model updates colors by manipulating widgets directly.

像这样直接更新UI组件并不是 Presentation Model 的一部分,这就是为什么 VisualWorks 的应用模型(ApplicationModel)并不是真的 PresentationModel。这种需要直接操作UI组件的方式是一种比较脏的妥协但是它衍生另一种模式 Model-View-Presenter。

现在该总结一下ApplicationModel模式了:

  • 遵从MVC,使用了 Separated PresentationObserver Synchronization 模式。
  • 引入了中间的应用模型(ApplicatonModel) 来处理表现逻辑和状态 - 是Presentation Model 的一部分。
  • UI组件不再直接观察领域模型,而是观察应用模型。
  • 大量使用属性对象来连接不同的层并通过观察者模式来实现细粒度的数据同步
  • 在应用模型中操作UI组件并不是应用模型原有的设计,但是在复杂的情况下,这种使用非常普遍。

Model-View-Presenter(MVP)


MVP模式最开始出现在IBM,在90年代的时候由Taligent提出才被人熟知。它在Potel论文中被普遍引用。这种思想被Dolphin Smalltalk的开发人员进一步的完善和阐述。虽然前后的描述并不完全一致,但是这种模式的低层思想还是很受欢迎的。

为了理解MVP,可以先先思考一下两种截然不同的UI思考方式。一种是 ”Form and Controller“ 架构,它是主流的UI设计方式(译注:06年左右),另一种是MVC和它的衍生版本。”Form and Controller“ 架构提供一种容易理解的架构,并且在可复用组件和App特有逻辑之间做了很好的分层。它缺少的但是MVC更擅长的是 Separated Presentation,而且程序是基于 Domain Model 编程的。我发现 MVP 更进一步的整合这些,并把各自的优点发挥到极致。

Potel所提到的第一种理念是把view看做是一组 widgets 的集合(a structure of widgets), widgets 就是 "Form and Controls" 中的控件(移除view/controller)。MVP中得View就是一组widgets构成的结构。它不包含任何描述响应用户手势的代码。

对用户的响应代码存在于一个分开的presenter对象。最基础的处理用户手势的操作还存在于widget中,但是会把最终的控制权还给presenter。

Presenter决定如何响应事件。Potel主要从model的角度讨论了这些交互,具体就是通过一个由命令(command)和选择(selections)组成的系统。一个需要强调的比较有用的事情是MVP会把对model的所有编辑打包在一条命令里面 - 这为处理 undo/redo 这种行为提供一个非常好的功能。

当 Presenter 更新 model的时候,view 还是通过 MVC的
Observer Synchronization 方式更新的。

Dolphin的描述类似。同样,主要的相同点是 presenter 的存在。在 Dolphin 的描述中presenter不会通过命令和选择的方式操作model。但是有明确的讨论presenter会直接操作View。Potel并没有说presenter是不是该这么做,但是在Dolphin看来,这种操作可以克服应用模型中(ApplicatonModel)中存在令人尴尬的处理文本颜色的问题(译注:在ApplicatonModel直接修改widget,因为ApplicatonModel本来是不应该引用widget的)。

在MVP中容易引起理解不一致的一点是presenter应该控制view中widgets的程度。一种观点是所有的view逻辑都应该在view中处理,presenter不应该插手如何渲染model - Potel 是这么认为的。Bower and McGlashan 提出一种方式被我称之为 Supervising Controller,view处理一些可以简单描述的view逻辑,pesenter负责处理复杂的情况。

你可以让presenter去操作所有的widgets,这种方式我称之为 Passive View并不是最早的MVP但是在处理测试问题时发展了这种方式。稍后会讨论这种模式,它是MVP模式风格的一种。

在我开始比较MVP模式和之前的几种模式之前,需要提一下前面的两篇MVP论文也都做了比较 - 但是和我的解释有所不同。Potel暗示MVC的Controller是一个整体的协调器 - 这并不是我理解的那样。Dolphin讲了很多MVC的问题,但是他说的是VisualWorks的ApplicatonModel模式而不是原始的MVC(我并没有责怪他们,因为获取早期经典MVC的信息并不容易,所以先放一边)。

现在该做一些比较了:

  • Form And Controls:MVP有底层model,Presenter会通过Observer Synchronization 模式操作model来更新view。尽管可以直接访问widget,但是这并不能成为除了使用model来更新之外的首选。
  • MVC:MVP使用一个Supervising Controller来操作model。Widgets会把用户操作转交给 Supervising Controller。Widget没有被分成 view和controllers。你可以把 Presenter看成是没有处理原始用户手势功能的controller。另外非常重要的是 presenter 是处在 form 层级的而不是 widget层级的,这是和MVC中Controller一个更大的不同。
  • ApplicationModel:views把事件转交给presenter这和ApplicationModel类似。但是view可以直接通过Model更新,presenter并不是Presentation Model的角色。另外,在不方面使用Observer Synchronization模式的行为中,presenter默认是可以直接访问widgets的。

MVP的presenter和 MVC的controller有着明显的相同,presenter是一种更松散的Controller。导致的一个结果是很多设计遵循的是MVP模式但是却使用 'controller' 作为 presenter 的名称。当我们讨论用户输入的时候,对于广泛的使用 controller 有一个合理的辩解。


Figure 12: Sequence diagram of the actual reading update in MVP.

现在来看 MVP(Supervising Controller)版本的冰淇淋监控程序。它开始的时候和 FormAndControls 模式很像,actual text field 在文本发生变化的时候发布一个事件,presenter监听这个事件,并拿到文本。然后presenter更新reading对象,然后 variance text field 观察并更新自己的值。最后一步是设置variance text field的颜色,这是 presenter 做的。它从 reading对象中读取分类然后更新variance field的值。

这里是MVP模式的总结:

  • 用户手势(译注:现在认为应该译成动作)通过 widgets 被转移到 Supervising Controller
  • presenter 协调 domain model 中得变化
  • MVP的不同版本处理view的方式不同。从Observer Synchronization 到使用 presenter 来实现所有的更新。

Humble View


在过去几年越来越流行可以自测的代码。尽管我不太追逐潮流,但是我还是沉浸在了这场运动中。我的很多同事都是xUnit框架的粉丝,自动的回归测试,测试驱动开发,可持续集成还有很多类似的商业用语。

当人们讨论可以自测的代码的时候,用户界面给他们提了一个难题。很多人发现测试UI即困难又不现实。这主要是因为UI和系统的UI环境耦合太紧,不易分离测试。

有时候这种测试困难是被高估了。通过创建自己的widgets,在测试代码中操作他们可以迈出很长的一步。但是有些情况这是不可能的,错过重要的交互,存在线程问题,测试运行缓慢。

这样导致了一个结果,在设计UI的时候尽量在不易测试的对象中减少行为。Michael Feathers 在The Humble Dialog Box这篇文章中总结了这种方式。Gerard Meszaros 把这种认知规范化为HumbleObject - 任何难以测试的对象都应该最小化行为。这样,如果无法测试的话,就可以最小化失败的可能。

The Humble Dialog Box论文中使用了MVP模式,但是比原始的MVP模式更进一层。presenter不仅仅处理用户输入,同时负责数据填充,这样,widget不在需要知道model。它形成了被 presenter操作的 Passive View

这不是唯一的使UI变得简单的方式。另外一种方式就是使用 Presentation Model,尽管这样还是需要在widget中做一些逻辑,但是对于widget来说已经足够把自己映射到 Presentation Model 了。

这两种方式的关键是测试presenter或者测试presenter model,你可以在不碰那些难以测试的widget之前测试到大部分的UI问题。

使用 Presentation Model 模式可以把大量真实的逻辑放在 Presentation Model 里面。所有的用户输入和现实逻辑都路由到 Presentation Model,这样所有的widget需要做的就是把自己映射到 Presentation Model 的属性上。然后你就可以测试Presentation Model 的大部分逻辑而不需要现实任何界面 - 唯一的风险存在于 widget映射。但是这些操作很简单可以不测。在这个例子中屏幕还是没有使用 Passive View 来的简单,但是区别也很小了。

因为 Passive View 使 widgets变得太简单了,甚至不存在映射,Passive View 甚至消除了 Presentation Model 模式中存在的风险。代价是你需要一个 Test Double 框架来模拟屏幕 - 这是你需要额外构建的机制。

Supervising Controller模式中也存在这种折衷。让每个view做简单的映射会引入风险(和Presentation Model类似)但是带来的好处是可以简单明确的声明映射。在 Supervising Controller 中映射会比较少,因为在 Presentation Model 中还要处理复杂的更新问题。而 Supervising Controller 只要在内部操作widget就行了而不需要映射回去。

PS:最后一段比较难以理解附上原文:

A similar trade-off exists with Supervising Controller. Having the view do simple mappings introduces some risk but with the benefit (as with Presentation Model) of being able to specify simple mapping declaratively. Mappings will tend to be smaller for Supervising Controller than for Presentation Model as even complex updates will be determined by the Presentation Model and mapped, while aSupervising Controller will manipulate the widgets for complex cases without any mapping involved.

PPS: 有很多理解不到位或者难于翻译的地方都加了备注。
PPPS:Android平台上并没有给出比较完善的App架构设计指南,基本上都是摸石头过河,滥用各种MVC\MVP\MVVM希望这篇可以给迷茫中得同学带来一些帮助。如有条件还是看看原文!!

日记本