GUI架构


原文地址:https://www.oschina.net/translate/gui-architectures?lang=chs&page=1#

英文地址:http://martinfowler.com/eaaDev/uiArchs.html

无论是对于用户还是对于开发者而言,图形用户界面早已成为软件领域中人们习以为常的部分了。从设计角度看,GUI代表着系统设计中一组特殊的问题 —— 这些问题具有大量不同但却又有些相似的解决方案。

我的兴趣是为应用程序开发者找出对于富客户端开发有用的通用模式。我已经在许多项目评审中看到过多种不同的设计方案,而且还也见到过许多以更加易于保留下来的方式来描述的设计方案。在这些设计方案中存在着很有用的模式,但是,将模式描述出来通常都不太容易。以模型-视图-控制器为例来说,人们常常把它称为一种模式,但是我发现把它看作一种模式并不是特别有用,因为在它里面包含着许多不同的理念。在不同的地方读到MVC时会从中得到不同的理念,而且人们还会把这些不同的理念都描述为‘MVC’。如果这还不足以造成很多困扰的话,通过“传话游戏”终将造成人们对MVC误解的结局。

在本文中,我想对许多很值得关注的架构进行探讨,并为它们最值得关注的那些特性给出我的解读。我希望这样将可以为人们能够理解我所描述的模式提供一个恰当的语境。

在一定程度上你可以将本文看作一种用来追溯多年来各种架构下UI设计理念的思想史。然而,我在此必须提醒大家注意,架构理解起来绝非易事,特别是很多架构还在不断的变化和消亡中。要追溯出理念的传播脉络更是难上加难,这是因为同一个架构不同的人有各自不同的解读。尤其是我并未对我所描述的架构进行完全彻底的研究。我所做的只是对它们的设计进行了一般性的描述。如果这些描述有所疏漏的话,我绝对是无意的。所以,请不要将我的描述看作是这些架构的权威性说明。而且,我还舍弃或精简了我认为并不怎么切题的内容。请记住,我所关心的主要是底层的模式,而不是这些设计的发展史。

(这里有个例外,我有一个能够运行起来的Smalltalk-80环境让我可以了解其中的MVC。还得说一次,我并不认为我对MVC的调查已经做到了非常彻底的程度,但我的调查已经揭示出许多对MVC比较常见的描述中并没有给出的内容 —— 这让我进一步对我在本文中给出的对其它架构的描述保持警惕。如果你熟悉这些架构中的某一个架构并且看到有些重要的东西我给弄错了或者遗漏了,恳请告知。我认为,这个领域还可以作为一个很好的学术研究对象,加以更为详尽的调查研究。)

表单和控件


我将从对大家来说即简单又熟悉的架构开始进行探讨。这个架构还没有一个通用的名称,为了便于在本文中进行讨论我暂且将它们称为“表单和控件”。这个架构之所以为大家熟知,是因为早它在上世纪90年代的Visual Basic、Delphi和Powerbuilder等等类似的工具中备受推崇,直到现在仍为大家广泛使用,虽然也常为象我这样的设计狂所诟病。

为了对包括这个架构在内的所有架构进行探讨,我将使用一个共同的例子。在我所居住的新英格兰,有一个用于监测空气中冰激凌微粒数量的政府项目。如果冰激凌微粒的浓度太低,就表明我们吃的冰激凌不够多,这对我们的经济以及公共秩序而言是一种严重的风险因素。(我所喜欢使用的例子同你通常在此类技术书中看到的例子相比来说会更加真实。译者注:这恐怕是作者在开玩笑

image

图1:本例所使用的UI

但我们看到这个界面时,我们会看到整个界面中有一个重要的分界线。该表单是我们的应用所专用的,但它却用到了很多通用的控件。绝大多数GUI环境带有大量的通用控件,我们可以之间将它们用在我们的程序中。我们还可以创建自己的控件,而且创建自己的控件往往还是个好主意,但是通用的可重用控件不同于专用的表单。即使专门为达到某个目的而编写的控件也可以重用于多个不同的表单。

该表单具有两个主要的职责:

  • 界面布局:用来定义控件在界面中的位置安排以及控件之间的层次结构。
  • 表单逻辑:完成那些在编程中很难放到控件本身之中的行为。

大部分GUI开发环境都会有一个图形化的编辑器,开发者可以在该编辑器中将控件拖拽到表单中,以此来完成对表单中大部分的界面布局的定义。采用这种方式,很容易就能够在表单中设置出一个讨人喜欢的布局(虽然这并不总是设置布局的最佳方式,我们会在下文中对此再次进行讨论。)

控件是用来显示数据的 —— 在这个例子中我们要显示的数据都是同工作人员读到的微粒值相关的数据。这些数据相当一部分总是来自其它的某个地方,在这个例子中让我们假设它们来自一个SQL数据库吧,这同大多数客户端服务器工具所假设的相一致。在大多数情况下,都会涉及到3份数据:

  • 一份数据保存于数据库之中。这份是数据的持久性记录,所以我将它称为记录装态(record state). 记录状态通常以各种不同的机制由多人来共享并以对多人可见。
  • 更进一步,还有一份拷贝保存于应用程序内存中的记录集(Record Sets)之中。大部分客户端-服务器环境都提供了相应的工具可以帮助你很简单地得到记录集。这种数据之同应用程序和数据库的一次会话相关,所以我把这样的数据称为会话状态(session state)。 这为用户提供了数据必不可少的临时性本地存储,用户可以对这些数据进行各种处理,直到将它们保存或者提交回数据库 —— 此时这些数据就同记录状态合并到了一起。本文我将不会对记录状态和会话状态之间的协调问题进行讨论;我在[P of EAA]中对该问题所涉及的各种技术进行了探讨。

保持screen state和session state同步是很重要的。对此有个很有用的手段Data Binding(数据绑定)。基本的思路是:无论是控件表现或是控件底层的数据集,任一方发生了改变都立即把改变传播给另一方。所以如果我改变了选择的station,那么文本框控件(station id)也会跟着改变(由于底层的数据集发生了改变)。

实际上数据绑定是有点棘手的,因为你需要避免循环,比如控件状态改变了,会导致数据集的改变,数据集的改变又会更新控件导致控件状态改变…..。要避免这种情况,我们要遵循如下的使用流程:界面初始化时从session state加载数据,之后任何界面上的改变(screen state)都传播给session state. 而当底层的数据集改变后立即更新桌面控件的状态通常是不常见的,所以数据绑定可能不是完全双向的-仅限于初始的上传(指数据从session state 传到 screen state),之后数据的改变会从控件传播到session state.

数据绑定将一个客户端服务器应用程序的大部分功能处理得很漂亮。如果我改变了被更新列的实际值,甚至只是改变了选择的目标,记录集中当前所选行也会跟着变,它也会导致其它的控件的刷新。

这些行为中的大多数都是内置于框架的构造器中的,它们专注于普遍的需求,并且让这些需求很容易被满足。这是通过修改控件的特定值做到了,这些值一般被叫做控件的属性。通过一个简单的属性编辑设置其列名,从而使控件被绑定到一个记录集的特定列。

使用合适类型的参数化来进行数据绑定,可以让你走得更远。然而它并不总是会合你得意——总是有一些逻辑不能同参数化的选项相匹配。这样的场景,像计算方差(variance)就是一个不能匹配内置行为的例子——因为应用程序使它具体的依赖于形式了。

为了实现数据绑定,在实际字段的值发生变化时,就需要通过某种形式来通知表单,这就要求通用的文本框要调用表单上的某个特定行为。这个过程远比采用类库然后通过控制反转来调用这个特定行为的情况要复杂得多。

实现这种调用的方法有很多种 —— 客户端-服务器工具箱中最常见的就是采用事件的概念。每个控件都有一个它可以激起的事件列表。任何外部对象都可以告诉控件它对某事件感兴趣 —— 然后在发生这种事件时控件就会调用该外部对象。本质上讲,这只是观察者(Observer)模式的另一种说法而已,此时表单就是控件的观察者。通常框架会提供某种机制让开发者可以用它来编写一个子程序,然后在相应的事件发生时该子程序就会得到调用。到底在事件和子程序之间的关联关系是怎么建立的,对于我们这里的讨论来说并不重要,而且其建立的方式也会随平台的不同而不同 —— 这里的关键在于的确存在某种能够让这一切发生的机制。

一旦表单上的子程序获得控制权,它就可以为所欲为了。该子程序可以在执行特定的行为后,再按照需要对控件的显示内容进行修改,随后再依赖数据绑定机制将控件中的数值改变传递回会话状态。

因为并不是总是有数据绑定可用,所以这么做也很有必要。Windows控件的市场非常大,但并不是所有的控件都提供了数据绑定功能。如果没有数据绑定功能,完成同步任务的责任就非表单莫属了。这可以通过刚开始时从记录集中将数据中拿到数据并将数据设定到界面组件中,然后在保存按钮按下后将修改后的数据保存回数据库中来实现。

下面假设在数据绑定功能情况下,让我们详细看看对实际数值的编辑过程。表单对象保存了一个直接指向通用控件的应用,而且为屏幕上的每个控件都保存了一个这样的引用,但是,这里我所关心的只是实际字段、 变化字段以及目标字段。

image

图2:表单和控件的类图

文本域为文本的改变声明了一个事件,当表单在初始化期间组装屏幕时,它自身领取了那个事件,将一个方法绑定到其自身的——这里是一个actual_textChanged(实际值文本变更)。

image

图3:使用表单和控件改变样式(genre)的序列图

当用户改变实际值得时候,文本域控件就会提升其事件,并且通过框架的魔术使得actual_textChanged的绑定得以运行。方法从实际的目标文本域中得到了文本值,做一些截取(subtraction),而后把这个值放到变化域(variance field)中。它也会计算出这个值应该用什么颜色来显示,而去适应恰当的文本颜色。

我们能用一些简评来总结该架构:

开发者编写使用了通用控件的应用程序定制表单。

表单描述了其控件的布局。

  • 表单监听着控件,并且有处理方法针对控件提交上来的感兴趣的事件作出响应。
  • 简单的数据编辑通过数据绑定受到处理。
  • 复杂的变更则在表单的事件处理方法中完成。

模型视图控制器(Model View Controller)


在UI开发领域被最广泛引述的模式很可能就是MVC(Model View Controller)了——它也是最为人所不确切引用的。我已经计算不出来我有多少次看到些一些东西被描述成为MVC,而最终被证明一点都不像MVC了。坦率点讲,发生这种情况许多是部分由于传统的MVC不怎么真正理解现在的富客户端。而目前先让我们来瞧一瞧它的起源。

我们看待MVC时,有一点很重要,那就是这是在任何规模上进行严肃的UI工作的首次尝试之一。图形用户界面在上个世纪70年代还并不怎么普及。我前面刚刚描述过的表单和控件模型出现在MVC之后——我首先描述它们是因为它们更加简单——但是请注意,我有意通过进行一些针对Smalltalk80实际细节上玩味来进行这一描述 —— 一开始它只是一个单色(monochrome)系统。

MVC的核心所在,还有对后来的框架起到最大影响的创意,就是我所谓的分离表现(Separated Presentation)。分离表现背后的观点是让对我们意识中的真实世界建模的域对象(domain object),与我们在屏幕上看到的GUI元素的展现对象(presentation object),这两者之间有一个清晰的分离。域对象应该完全独立的,并且工作时不涉及到展现,同时可能的话,它们应该能够支持多种展现。这一方法也是Unix文化中的一个重要部分,而在今天的许多应用程序仍然继续可以同时通过图形方式和命令行方式的界面进行操作。

在MVC中,域对象被当做模型(model)被引用。模型对象完全不用去管UI。为了开始讨论我们所评估的UI示例,我们将使用一个读取模型,带上所有它感兴趣数据的域(field)。(正如我们待会儿将要看到的列表框,使得什么事模型这一问题变得更加复杂,但我们将忽略那个列表框的一些问题。)

在MVC中我假定域模型(Domain Model)为常规的对象,而不是我在表单和控件中记录集(Record Set)的概念。这反映了设计背后的一般假设。表单和控件假设大多数人想要方便的操作来自关系型数据库中的数据,MVC假定我们我们操作的是一般的Smalltalk对象。

MVC的展现部分由两个主要的元素组成:视图(view)和控制器(controller)。控制器的作用是获取用户的输入和决定拿它(指输入的数据)来做什么。

在这一点上我应该要强调的是不仅仅只有一个视图和控制器,对于屏幕上面的每一个元素,你都应该有一个视图-控制器对,每一个控件和屏幕是一个整体。因此对于用户输入响应的首要部分是不同控制器协作,来看谁获取获得了编辑。在事实上是文本域的时候,文本域控制器会来处理接下来将会发生什么。

image

图4:模型、视图和控制器之间的必要依赖。(我称这为必要的,是因为事实上视图和控制互相之间是直接链接起来的,但是开发者大多数都不适用这个事实。)

像后来的环境中,Smalltalk算准了你想要能够被服用的通用UI组件。在这种情况下组件应该会是视图-控制器对。都是通用的类,因此需要插入到应用程序特定的行为中去。应该要有一个可以代表整个屏幕和定义低级控制布局的评估视图(assessment view),那样子就同表单和控件中的表单类似了。然而不同于表单,MVC在低级组件的评估控件(assessment controller)上没有时间处理器(event handler)。

image

图5:一个ice-cream模拟器展示的一种MVC版本的类图

文本域的配置来自给其赋予的指向其模型的链接,当文本发生改变是,读取,并且告诉它什么方法应该被调用。这里是设置成了’#actual’:当屏幕已经被初始化时(在Smallta中,打头的‘#’声明一个符号(sysbol),或者是内部的字符串(interned string))。然后文本域的控制器就会回调那个方法以做出改变。重要的是这同数据绑定(Data Binding)所发生的状况是同一种机制,控件被链接到底层的对象(行),并被告知它所操作的方法(列)。

image

图6:改变MVC的实际值

因此没有什么全局对象会去观察低级的小部件(low level widget),而是低级部件在观察着模型,它自身会处理或许应该由表单(form)做出的许多的决定。在这种情况下,当它要计算出方差时,读取对象自身是很自然的处理场所。

观察者(Observer)确实在MVC中,它确实是MVC的创意之一。这里所有的视图和控制器观察着模型。当模型发生改变时,视图会做出反应。这种情况下实际的文本域视图会被提醒读取对象已经发生了改变,而去调用为文本域定义的作为切面(aspect)的方法——这种情况下实际上是#——将它的值设置到了结果之中。(它做了一些同颜色情况类似的事情,但这里提升我稍后会涉及到的它自身的理念。)

你将会注意到文本域控制器并不设置视图自身中的值,而是它去更新模型,然后让观察者机构去接管这些更新。这和表单和控件的方式存在相当大的不同,表单更新控件之后依赖数据绑定去更新底层的记录集。我将这两种类型描述成模式:流同步(Flow Synchronization)和观察者同步(Observer Synchronization)。这两种模式描述了处理在屏幕状态和session状态之间触发同步的可选方法。表单和控件通过应用程序控制的需要直接被更新的各种控件的流来实现。MVC则借助在模型上进行更新,然后依赖观察者关联去更新正在观察着那个模型的视图,来做到这一点。

当数据绑定不存在时,流同步表现得更加明显。如果应用程序需要同步自身,那么它通常会是在应用程序流中非常重要一点——例如打开一个屏幕,或者点击保存按钮。

观察者模式的意义之一是控制器在用户操作一个特定的部件(widget)时,可以一点也不用去管其他需要一并改变的部件。表单在需要保持事物上的标签并且确保整个屏幕状态在改变上保持一致,这样使得诸多复杂的屏幕可以相当漂亮的加入其中,观察者模式中控制器使得我们可以忽略所有这一切。

如果有多个屏幕打开来显示同一个模型对象,这种实用的忽略就会变得特别方便。经典的MVC实例是一个像带有针对同一数据的一对分属单独窗口的不同图表的电子表格。电子表格窗口不需要去知道还有没有其它窗口开着,它只需要改变模型,其余的事情就交给观察者同步去打理。使用流同步的话就需要某种方法去获知有没有其它窗口打开,以便告知它们需要刷新。

虽然观察者同步很好,然而它确实存在缺点。 观察者同步的问题是观察者模式自身的核心问题-即你不能通过阅读代码就说出什么将发生。当我试图确定某些Smalltak 80屏幕是如何工作的时候,我就非常强烈地想起这个问题。通过阅读代码我能知道很多,不过一旦观察者机制不采用这一种唯一的方式,我就能通过调试器和追踪语句看到正在运行的一切。观察者的行为是很难理解和调试的,因为它的行为是内在固有的行为。

虽然从序列图就可以看到同步方式的差别特别明显,然而最重要,最有影响力的差别是MVC使用了分离展现。计算实际值和目标值之间的差值是域的行为,与用户界面没有任何关系。因此,正如分离展现所示,我们应当把它放在系统的域层-它才真正是读取对象所要表示的内容。当这时我们看看读取对象的时候,差值特性在没有任何用户界面概念的情况下就非常清楚了。

然而,这时我们可以开始看看一些复杂的问题。我已跳过了一些令人困惑并且阻碍理解MVC理论的地方,这样的地方有两处。问题的第一处是如何设置色彩变量。这实际上不应当是在域对象里,因为我们用来显示差值的色彩不是域的一部分。处理这个问题的第一步是认识到这个逻辑的一部分是域逻辑。这儿我们正在做的事情是给这个变量一个定性的描述,我们称它为好(低于5%),坏(超过10%),一般(其余情况)。做这样的评估当然是属于域语言,而把这个映射到色彩和更改差值域则是视图逻辑。问题是我们把视图逻辑放在什么地方-它不是标准文本域的一部分。

这种问题早期的smalltalk开发者也碰到过,而且他们提出了一些解决方案。上面我所展示的方案是一个模糊不清的方案 —为了使事情正常运作而伤害了域的某些纯粹性。我允许了临时的混合的行为-不过我将尽量不养成这个习惯。

我们可以做相当多表单和控件做到的事情-让评估屏幕视图看到差值域视图,当差值域更改的时候,评估屏幕能做出反应并设置差值域的文本颜色。这儿的问题仍然包括更多的使用了观察者机制—你使用的越多就会陷入指数形式增长的复杂里—和不同视图之间的连接。

我优先选的方法是构建一个新类型的UI控件。实质上我们需要的是这样的UI控件:它向域请求定性的值,把这个值与某些值和色彩的内部表相比较,然后以此来设置字体颜色。向域对象请求的表和消息在评估视图形成的时候将由评估视图自身去设置,同时它也为监控这个域而设置了外观。如果我们能够仅仅增加其他行为而很容易的子类化文本域,那么这种方法就运行的非常良好。这明显地取决于设计的组件启用子类化的程度-Smalltalk让这种方法变得非常简单-其他环境则让这中方法变得更加困难。

image

图7:使用了文本域的可配置为决定色彩的特殊子类

最后的路线就是产生一种新的模型对象,它是面向于围绕屏幕的,但仍然是独立于部件。它应该是在屏幕上的模型。同那些在读取对象上的一样的方法,应该只是被委派于读取逻辑,但是它也会添加方法来支持只和UI相关的行为,比如文本的颜色。

image

图8:使用中间展示模型(Presentation Model)来处理视图逻辑。

这最后一个选择在许多情况下工作得很好,但是,像我们将会看到的,这成为了一种Smalltalk用户共同遵循的路线——我将这称之为展示模型(Presentation Model),因为它是一种真正为那些展示层而设计的模型。

展示模型在另外一个展示逻辑问题上也能起到很好的作用——这就是展示状态。基础的MVC概念假设视图的所有状态能够从模型的状态中获取。在这种情况下我们如何去计算出列表框中那一项被选中了呢?展示模型通过为我们提供一个场所放置这种类型的状态,给我们解决了这个问题。类似的问题发生在当我们有一个只在数据被改变时可用的保存按钮——那也是一种关于我们所交互的模型的状态,而不是模型自身。

因而现在我想该是为MVC作一些阐述的时候了:

  • 展现(视图&控制器)与域(模型)之间应该有明显的分离——分离展现。
  • 将GUI部件分成控制器(用来对用户刺激做出反应)和视图(用来展示模型的状态)。控制器和视图应该(大多数时候)不能直接联系,而是通过模型。
  • 用模型(和控制器)来观察模型,以允许多个部件在没有直接联系的前提下获取更新——观察者同步。

可视化作业应用程序模型(VisualWorks Application Model)


如我上述所论,Smalltalk 80的MVC非常有影响力,拥有一些优秀的特性,但也有一些错误的地方。在80和90的Smalltalk开发之间就经典的MVC模型已经导致了一些重要的分歧。如果你考虑到视图/控制器分离是MVC的一个重要组成部分,某些人就这个名字的意义,确实几乎可以断定MVC已经消亡了。

来自MVC真正起作用的东西是展示分离和观察者同步。因此这些都在Smalltalk中保留着——对许多人来说它们确实是MVC的关键要素。

这些年Smalltalk也变得支离破碎起来。Smalltalk的基本理念,包括(小型化的)语言定义保持了原样,但我们发现使用不同库的多种Smalltalk开发。从UI界面的角度来看,一些库开始使用原生的,控件在表单和控件的风格上使用的部件,已经变得重要起来。

Smalltalk最初是由施乐(Xerox)的帕洛阿尔托研究中心(Palo Alto Research Center,简称Parc)实验室开发的,后来他们拆分成一个叫做ParcPlace的单独公司,专门运营和发展Smalltalk。ParcPlace的Smalltalk被称为VisualWorks,并提出了要成为一个跨平台的系统的创意。早在Java之前你就能用在Windows里写的Smalltalk程序在Solaris上正确运行。因此VisualWorks不使用本机的窗口小部件,完全用Smalltalk中的GUI。

在我讨论MVC时我完成了一些MVC的问题——特别是如何处理视图逻辑和视图状态。VisualWorks精炼了其框架,通过提出一种称为应用程序模型的构造来解决这个问题,这种构造向演示模型前进了一步。使用类似演示模型的想法,对VisualWorks来说并非新鲜事,原始的Smalltalk 80代码浏览器与之非常相似,但VisualWorks的应用程序模型把它完全烘制进框架中了。

这种smalltalk的关键要素是把属性转化为对象的理念。在通常具有属性的对象概念里,我们认为Person对象具有name和address属性。这些属性可能是域,不过还可能是其他东西。访问这样的属性通常有一个标准的常用的做法:在Java里,我们可以看到temp=aPerson.getName()和aPerson.setName(“martin”),在C#里,是temp=aPerson.name和aPerson.name=”martin”。

属性对象通过让属性返回一个封装了实际值的对象而改变了常用的做法。因此在VisualWorks里,当我们请求名字的时候,我们取回一个封装对象,然后我们通过向封装对象请求对象的值而获得实际的值。所以要访问一个人的名字使用temp=aPerson name value和aPerson name vlue:’martin’。

属性对象在窗体组件和模型之间较容易地建立了映射。我们只需要告诉窗口组件发送什么消息来获得对应的属性。窗口组件知道使用value或者value:访为属性值。VisualWorks的属性对象还允许你使用onChangeSend:aMessage to:anObserver消息创建观察者。

在Visual Works里你实际上找不到一个名字为属性对象的类。然而却又许多类它们尊许vale/value:onchangeSend:协议。最简单的就是ValueHolder-它只包含自己的值。与这次讨论关系更密切的是AspectAdaptor。AspectAdaptor允许属性对象完全封装另一个对象的属性。这样你就可以使用以下代码在PersonUI类里定义一个属性对象,而PersonUI类在Person对象里封装了属性:

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

让我们看看应用模式是如何适应我们正在运行的例子。

image

图9:正在运行的例子里的visual works应用模型的类图

使用应用模型和古典的MVC最主要的区别是我们现在在域模型类(Reader)和窗口组件之间拥有了中间类-即应用模型类。窗口组件不能直接访问域对象-它们的模型是应用模型。窗口组件还分割为视图和控制器,不过除非你正在构建新的窗口组件,否则这种区别不重要。

但你在一个UI设计器里面组装UI时,你会在那个设计器里设置每一个部件的切面(aspect).切面负责返回属性对象的应用程序模型上的方法.

image

图10:展示实际值的更新怎样去更新方差文本(variance text)的序列图

图10展示了如何进行基础的更新序列工作.当我改变了文本域中的值时,那个域就会更新应用程序模型里面的属性对象中的值.该更新跟着通过底层的领域对象,更新它的实际值.

在这一点上观察者关系就加入了进来。我们需要设置一些东西以便更新实际值时,会致使读取时可以表明它已经得到了改变。我们通过在实际的调节器中放置一个调用,来声明读取对象已经得到了改变——尤其是方差切面已经得到了改变。为方差设置切面适配器的时候是很容易告知它要去观察读取器的,因此它拾取了更新消息,而后跟踪到它所对应的文本域。文本域而后就会再一次通过切面适配器来发起获取新的值。

像这样使用应用程序模型和属性对象帮助我们不用写太多代码就能将更新联系起来。它也同样支持细粒度的同步(在我看来这不是一个好东西)。

应用程序模型允许我们将特定于UI的行为和状态,同实际的领域逻辑分离。因而我早期提出的问题之一,保持一个列表中的当前选择项,可以通过使用包含领域模型列表,同时存储了当前选择项的一种特定类型的切面适配器,来得到解决。

然而,所有这些都有一个限制,那就是对于更加复杂的行为,你需要去构建特殊的部件和属性对象。例如所提供的对象的设置并没有提供文本颜色的变化同变化的程度,这两者联系起来的方法。应用程序和领域模型的分离确实允许我们去分辨使用正确方式作出的决定,但是随后使用观察者切面适配器的部件我们还是需要去创建一些新的类。这常常意味着太多了工作,因此我们可以通过允许应用程序模型直接访问部件来使得这种事情变得更加容易,像图11所示:

image

图11:应用程序模型直接通过操作部件来更新颜色

像这样直接更新部件并不是展示模型的一部分,这就是为什么可视化工作的应用程序模型并不是真正的展示模型了。这种需要去直接操作部件的情况在处理一些脏活累活可以被看到,并且帮助开发出了模型-视图-展示器方法。

所以现在就有了关于应用程序模型的言论:

  • 跟随MVC使用展现分离和观察者同步。
  • 介绍了一种作为展示逻辑和状态安身之处的中间应用程序模型——一种部分使用展示模型的开发形式。
  • 部件不直接去观察领域对象了,而是它们回去观察应用程序模型。
  • 广泛使用属性对象来帮助连接不同的的层,并且使用观察者来支持细粒度的同步。
  • 操作部件并不是应用程序模型的默认行为,但在复杂的情况下一般都是这样做的。

模型-视图-展示器(MVP)


MVC是一种首次出现在于IBM的架构,并且在二十世纪九十年代变得更加出众。它在 Potel 的论文中被广泛援引。这个创意也获得了Dolphin Smalltalk开发者们的进一步推广和描述。像我们将会看到的两种没有完全涵盖但其下的基本理念已经变得流行起来。

接近MVC,我发现思考两股关于UI的思维之间的重要分歧是很有帮助的。一方面是UI主流的设计方法——表单和控制器架构,另一方面是MVC及其衍生物。表单和控制模型提供了一种可以很容易去理解和将可复用部件和特定于应用程序代码分离的设计。它所缺乏的,同时也是MVC所长于的,是展示分离,而且确实有使用领域模型进行上下文的编程。我看到了MVP向着统一这些流派、尝试各取所长所迈出的步伐。

Potel论文中的首要内容是将视图看做是一种部件的架构,部件负责了对表单和控件的控制,以及移除任何视图/控制器的分离。MVP的视图就是这些部件的架构。它并不包含描述部件对用户交互如何进行响应的任何描述。

对于用户操作行为的响应在一个单独的展示对象中起作用。针对用户行为的基础处理器仍然存在于部件中,但是这些处理器很少将控制传递给展示器。

而展示器决定了如何去响应事件。Potel谈到了这种交互主要是在模型上的动作,通过一种命令和选择的系统。这里需要明确提出的一件很有用的事情就是在一个命令中对模型进行的所有编辑进行打包的方法——这提供了一种非常好的提供撤销/重来行为的基础。

由于展示器更新了模型,视图会通过MVC使用的同样的观察者同步方式得到更新。

Dolphin的描述也是类似的。主要的相同之处仍然是展示器的显示。在Dolphin的描述中并没有展示器可以通过命令和选择在对象上起作用的架构。对于展示器直接操作视图的方式也有明确的讨论。Potel并没有明确表示展示器该不该这样做,但是对于Dolphin来说这种能力对于克服应用程序模型的一种缺陷非常重要,这种能力在我为变化域中的文本上色是必不可少的。

MVC中思维的差异之一是对于展示器控制视图中部件的程度。一方面是所有视图逻辑被放到了视图中,而展示器并不去参与决定如何去渲染模型。这是一种Potel所暗示的类型。Bower和McGlashan背后的方向是我所谓的监视控制器(Supervising Controller),这里视图会来处理很多的能够进行声明式描述的视图逻辑,而展示器则会来处理更多复杂的情况。

你也可以去除展示器针对部件进行操作的所有途径。这种我称之为被动视图(Passive View)的类型并不是MVP原始描述的一部分,但当人们探索可测性问题的时候它就被开发了出来。我将在稍后讨论这个类型,而那种类型是MVC的特色之一。

在我用之前的讨论同MVP进行对比之前,我应该提醒的是这里所有的论文就MVP都不完全与我持相同的解释。Potel意谓MVC控制器是整体协调员——而我并不是这样看待它们的。Dolphin谈论了许多关于MVC的问题,但它们所意谓的MVC是可视化工作应用程序模型(VisualWork Application Model)的设计,而不是我描述的经典的MVC(我并不像就此非议他们——如今尝试去获取关于经典MVC的信息并不容易啊。)

现在是时候做一些比较了:

  • 表单和控件:MVP拥有一个模型,而展示器会去使用观察者同步操作这个模型,然后更新视图。虽然直接访问控件是被允许的,但除非使用模型不是首选的时候才可以这样做。
  • MVC:MVP使用了一种监督控制器(Supervision Controller)来操作模型。部件将用户的操作处理交给给监督控制器。部件没有被区分成视图和控制器。你可以吧展示器想成像是一个控制器,但是它没有对用户操作处理进行初始化。然而还有一点也很重要,那就是展示器通常处于表单级别,而不是部件级别——这或许是一个相当大的区别。
  • 应用程序模型:跟对应用程序模型所做的一样,视图把事件处理交给了展示器。然而视图也可能直接通过域模型更新自身,展示器并不是作为一个展示模型参与进来的。展示器更加欢迎对于不适合观察者同步的行为就直接对部件进行访问。

MVP展示器和MVC控制器之间有着明显的相似之处,而展示器是MVC控制器的一种宽松形式。由于许多设计将会随MVP之流,而把‘控制器’作为展示器的一个代名词。在我们谈论处理用户输入的时候使用控制器就有了合理的说法。

image

图12:MVP实际读取更新的序列图

让我们来看一看一个MVP(监督控制器)版本的冰激凌监视器(图12)。开始它同表单和控件版本的有很多类似之处——实际的文本域在当它的文本发生变化时触发一个事件,展示器会监听这个事件并且获得这个域的新值。在这一个点上展示器更新了读取域对象,受到改变的域会用这个对象观察和更新其文本。最后的一部分是变化域颜色的设置,是通过展示器来实现的。它从读取中获得其分类,然后就去更新变化域的颜色。

下面是MVP的评述:

  • 用户操作的处理被部件传递到监督控制器。
  • 展示器负责在域模型中协调变更。
  • MVP的不同变体在处理视图更新时有很大的不同。这些不同来自使用观察者同步来让展示器里里外外奔走进行更新。

低级别视图(Humble View)


过去几年以来非常流行编写自测试代码。尽管不是最后一个找寻时代气息的人,但这是一种我深深沉溺其中的运动。我的许多校友都是xUnit框架、自动回归测试、测试驱动开发、持续集成还有类似流行词汇的超级粉丝。

当人们谈论自测试代码时,用户界面迅速变成了萦绕他们额头的难题。许多人发现GUI的测试介于某种强硬和不可能之间。这很大一部分归因于UI是精密耦合到整个UI环境中的,并且很难将它们分开然后一块一块测试。

有时候这种测试难度被过分强调了。你能够经常令人惊讶的通过创建部件然后在测试代码中操作它们来走得更远。但也有不可能的时候,你丢失了重要的交互,存在着线程问题,而且测试运行起来也很慢。

因此出现了一个将对象中令测试变得尴尬的行为最简化,这样一种方式来进行UI设计的稳定运动。Michael Feathers将这种方法简明扼要的总结为低级别的对话框(The Humble Dialog)。Gerard Meszaros 将这一概念广义化为一种低级别对象(Humble Object)的思想——任何难于测试的对象都应该具有简化的行为。那样如果我们不能将它归入到我们的测试套件中的话,我们就去降低不可预计风险出现的机会。

低级别的对话框论文引述了一种比原始的MVP使用方式更深入的展示器。并不仅仅让展示器去决定如何响应用户事件,它也处理着UI部件自身中数据的数量。因此部件不再必须要,或者需要对模型的可见性;它们形成了一个由展示器操作的被动视图(Passive View)。

这不是使得UI低级化的唯一方式。另一种方式是使用展示模型,尽管你确实需要部件中有更多一点点的行为,而对于部件来说知道它们自己如何在展示模型中布局已经足够了。

两种方法的关键都是借助于测试展示器,或者借助于测试展示模型,你不用去触碰最难测试的部件就能去测试最大的风险。

使用展示模型,你借助于所有展示模型所做出的实际决定来达到目的。所有的用户事件和展示逻辑都源于展示模型,因而所有的部件都需要将它们自身映射到展示模型的属性中去。而后你不用任何部件被展示的前提下就能测试展示模型的大部分行为了——唯一的风险存在于部件的映射。这里提供的简单办法就是不去测试它你也能够继续。在这种情况下屏幕同被动视图方法并不是一样程度低级别的,但差异会很小。

由于被动视图使得部件完全的低级别,甚至不存在映射,被动视图消除了甚至是使用展示模型的小风险。成本不过是在你的测试运行期间你需要一个双重测试(Test Double)来模仿屏幕——它是你需要建立的一个额外机制。

一种使用监督视图的权衡存在。让视图做一些简单的映射虽然引入了一些风险,但是也(通过展示模型)收获了声明特殊简单映射这一能力的好处。由于更加复杂的更新将会通过展示模型来决定,并进行映射,而一个监督模型不用加入任何映射就能针对复杂的情况对部件进行操作,因而映射对于监督控制器来说将比展示模型更加趋于小型化。

深入阅读


最近的文章能使进一步阐释我的想法,在bliki.

致谢


Vassili Bykov慷慨地让我有一份霍布斯在现代VisualWorks运行实施的第2版的Smalltalk-80(从20世纪80年代初)。这给我提供了一个鲜活的例子,比如MVC是如何在回答细节问题时工作的和如何用默认的图片。在那些日子里,很多人认为使用虚拟机是不切实际的。我不知道我们之前认为看我VisualWorks在的VisualWorks虚拟机运行在Windows XP上运行在VMware虚拟机上运行Ubuntu的写在一个虚拟机运行Smalltalk 80


上篇: 关于UI的思考 下篇: 修改Qt on android源码,支持鼠标右键,中键和滚轮事件