GUI架构(翻译)

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

 

GUI架构

组织富客户端系统的代码有许多不同的方式。在这里我讨论最有影响力的方式,并介绍它们与模式的关系。

上次重大更新: 2006-7-18 
 



 

对用户和开发者,图形用户界面(GUI)已成为软件景观一个常见部分。从设计的角度来看,它们所代表的系统设计中的特定问题,引起了一些不同但类似的解决办法。

我的兴趣是确定富客户端开发中共同的和有用的模式。在经历过的软件项目的设计审查中,我见到许多不同的设计,也见过不少用相同方式表达的设计。这些设计中存在有用的模式,但描述它们往往是不容易的。以模型-视图-控件器(MVC)作为一个例子。它往往被称为一种模式,但我觉得把它作为一个模式并不是非常有用,因为它包含相当多的不同观点。对MVC,不同的人在不同的场合有不同的看法,并都形容这是“MVC”。如果这样做不够混乱,它会形成一个中国谜语式的系统,从而使MVC更容易得到误解。(译注:不太通,原文If this doesn't cause enough confusion you then get the effect of misunderstandings of MVC that develop through a system of Chinese whispers.)

我想在这篇文章中探讨一些有趣的架构,并按我的方式解释它们最有趣的特点。我希望这篇文章能为理解这些模式提供一个环境。

在一定程度上,你可以把这篇文章作为一个历史追溯,来回顾以往多年来在用户界面设计中出现的思想。但是,我必须提醒,了解架构是不容易,尤其是当它们还在变化、消亡时。描述思想的散布更难,因为人们对同一架构见仁见智。我没有详尽描述各个架构,而是指出这些设计中共同的描述。如果这些描述丢失了什么,那是我根本不了解这一点。所以不要把我的说明,作为我所提及架构的权威说明。 此外,有些无关的内容已经省略或简化。请注意我的主要兴趣是基本模式,而不是在记录这些设计的历史。

(也有一些是例外,在我没有运行Smalltalk-80来检查MVC。再次,我不会认为我的描述是详尽无遗,但它确实揭示了用共同描述不能解释的事情,并让我更谨慎所描述的其它架构。如果你熟悉其中的一些架构,并发现我有重要的错误或缺失,请让我知道。并且,我想对这个领域一个详尽的调查,将是一个很好的学术研究课题。)

 

Forms and Controls窗体和控件

首先我想从一种既简单又熟悉的架构开始。它没有一个共同的名字,所以在本文中我称之为“窗体和控件”(为简化,后面译为F&C)。这是一个熟悉的架构,虽然也常常被一些像我这样的设计爱好者诋毁,但它普遍出现在在客户-服务器的开发环境中,如90年代的VB,Delphi,PowerBuilder。

为了探讨它和其它架构,我使用一个常见的例子。在我生活的新英格兰,有一个监测大气中的雪榚颗粒物数量的政府项目。如果浓度太低,这说明我们没有吃足够的冰淇淋-这构成了对经济和社会秩序的严重威胁。(我喜欢的例子,和其他同类文章同等真实J) 。

为了监控雪榚消费的健康情况,政府建立了遍布新英格兰州的监测站。基于复杂的大气模型,政府为每个检测站都设定了目标值。记录员经常到各个检测站检测

实际雪榚粒子浓度。这个用户界面允许他们选择站点,并输入日期和实际浓度值。然后软件系统计算并显示出与目标值的偏差;此外,如果实际浓度值低于目标值10 %以上,偏差用红色显示,如果偏差小于5 %,用绿色显示。



 

图1 :我所使用例子的用户界面。

观察这个界面,我们可以看到一个重要的区分。这个窗体是特定于我们应用,但它使用的控件是一般性的。多数的GUI开发环境都提供了非常多的常用控件,以供我们在应用程序窗体中使用。我们可以建立新的专用控件,这常常是一个很好的主意。但还是可以区分通用的、可重复使用的控件和特定窗体,即使这些专用控件可以在多个窗体中复用。

窗体包含两项主要职责:

  • 界面布局:在屏幕上定义控件的排列以及和其他控件的层次结构。
  • 窗体逻辑:那些不容易在通用控件中编码的行为。

大多数图形用户界面开发环境,允许开发者用图形化的编辑器定义屏幕布局,可让您拖放控件到窗体上的特定位置。这样人们很容易地设定漂亮的控件布局(尽管它不一定是最好的方式,我们后面会谈到)。

控件显示数据-这涉及到读取。多数情况下,数据来自其他地方,在这个例子中让我们假设SQL数据库,这和大多数客户-服务器工具一样。 在大多数情况下,有三份数据拷贝:

  • 一份拷贝的数据在数据库。这个拷贝是持久的记录数据,因此我称它为记录状态。记录状态通常是共享的,对多人可见的。
  • 另一份拷贝在应用程序内存内的记录集中。大多数客户-服务器环境提供了工具,使这很容易做到。这个数据只是和应用程序和数据库之间一个特定的会话相关,所以,我称之为会话状态。这提供了一个本地版本的临时数据以供用户操作,直到他们保存或提交到数据库中,这时它和记录状态一致。本文不关注记录状态和会话状态的相关问题,这在另外一篇文章[P of EAA]中提到。
  • 最后拷贝在GUI组件中。准确的说,这是屏幕上显示的数据,因此我称之为视图状态。 对于用户界面来说,保持视图状态和会话状态是非常重要的。

保存视图状态和会话状态的同步是一项重要任务。 数据绑定工具使这变得容易。其想法是,任何改变,无论是在控件数据中,还是在后台数据集中,立即被传播到另一方。所以,如果我修改屏幕上的数据,文本控件正确地更新后台数据集中对应的列的值。

通常数据绑定都使用了特殊的技巧来避免这样的循环修改:控件数据修改,导致记录集修改,然后更新控件,然后再次更新数据集… … 。使用流程有助于避免这个循环-在界面打开时我们从会话状态加载数据到视图状态,屏幕打开后,以后任何屏幕数据的改变都更新到会话状态中。每次屏幕数据修改就直接更新状态数据的情况比较少,因此,数据绑定可能也不是真正双向,而是在窗体打开时加载,然后从控件更新到记录集中。

数据绑定在处理大部分的客户-服务器应用时相当精巧。如果我改变了(界面中的)值,(记录集中)相关的列也更新了,甚至改变选定的位置可改变纪录集的当前选中行,从而导致其他控件刷新。

很多这样的行为是框架开发者建立的,他们关注共同的需求,并在框架中使之容易得到实现。这通常是对控件通过设定值(通常被称为属性)来实现的。通过一个简单的属性编辑器来指定字段名,控件就可以绑定到记录集的某个字段。

使用数据绑定加上合适的参数化,可以帮助您处理很多事情,但是它不能解决所有的问题,总是会有些逻辑不符合参数化的选项。在这个案例中计算差额就是一个与内建行为不符的例子-因为它是特定应用相关的,因此它通常被放在窗体中。

为了做到这一点,当实际字段的值变化时,窗体需要随之更新,这就要求通用文本控件来调用窗体中某些特定的方法。这比选择一个类库并用某些机制,如通过控制反转(Inversion of Control),来调用它更复杂一些。。

有很多不同的方法可以达到这个目的,多数客户-服务器工具的方法是通过事件的概念。每个控件都有一个它能触发的事件列表。任何外部实体都可以告诉控件他对某个事件感兴趣-在这种情况下,控件在事件触发时会调用外部对象。基本上,这是一个改写的观察者模式,本例中窗体观察控件。框架通常提供了一些机制,开发人员可以在一个子程序中编写代码,这些代码在事件发生时被调用。事件和子程序之间的关联如何建立,不同框架之间可能有所不同,而且对本文的话题并不重要的,唯一需要确定的是存在一些机制使得它们能够建立关联。

一旦窗体中的方法获得控制权,它就可以做任何希望的事情。它可以执行某些动作,修改控件,依靠数据绑定,上述任何变化都更新到会话状态。

这也是有必要的,因为数据绑定不是永远存在。窗体控件有一个很大的市场,但并非所有的都使用数据绑定。如果控件没有数据绑定,那么进行数据同步就是窗体的责任。这项工作可以这样完成:初始化时把数据从记录集拉取到界面控件,在保存按钮按下时把已修改的数据拷贝到数据集中。

让我们审视在有数据绑定的情况下,编辑的实际值的过程。屏幕上的每个通用控件,窗体实体都拥有直接引用,在这里我只关注实际值、偏差值和目标值字段。



  图2 :窗体和控件的类图

文本控件声明了一个文本改变事件,窗体在初始化过程订阅了该控件事件,把它绑定到自身的一个方法,在这里是actual_textchanged。


 

图3 :窗体和控件模式中修改字段的顺序图

当用户修改实际价值,文本控件触发事件,通过框架的绑定机制, actual_textchanged开始运行。该方法得到实际值和目标值,相减,并把差值填到偏差值控件中。它还计算出偏差值应该用什么颜色显示并调整控件的字体颜色。

我们可以总结这个架构:

  • 开发者编写和应用相关的窗体,窗体使用通用的控件。
  • 窗体描述了控件的布局。
  • 窗体监听控件事件,对控件触发的事件调用对应的处理函数。
  • 简单的数据编辑通过数据绑定处理。
  • 复杂的数据编辑在窗体的事件处理函数中实现。

模型-视图-控件器(MVC)

MVC大概是界面开发中被最广泛的引用的模式,也是被误解最多的模式。我已经记不清有多少次,被称为MVC的东西最终被证明完全不是MVC。坦白说,很多时候原因在于经典的MVC并不适用与目前的富客户端应用,但现在先让我们考察其起源。

在我们考察MVC时,最重要的是要记住,这是第一次严肃处理各种规模的用户界面的尝试。准确的说,图形用户界面并不是在70年代才得到流行。上述的窗体和控件模型其实出现在MVC之后。我先描述它并不是因为它好,而是因为它简单。同样地,我将用前面的评估例子来讨论Smalltalk-80的MVC模式。但需要提醒,我在Smalltalk-80的几个细节上采取了一些自由来进行简化,这是一个单色的系统。

MVC的核心的、对后续系统影响最大的想法, .被我称为分离呈现 分离呈现背后的想法是要明确区分领域对象(这是我们对现实世界中的抽象)和呈现对象(这是我们在屏幕上能看到的GUI元素)。领域对象应该可以完全自包含,可以在不涉及呈现对象的情况下工作,并可以同时支持多个呈现对象。这是Unix文化中的重要部分,一直持续到今天,这使许多应用程序可以同时通过图形和命令行界面操作。

在MVC中,领域元素被称为模型。模型对象和UI元素无关。为了开始讨论我们的评估界面的例子,我们把模型当作一个包含各个字段的reading(译注:在这里没有找到合适的词,用原文)。(正如我们将看到的,列表框的出现使得什么是模型这个问题变得更复杂,但先让我们忽略列表框) 。

在MVC中,我假设领域模型是普通对象,而不是实体窗体和控件模型中的记录集。这反映了设计背后的基本假设。窗体和控件模式假定大多数人想要轻松的操作关系型数据库中的数据,而MVC假设我们经常面对普通的Smalltalk对象。

MVC中的呈现部分由另外两个要素组成:视图和控制器。控件器的工作是接受用户的输入,并考虑如何处理它。

在这一点上我要强调,不是只有一个视图和控制器。对于界面上的每个元素,包括每个控件和整个窗体,都有一个视图-控制器组合。所以,对用户输入的响应,第一个步骤是各个控制器协作来确定哪一个得到编辑。本例中是实际值控件,因此是实际值控制器来处理后续环节。

 

图4 :模型,视图,控件器基本依赖关系。 (我称这个为基本的,因为实际上的视图和控件器是直接关联的,但多数开发者都没有使用这个事实)。

就像后来的开发环境,Smalltalk揣摩出你想重用通用的用户界面组件。 在这种情况下,控件是视图和控制器对。它们都是通用类,需要插入应用程序相关的特定行为。存在一个表现整个屏幕、定义基础控件的布局的整体视图,这类似于窗体和控件器模式的窗体。不同的是,MVC的整体视图的控制器中没有处理控件事件的函数。

 

图5 :MVC版本的雪榚颗粒浓度监控程序界面的类图

文本控件的配置确定链接到模型的方式,并告诉它在文本变化时应该调用什么函数。界面初始化时配置被设置为'#actual: '(Smalltalk中前导的’#’表示符号,或者内部字符串)。字段被修改时,文本控件的控制器利用反射来调用模型的函数。本质上,这是和数据绑定同样的机制,控件和后台实体(记录行)关联,并被告知回调的函数(需要修改的列)。


 

图6 :MVC中改变实际值的时序图。

因此,并没有一个整体的对象来监听低层次的控件,而是低层次控件监听模型,模型中处理了窗体和控件模式中窗体做的很多决策。本例中,模型就是一个计算差额的最合适的实体。

观察者模式在MVC中确实存在,实际上它的出现应该归功于MVC模式。本例中,视图和控制器监听模型。当模型变化时,视图做出响应。模型中的值变化时,实际值控件收到通知,然后调用被定义为文本控件的一个aspect(译注:找不到合适的词,用原文)的函数,在这里是#Actual,并把文本设置为函数的结果值。(颜色的设置和实际值类似,但这带来它自身的问题,后面我会提到。)

你会看到,文本控件器没有设定视图中的值,它更新了模型,然后让观察者模式来进行更新。在窗体和控件模式中是完全不同的:窗体更新了控件,并通过数据绑定来更新后台数据集。我把这两种风格描述为两种模式: and Observer Synchronization 流同步模式和观察者同步模式。这两种模式描述了两种不同的方式来解决视图状态和会话状态的同步触发问题。窗体和控件模式在不同的流程中直接更新不同的控件,MVC模式更新模型,然后通过观察者模式来更新监听模型的实体。

流同步在没有数据绑定时更加明显,如果需要应用程序自身来做同步,那么它将在重要的流程点来进行,比如打开窗体或者点击保存按钮。

观察者同步模式带来的一个结果是,在用户操作一个特定的控件时,控制器是无需知道那些其他控件是需要处理的。而(在窗体和控件模式中),窗体需要在一次变化中保持整个窗体所有控件的一致性,在复杂的窗体中,这是比较麻烦的,而观察者同步模式中的控制器不需要关注这一点。

在有多个视图同时观察一个模型时,这个无需关注的优势尤其的明显。经典的MVC模式例子中,有一个和电子表格类似的数据窗体,还有几个图形窗体(来显示同一数据)。该电子表格窗口并不需要了解其他窗体是否被打开,它只是改变了模型,观察者模式接手后续的工作。而在流同步模式中, 它需要以某种方式知道其他窗体被打开了,然后通知它们进行刷新。

虽然观察者同步模式很好,但它也有不足。观察者同步的核心问题在于观察者模式本身-阅读代码时,你不知道发生了什么事情。在想知道一个Smalltalk-80窗体中发生了什么时,我对这记忆深刻。我确实可以阅读代码,但一旦观察者机制发生作用,我只能通过调试,逐条跟踪被调用的语句。因为它的隐性行为,观察者模式的行为难以理解和调试。

从时序图中看,不同模式的作用差异尤为明显。 .最重要以及最有影响力的差异是MVC模式的分离表现。计算实际和目标差异是业务行为,和UI无关。按照分离表现的结论,我们应该把这放在系统的业务层,这是模型实体该做的。在模型实体中,差异的计算在不不涉及任何用户界面的情况下也完全是符合逻辑的。

现在,我们可以开始看看一些更复杂的问题。论述MVC理论时我跳过了两个丑陋的问题。第一个问题便是处理差异值的颜色。这不适合放在领域模型中,因为设置颜色不是业务行为的一部分。首先要区分哪些是领域模型逻辑。我们需要确定差异值的性质。差异值被确定为良好(5 %以下),坏( 10 %以上),正常(其他值)。做出这一评价是肯定领域语言,根据差异性质确定颜色则是视图逻辑。问题出在,我们把视图逻辑放在在哪里-这不是标准文本控件的内容。

早期Smalltalk开发人员面临这个问题时想出了一些解决办法。我上面提到的解决办法是一个不干净的方法-牺牲领域模型的纯粹性来实现。 我可以接受偶尔的不纯粹-不过,我尽量不让它成为一个习惯。

我们依照窗体和控件模式的做法-让评估窗体监听差异的变化,差异值变化时窗体做出响应,并确定了差异字段的字体颜色。这里的问题包括更多地运用了观察者机制,逻辑变得更复杂,该机制的使用就是指数性的增长。同时,不同的视图之间的耦合程度也增加了。

我选择建立一个新的用户界面控件。本质上,我们需要这样一个GUI控件,它可以从领域模型获取值,参照内部的一个数值-颜色对照表设定字体颜色。控件的对照表以及需要参考领域模型的哪些值是在评估窗体在加载控件时设定的,就像它把字段的属性和观察者绑定类似。如果我可以继承文本控件并增加额外的行为,这可以很好的工作。继承的容易与否取决于组件的设计是否能够很好的支持继承-在Smalltalk这很容易,其他工具中有可能会困难一些。


 

图7 :使用一个可以根据配置确定颜色的特殊的文本控件子类

最后一种方法,是建立一个新的模型对象,该模型针对呈现,但和特定的构件无关。和领域模型相关的方法会委托领域模型对象进行,但它会增加新方法,支持和用户界面有关的行为,例如文本的颜色。


 

图8 :用一个中间呈现模型来处理视图逻辑。

最后一种方案在许多案例中都工作的很好,并成为Smalltalk开发则遵循的准则。我称之为这是一个呈现模型因为它的模型针对表现层,并成为表现层的一部分。

在处理试图状态时呈现模型也工作的很好。在MVC观念中,所有的视图状态均可以从模型的状态中计算得到。本例中我们如何得到列表框应该选中哪些站点? 呈现模型为我们提供了一个存放这种状态的地方。类似的问题还包括,如果我们的保存按钮只在数据发生变化时才使能,这是我们和模型交互的状态,而不是模型本身的状态。

现在可以对MVC模式作出总结:

  • 对呈现(视图和控制器)以及领域( .模型)作出明确区分-分离表现。
  • 把GUI控件切分为控件器(响应用户输入)和视图(展示模型的状态)。 控件器和视图,大多数情况下通过模型,而并非直接通讯。
  • 视图 .(以及控件器)监听模型,以允许同时更新多个构件而不需要直接沟通-观察者同步模式。

VisualWorks的应用模型

如上所述,Smalltalk-80的MVC模式非常有影响,有一些很好的特点,但也有一些问题。Smalltalk在80和90年代发展出经典MVC模型的一些变种。事实上,如果你认为视图/控件器分离是MVC的一个基础概念-就像MVC的名字意味的那样-那我们几乎可以说MVC消失了。

分离呈现和观察者同步是MVC中出现的概念,实际上对许多人而言,它们是MVC的关键要素。

在这些年来Smalltalk的概念也分裂了,Smalltalk的基本思想以及最小语言定义保留不变,但我们看到的多Smalltalk开发出不同的类库。从用户界面的角度来看,这很重要,因为有些类库开始使用F&C风格的本地构件。

Smalltalk开始由Xerox Parc实验室开发,后来他们建立了一个独立的公司,ParcPlace来进行Smalltalk的市场和开发工作。ParcPlace Smalltalk被称为VisualWorks,并决定要成为一个面向跨平台的系统。在Java出现之前,你就可以在Windows中开发Smalltalk程序,然后再Solaris重运行。VisualWorks没有采用本地控件,而是把GUI完全构建在Smalltalk中。

在我关于MVC的讨论中,我碰到一些问题,尤其是如何处理视图逻辑和视图状态的问题。为了处理这个问题,完善了他们的框架,推出一个趋近呈现模型的应用模型。 使用像呈现模型的概念对于VisualWorks来说不是第一次-原Smalltalk-80的代码浏览器与此非常相似,VisualWorks应用模型把它完全整合到框架中。

这个Smalltalk变种的一个关键理念,是用实体表达属性。在通常包含属性的实体概念认为一个人的对象具有名称和地址的属性。这些属性可能是一个字段,也可能是其他东西。通常有一些标准惯例来访问这些属性。在Java中,我们将看到

temp= aperson.getname()aperson.setname ("martin")

在c#是

temp=aperson.nameaperson.name="martin"

    属性实体改变了这些,通过一个实体来封装实际值。因此,在VisualWorks中,在需要访问名字时我们得到一个封装实体。通过访问封装实体的值我们得到实际值。所以访问一个人的名字将使用以下语法:

temp= aperson name valueaperson name value: "martin"

属性实体使得模型和控件之间的映射变得容易了一些。我们只是要告诉控件需要传递什么信息来得到相应的属性,控件知道如何使用value和value:来获取正确的值。VisualWorks的属性实体,也容许你通过onChangesend: amessage到 :anobserver来设立观察者。

你不会在VisualWorks中找到所谓属性实体类。相反,有一些类遵循value/value:/onChangedSend:协议。最简单的就是ValueHolder-仅持有其实际值。和本文更相关的是AspectAdaptor。该AspectAdaptor 让一个属性对象可以完全封装例外一个对象的属性。这样你可以用下列代码在PersonUI类中定义一个属性对象来封装Person类中的属性:

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

让我们看看如何在我们的例子中使用应用模型。


 

图9 :本案例中的VisualWorks应用模型类图

应用模型和经典的MVC一个重要区别是,在领域模型和控件之间有一个类-即应用模型。控件不直接关联领域模型-它们的模型是应用模型。构件仍可以分解成视图和控件器等,但除非你在建立新的控件,这并不重要。

在组装用户界面时,在一个UIPainter类中设置各个控件的方面。该方面对应应用模型上的一个方法,该方法返回一个属性对象。



 

图10 :显示更新的实际值如何更新差异值文本的时序图。

图10显示基本更新过程的工作方式。当我改变一个文本控件的值,该值更新了存在应用模型中的属性实体的值,并随之更新领域模型中的实际值。

在这一点上观察者模式出现了。我们必须建立机制来保证更新实际值导会致Reading的变化。通过放在设置实际值的函数中的一个回调,我们标明Reading已经被修改。特别的,差异值的Aspect改变了。一旦建立差异值Aspect的适配器,就很容易监听Reading。因此它接收到更新信息并通知差异值文本控件。因此差异值控件通过Aspect适配器来获取已经更新的值。

利用应用模型和属性实体,我们不必写很多代码就可以解决更新问题。同时它也支持细粒度同步(我不认为是一件好事)。

应用模型让我们区分UI和领域模型中不同的行为和状态。所以我前面提到的如何存储目前选择的站点的问题,可以通过使用一个特定Aspect适配器来包装领域模型的清单,并存储当前选定项目的方式解决。

这种方式的限制在于,对于较复杂的行为,你需要建立特殊构件和属性实体。比如所提供的对象中不包括差异值控件的字体颜色和差异值级别的关联。应用模型和领域模型的分离确实提供了分开(业务逻辑和视图逻辑)决策的正确方法,但为了使用控件来检测Aspect适配器,我们需要一些新的类。 .这经常被认为做了太多的工作,所以我们通过在应用模型中直接访问控件来进行简化。


 

图11 :通过直接操作控件来更新颜色的应用模型

直接更新构件,这不是呈现模型的方式,而是VisualWorks的方式,因此VisualWorks不是真正呈现模型。需要直接操纵构件被许多人认为是有点“肮脏”的工作,这导致模型-视图-呈现者模式的出现。

对应用模型的总结:

  • 追随MVC模式使用分离表现和观察者的同步。
  • 引入一个中间应用模型作为表现逻辑和状态的场所-一个呈现模型的局部扩展。
  • 界面构件不直接监听领域模型,而是监听应用模型。
  • 扩展属性对象来连接不同的层,并使用观察者实现细粒度的同步。
  • 应用模型操纵直接构件不是默认行为,但在复杂案例中普遍使用。

模型-视图-呈现者(MVP

MVP架构最早出现在IBM,更明显的是在20世纪90年代的Taligent。它通过potel的文章被大量引用。这一构想被Dolphin Smalltalk描述并得到进一步的普及。正如我们将看到,这两个描述并不完全一致,但其基本理念已成为热门。

为了讨论MVP,我觉得有必要讨论两种不同UI思路的明显不同之处。一方面窗体和控件器的架构是用户界面设计的主流做法,另一方面是MVC及其衍生。窗体和控件模型提供了一种很容易理解的设计方法,并明确区分了可以复用的构件和特定于具体应用的代码。它所缺少的,也正是MVC的明显优势,是分离呈现,以及使用领域模型的环境。我认为MVP统一两种潮流,并从中吸取其长处。

potel的第一要素是将视图认为是由Widget组成的结构。Widget对应于窗体和控件模型中的控件,并消除任何视图/控件器的分离。MVP中的视图就是这些Widget的结构。它不包含任何行为来描述构件对于用户的输入如何响应。

对于用户输入的积极响应存在于独立的呈现者中,Widget中依然存在对用户响应的基本处理器,但这些处理器只是简单的把控制发送给呈现者。

呈现者决定如何响应事件。potel认为这种互动主要是通过模型中的活动(Action),既是通过一个命令和选择的系统。在此要强调的是把所有对模型的编辑打包在一个命令中的做法,这提供了一个undo/redo的坚实基础。

一旦呈现者更新了模型,视图和MVC模式中一样,就通过观察者同步方式得到更新。Dolphin Smalltalk的描述是相似的。主要的相同点是存在呈现者Dolphin Smalltalk描述中呈现者结构没有通过命令和选择来作用于模型。也有明确的论点支持Presenter直接操作视图。Potel没有论述呈现者是否应该这样做,但是在Dolphin Smalltalk中,这种能力是必不可少的,它被用来克服应用模型中获取文本颜色的丑陋方式的缺陷。

MVP的一个变化是,在什么程度上用呈现者修改视图中的Widget。一个极端是,所有的逻辑都在视图中,呈现者不涉及如何显示模型。这是隐含在Potel中的观点。Bower and McGlashan的观点,被我称为Supervising Controller,视图处理多数可以用声明式描述的视图逻辑,而用呈现者来处理更复杂的情况

你也可以走向另外一个方向,让呈现者处理所有的对Widget的操作。这种方式被我称为Passive View,并不是原始MVP的一部分,但在人们涉及可测试性话题时得到发展。我后面将提到该风格,但是这种风格依然是MVP的一种。

在将MVP和之前的论述做比较之前,我需要指出,两篇关于MVP的文章也做了这个比较,但和我的解释方式不同。Potel认为MVC的控制器是一个全局协调者,而我不这样认为。Dolphin 谈到很多和MVC相关的内容,但是他们的MVC指VisualWorks应用模型,而不是我描述的传统的MVC。(我不责怪他们-试图获得关于经典MVC的信息现在都不容易,更何况在当时)。

现在做出比较:

  • 窗体和控件:MVP拥有模型,而呈现者按照观察者同步模式操作模型。 虽然允许直接操作Widget,但应该首先考虑利用模型,而不应成为第一选择。
  • MVC:MVP使用Supervising Controller来操作模型。Widget把用户动作传递到Supervising ControllerWidgets没有被分为视图和控制器。你可以认为Presenter是一个控制器,但是没有用户输入的初步处理。需要注意的是,呈现者通常是在窗体层面上,而不是在widget层面,这或许是一个更大的差异。
  • 应用模型:视图把事件传递到呈现者,这和应用模型模式相同,然而,视图可以直接从模型更新自身,而呈现者和表现模型不同。而且,呈现者r也被欢迎来直接更新Widget,这并不符合观察者同步模式。

MVP的呈现者和MVC的控制器有显著的相似之处,而且呈现者是一个松散的MVC的控件器。其结果是,大量的设计遵循MVP风格,但是把呈现者设计为控制器。通常当我们谈到处理用户输入的时候,关于控制器的使用有一个讨论:


 

图12 :MVP实际模型的更新的时序图

让我们看看在MVP方案(Supervising Controller)版本的雪榚颗粒监测器(图12)。开始时它和窗体和控件版本相同,实际值文本变化时,它出发了一个事件,呈现者监听了该事件,并获取了字段的新值。这是呈现者更新了领域模型,而差异值字段监听了该变化,然后更新了它的文本。最后一步是设置差异值得字体颜色,这由呈现者完成。它从领域模型中获取变化的类型,并更新了差异值得颜色。

对MVP的总结:

  • 用户输入由Widget传到Supervising Controller
  • 呈现者协调领域模型的变化。
  • MVP不同的变体处理视图更新的方式不同,包括使用观察者同步模式到呈现者处理所有的更新,两种方式之间存在多个变种。

谦虚的视图(Humble View)

过去几年中出现了编写自我测试代码强烈的风气。尽管在时尚感方面我是最差的一个,但是我完全沉浸于这个(编写自我测试代码的)风气中。我的许多同仁都是xUunit框架、自动化回归测试、测试驱动开发、持续集成和类似的实践的支持者。

当人们谈论到自测试代码的用户界面时,总是因为这个问题迅速抬起他们的头。很多人觉得测试GUI是很难的,甚至是不可能的。这主要是因为UI和整个用户界面环境紧密耦合,很难分开而分块测试。

有时这种测试难度被过分强调了。在测试代码时,若你创建和操作控件,你经常能得到出乎意料的进展。但也有场合,这是不可能的,(比如)你错过了重要的互动,或是有线程的问题,以及测试运行速度太慢。

UI设计出现一种坚定的态度,认为要减少实体中难以测试的行为。Michael Feathers简朴的对话框中总结了这种看法。Gerard Meszaros这一做法总结为Humble Object-难以测试的实体,行为应该尽量的少。这样,即使不能列入测试套件,未被发现的错误出现的机会还是减少了。

简朴的对话框中文章中使用呈现者,但程度比基本MVP模式更深。呈现者不但决定如何响应用户输入事件,它也处理用户界面控件如何显示数据。因此,界面部件没有,也不需要有,对模型的关联,它们组成一个被动视图,由呈现者操作。

这不是唯一的使用户界面变得“谦虚”的方式。使用呈现模型是另外一种方式,虽然你需要在界面部件中需要多增加一些行为来和呈现模型绑定。

两种办法的关键是,通过测试呈现者或者呈现模型,你测试了大部分的风险,而无需涉及难以测试的界面部件。

通过呈现模型你可以让所有的视图决策由呈现模型实现。所有的事件和显示逻辑都经过呈现模型,而界面部件需要做的仅是把自身和呈现模型绑定,因此,你可以在没有视图时测试大多数的行为,剩下的风险只存在于界面部件的映射中。由于这足够简单,你可以在不测试它的情况下还工作的很好。在这种情况下,界面并不是和被动的视图中一样被动,但差别不大。

被动视图使得构件变得完全是“谦虚”的,和呈现模型相比,甚至不需要映射,(连映射出现错误的)微小的风险都被消除了。它的成本的是,在你进行测试时,需要建立一个Test Double来模仿屏幕,这是额外的机制。

类似的考虑在Supervising Controller中也存在,让视图进行简单的映射会引入了一些风险,但优势和呈现模型相同,可以用宣称的方式来定义关联。在Supervising Controller模式中,映射比呈现模型更少。复杂的行为将被表现模型和映射决定,而Supervising Controller在复杂的情况下操作界面部件,不再采取映射方式。

进一步阅读

进一步阅读可参考我的bliki。 

 

译著附录

词汇对照表

模型-视图-控制器:Model-View-Controller, MVC

窗体和控件:Forms and Controls,译文中简称为F&C

记录状态 record state

会话状态 session state

视图状态 screen state

分离呈现 Separated Presentation

流同步模式 Flow Synchronization

观察者同步模式 Observer Synchronization

呈现模型 Presentation Model

属性实体 Property Object

模型-视图-呈现者Model-View-PresenterMVP

界面部件 Widget





转载于:https://www.cnblogs.com/wangpy/articles/978774.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值