设计模式可复用面向对象软件的基础02.pdf
第2章实例研究:设计一个文档编辑器这一章将通过设计一个称为 L e x i的“所见即所得”(或“W Y S I W Y G”)的文档编辑器,来介绍设计模式的实际应用。我们将会看到在 L e x i和类似应用中,设计模式是怎样解决设计问题的。在本章最后,通过这个例子的学习你将获得 8个模式的实用经验。图2-1是L e x i的用户界面。文档的所见即所得的表示占据了中间的大矩形区域。文档能够以不同的格式风格自由混合文本和图形。文档的周围是通常的下拉菜单和滚动条,以及一些用来跳到特定页的页码图标。图2-1 Lexi的用户界面L e x i的设计是基于C a l d e r开发的文本编辑应用D o c的。C L 9 2 第2章实例研究:设计一个文档编辑器2 32.1 设计问题我们将考察L e x i设计中的7个问题:1)文档结构对文档内部表示的选择几乎影响L e x i设计的每个方面。所有的编辑、格式安排、显示和文本分析都涉及到这种表示。我们怎样组织这个信息会影响到应用的其他方面。2)格式化L e x i是怎样将文本和图形安排到行和列上的?哪些对象负责执行不同的格式策略?这些策略又是怎样和内部表述相互作用的?3)修饰用户界面L e x i的用户界面包括滚动条、边界和用来修饰 W Y S I W Y G文档界面的阴影。这些修饰有可能随着 L e x i用户界面的演化而发生变化。因此,在不影响应用其他方面的情况下,能自由增加和去除这些修饰就十分重要了。4)支持多种视感(l o o k-a n d-f e e l)标准L e x i应不需作较大修改就能适应不同的视感标准,如M o t i f和Presentation Manager(PM)等。5)支持多种窗口系统不同的视感标准通常是在不同的窗口系统上实现的。L e x i的设计应尽可能的独立于窗口系统。6)用户操作用户通过不同的用户界面控制L e x i,包括按钮和下拉菜单。这些界面对应的功能分散在整个应用对象中。这里的难点在于提供一个统一的机制,既可以访问这些分散的功能,又可以对操作进行撤消(u n d o)。7)拼写检查和连字符L e x i是怎样支持像检查拼写错误和决定连字符的连字点这样的分析操作的?当我们不得不添加一个新的分析操作时,我们怎样尽量少修改相关的类?我们将在下面的各节里讨论这些设计问题。每个问题都有一组相关联的目标集合和我们怎样达到这些目标的限制条件集合。在给出特定解决方案之前,我们会详细解释设计问题的目标和限制条件。问题和其解决方案会列举一个或多个设计模式。对每个问题的讨论将在对相关设计模式的简单介绍后结束。2.2 文档结构从根本上来说,一个文档只是对字符、线、多边形和其他图形元素的一种安排。这些元素记录了文档的整个信息内容。然而,一个文档作者通常并不将这些元素看作图形项,而是看作文档的物理结构行、列、图形、表和其他子结构。而这些子结构也有自己的子结构。L e x i的用户界面应该让用户直接操纵这些子结构。例如,一个用户应该能够将一个图表当作一个单元,而不是个别图形原语的一组集合。用户应该能够对表进行整体引用,而不是将表作为非结构化的一堆文本和图形。这有助于使界面简单和直观。为了使L e x i的实现具有类似的性质,我们选择能匹配文档物理结构的内部表示。特别的,内部表示应支持如下几点:保持文档的物理结构。即将文本和图形安排到行、列、表等。可视化生成和显示文档。根据显示位置来映射文档内部表示的元素。这可以使 L e x i根据用户在可视化表示中所点击的某个东西来决定用户所引用的文档元素。作者也常从逻辑结构来看文档,即看成句子、段落、节、小节和章。为了使这个例子简单,我们的文档内部表示不显式储存逻辑结构信息。但是我们描述的设计方案同样适用于表述逻辑结构信息的情况。除了这些目标外,还有一些限制条件。首先,我们应该一致对待文本和图形。应用界面允许用户在图形中自由的嵌入文本,反之亦然。我们应该避免将图形看作文本的一种特殊情形,或将文本看作图形的特例。否则,我们最后得到的是冗余的格式和操纵机制。机制集合应该使文本和图形都能满足。其次,我们的实现不应该过分强调内部表示中单个元素和元素组之间的差别。L e x i应该能够一致地对待简单元素和组合元素,这样就允许任意复杂的文档。例如,第 5行第2列的第1 0个元素既可以是一个字符,也可以是一个由许多子元素组成的复杂图表。一旦我们知道这个元素能够画出自己并指定了它的区域,那么它怎样显示在页面上和它的显示位置的确定就并不困难了。然而,为了检查拼写错误和确定连字符的连接点,需要对文本进行分析。这就与第二个限制条件产生了矛盾。我们通常并不关心一行上的元素是简单对象还是复杂对象,但是文本分析有时候依赖于被分析的对象。例如,检查多边形的拼写或以连字符连接它是没有意义的。文档内部表示设计应该考虑和权衡这个或其他潜在的彼此矛盾的限制条件。2.2.1 递归组合层次结构信息的表述通常是通过一种被称为递归组合(Recursive Composition)的技术来实现的。递归组合可以由较简单的元素逐渐建立复杂的元素,是我们通过简单图形元素构造文档的方法之一。第一步,我们将字符和图形从左到右排列形成文档的一行,然后由多行形成一列,再由多列形成一页,等等,见图 2-2。图2-2 包含正文和图形的递归组合我们将每一个重要元素表示成一个对象,就可以描述这种物理结构。它不仅包括字符、图形等可见元素,也包括不可见的、结构化的元素,如行和列。结果就是如图2-3所示的对象结构。通过用对象表示文档的每一个字符和图形元素,我们可以提高L e x i最佳设计的灵活性。2 4设计模式:可复用面向对象软件的基础字符空格图组合(行)组合(列)第2章实例研究:设计一个文档编辑器2 5我们能够在显示、格式化和互相嵌入等方面一致对待图形和文本。我们能够扩展L e x i以支持新的字符集而不会影响其他功能。L e x i的对象结构与文档的物理结构非常相像。图2-3 递归组合的对象结构这里隐含了两个重要的地方。第一个很明显,对象需要相应的类。第二个就不那么明显了,因为我们要一致性地对待这些对象,所以这些类必须有兼容的接口。在像C+这样的语言中,可以通过继承来关联类,使得接口兼容。2.2.2 图元我们将为出现在文档结构中的所有对象定义一个抽象类图元(G l y p h)。它的子类既定义了基本的图形元素(像字符和图像),又定义了结构元素(像行和列)。图2-4描述了G l y p h类组合(列)组合(行)组合(行)空格C a l d e r第一个在这种上下文使用术语“Gl y p h”C L 9 0。大多数同时代的文档编辑器由于效率原因,并不是对一个字符就使用一个对象的。C a l d e r在他的论文 C a l 9 3 中论证了该方法的可行性。为了简单起见,我们将图元严格限制在类层次结构上,所以没有 C a l d e r的那么复杂。C a l d e r的图元还能减少存储开销,形成有向无环图结构。我们也可以使用F l y w e i g h t(4.6)模式来达到相同的效果,我们将把它作为留给读者的一个练习。图2-4 部分Glyph类层次2 6设计模式:可复用面向对象软件的基础层次的部分表示,表2-1以C+表示法描述了基本的G l y p h接口。表2-1 基本G l y p h接口R e s p o n s i b i l i t yO p e r a t i o n sA p p e a r a n c eVirtual Void Draw(Wi n d o w*)Virtual Void Bounds(Rect&)hit detectionVirtual bool Intersects(Const Point&)S t r u c t u r eVirtual Void Insert(Glyph*,int)Virtual Void Remove(Glyph*)Virtual Glyph*Child(int)Virtual Glyph*Parent()图元有三个基本责任,它们是 1)怎样画出自己,2)它们占用多大空间,3)它们的父图元和子图元是什么。G l y p h子类为了在窗口上表示自己,重新定义了 D r a w操作。调用D r a w时,它们传递一个引用给Wi n d o w对象。Wi n d o w类为了在屏幕窗口上表示文本和基本图形,定义了一些图形操作。一个G l y p h的子类R e c t a n g l e可能会像下面这样重定义D r a w:void Rectangle:Draw(Window*w)w-DrawRect(_x0,_y0,_x1,_y1);这里的_ x 0,_ y 0,_ x 1,_ y 1是R e c t a n g l e的数据成员,定义了矩形的对顶点。D r a w R e c t是Wi n d o w操作,用来在屏幕上显示矩形。父图元通常需要知道像子图元需要占用多大空间这样的信息,以把它和其他图元安排在一行上,保证不会互相覆盖(参见图2-2)。B o u n d s操作返回图元占用的矩形区域,它返回的是包含该图元的最小矩形的对角顶点。G l y p h各子类重定义该操作,返回它们各自画图所用的矩形区域。I n t e r s e c t s操作判断一个指定的点是否与图元相交。任何时候用户点击文档某处时,L e x i都能调用该操作确定鼠标所在的图元或图元结构。R e c t a n g l e类重定义了该操作,用来计算矩形和给定点的相交。因为图元可以有子图元,所以我们需要一个公共的接口来添加、删除和访问这些子图元。例如,一个行的子图元是该行上的所有图元。I n s e r t操作在整数I n d e x指定的位置上插入一个图元。R e m o v e操作移去一个指定的子图元。C h i l d操作返回给定I n d e x的子图元(如果有的话),像行这样有子图元的图元应该内部使用C h i l d操作,而不是直接访问子数据结构。这样当你将数据结构由数组改为连接表时,你也无需修改像D r a w这样重复作用于各个子图元的操作。类似的,P a r e n t操作提供一个标准的访问父图元的接口。L e x i的图元保存一个指向其父图元的指引,P a r e n t操作只简单的返回这个指引。为了使讨论简单化,我们这里特地使用最小化的接口。一个完备的接口应该包括管理颜色、字体和坐标转换等图形属性的操作,和管理更复杂子对象的操作。一个整数I n d e x可能并不是指定子图元的最好方法,它依赖于图元所用的数据结构。如果图元在连接表中储存子图元,那么使用连接表指针应该更有效。我们在 2.8节讨论文档分析的时候,将会给出索引问题的更好解决方案。第2章实例研究:设计一个文档编辑器2 72.2.3 组合模式递归组合不仅可用来表示文档,我们还可以用它表示任何潜在复杂的、层次式的结构。C o m p o s i t e(4.3)模式描述了面向对象的递归组合的本质。现在是回到此模式并学习它的时候了,需要时再回头参考这个场景。2.3 格式化我们已经解决了文档物理结构的表示问题。接着,我们需要解决的问题是怎样构造一个特殊物理结构,该结构对应于一个恰当地格式化了的文档。表示和格式化是不同的,记录文档物理结构的能力并没有告诉我们怎样得到一个特殊格式化结构。这个责任大多在于L e x i,它必须将文本分解成行,将行分解成列等等。同时还要考虑用户的高层次的要求,例如,用户可能会指定边界宽度、缩进大小和表格形式、是否隔行显示以及其他可能的许多格式限制条件。L e x i的格式化算法必须考虑所有这些因素。现在我们将“格式化”含义限制为将一个图元集合分解为若干行。下面我们可以互换使用术语“格式化”(f o r m a t t i n g)和“分行”(l i n e b r e a k i n g)。下面讨论的技术同样适用于将行分解为列和将列分解为页。2.3.1 封装格式化算法由于所有这些限制条件和许多细节问题,格式化过程不容易被自动化。这里有许多解决方法,实际上人们已经提出了各种各样具有不同能力和缺陷的格式化算法。因为L e x i是一个所见即所得编辑器,所以一个必须考虑的重要权衡之处在于格式化的质量和格式化的速度之间的取舍。我们通常希望在不牺牲文档美观外表的前提下,能得到良好的反映速度。这种权衡受许多因素影响,而并不是所有因素在编译时刻都能确定的。例如,用户也许能忍受稍慢一点的响应速度,以换取较好的格式。这种选择也许导致了比当前算法更适用的彻底不同的格式化算法。另一个例子,更多实现驱动的权衡是在格式化速度和存储需求之间:很有可能为了缓存更多的信息而降低格式化速度。因为格式化算法趋于复杂化,因而可以考虑将它们包含于文档结构之中,但最好是将它们彻底独立于文档结构之外。理想情况下,我们能够自由地增加一个 G l y p h子类而不用考虑格式算法。反过来,增加一个格式算法不应要求修改已有的图元类。这些特征要求我们设计的 L e x i易于改变格式化算法。最好能在运行时刻改变这个算法,如果难以实现,至少在编译时刻应该可以很方便地改变。我们可以将算法独立出来,并把它封装到对象中使其便于替代。更进一步,可以定义一个封装格式化算法的对象的类层次结构。类层次结构的根结点将定义支持许多格式化算法的接口,每个子类实现这个接口以执行特定的算法。那时就能让G l y p h子类对象自动使用给定算法对象来排列其子图元。2.3.2 Compositor和Composition我们为能封装格式化算法的对象定义一个 C o m p o s i t o r类。它的接口(见表 2-2)可让用户可能更关心的是文档的逻辑结构句子、段落、小节、章节等等。相比而言,对物理结构就没有这样的兴趣了。大部分用户不在意段落中的换行发生在何处,只要该段落能正确格式化就行了。格式化列和页,也是这样的。因而用户最终只指定物理结构的高层限制条件,用来满足他们的艰难工作则由 L e x i去完成。2 8设计模式:可复用面向对象软件的基础c o m p o s i t o r获知何时去格式化哪些图元。它所格式化的图元是一个被称为 C o m p o s i t i o n的特定图元的各个子图元。一个 C o m p o s i t i o n在创建时得到一个C o m p o s i t o r子类实例,并在必要的时候(如用户改变文档的时候)让C o m p o s i t o r对它的图元作 C o m p o s e操作。图 2-5描述了C o m p o s i t i o n类和C o m p o s i t o r类之间的关系。表2-2 基本C o m p o s i t o r接口责任操作格式化的内容void SetComposition(Composition*)何时格式化virtual void Compose()图2-5 Composition和Compositor类间的关系一个未格式化的C o m p o s i t i o n对象只包含组成文档基本内容的可见图元。它并不包含像行和列这样的决定文档物理结构的图元。C o m p o s i t i o n对象只在刚被创建并以待格式化的图元进行初始化后,才处于这种状态。当C o m p o s i t i o n需要格式化时,调用它的C o m p o s i t o r的C o m p o s e操作。C o m p o s i t o r依次遍历C o m p o s i t i o n的各个子图元,根据分行算法插入新的行和列图元。图2-6显示了得到的对象结构。图中由 C o m p o s i t o r创建和插入到对象结构中的图元图2-6 对象结构反映Compositor制导的分行生成的行列行空格C o m p o s i t o r为了计算换行必须知道字符图元的字符代码。在 2.8节,我们将会看到:怎样可以不在 G l y p h接口中添加一个特定于字符的操作,而多态地获得这个信息。以灰色背景显示。每一个C o m p o s i t o r子类都能实现一个不同的分行算法。例如,一个 S i m p l e C o m p o s i t o r可以执行得很快,而不考虑像文档“色彩”这样深奥的东西。好的色彩意味着文本和空白的平滑分布。一个Te X C o m p o s i t o r会实现完全的TEX算法 K n u 8 4,会考虑像色彩这样的东西,而以较长的格式化时间作为代价。C o m p o s i t o r-C o m p o s i t i o n类的分离确保了支持文档物理结构的代码和支持不同格式化算法的代码之间的分离。我们能增加新的 C o m p o s i t o r子类而不触及G l y p h类,反之亦然。事实上,我们通过给C o m p o s i t i o n的基本图元接口增加一个 S e t C o m p o s i t o r操作,即可在运行时刻改变分行算法。2.3.3 策略模式在对象中封装算法是 S t r a t e g y(5.9)模式的目的。模式的主要参与者是 S t r a t e g y对象(这些对象中封装了不同的算法)和它们的操作环境。其实 C o m p o s i t o r就是S t r a t e g y。它们封装了不同的格式算法。C o m p o s i t i o n就是C o m p o s i t o r策略的环境。S t r a t e g y模式应用的关键点在于为 S t r a t e g y和它的环境设计足够通用的接口,以支持一系列的算法。你不必为了支持一个新的算法而改变 S t r a t e g y或它的环境。在我们的例子中,支持子图元访问、插入和删除操作的基本 G l y p h接口就足以满足一般的用户需求,不管 C o m p o s i t o r子类使用何种算法,都足以支持其对文档的物理结构的修改。同样地,C o m p o s i t o r接口也足以支持C o m p o s i t i o n启动格式化操作。2.4 修饰用户界面我们针对L e x i用户界面考虑两种修饰,第一种是在文本编辑区域周围加边界以界定文本页;第二种是加滚动条让用户能看到同一页的不同部分。为了便于增加和去除这些修饰(特别是在运行时刻),我们不应该通过继承方式将它们加到用户界面。如果其他用户界面对象不知道存在这些修饰,那么我们就获得了最大的灵活性。这使我们无需改变其他的类就能增加和移去这些修饰。2.4.1 透明围栏从程序设计角度出发,修饰用户界面涉及到扩充已存在的代码。我们可以用继承的方式完成这种扩充,但如此运行时刻对这些修饰作重新安排则十分困难。并且同样严重的问题是,基于类继承方法通常会引起类爆炸现象。我们可以为C o m p o s i t i o n创建一个子类B o r d e r e d C o m p o s i t i o n,用来给C o m p o s i t i o n添加边界,或者以同样方式创建子类S c r o l l a b l e C o m p o s i t i o n来添加滚动条。如果我们既想要滚动条又想要边界,则可创建B o r d e r e d S c r o l l a b l e C o m p o s i t i o n等等。极端情况下,我们创建一个包含各种可能修饰组合的类。但一旦修饰类型增加,它就变得无效了。对象组合提供了一种潜在的更有效和更灵活的扩展机制,但是我们组合一些什么对象呢?既然我们知道要修饰的是已有的图元,我们就可以把修饰本身看作对象(如,类 B o r d e r的实例)。这样我们有了两个组合候选对象:图元(G l y p h)和边界(B o r d e r)。下一步是决定用谁来组合谁的问题。我们可以在边界中包含图元,这给人以边界在屏幕上包围了图元的感第2章实例研究:设计一个文档编辑器2 9觉。或者,反之在图元中包含边界,但是我们必须对相应的 G l y p h子类作修改以使边界对所有子类有效。在我们的第一个选择中,可以将画边界的代码完全保存在 B o r d e r类中,而独立于其他类。B o r d e r类看起来是什么样的呢?边界有形这个事实说明它的确应该是图元,即 B o r d e r类应该是G l y p h的子类。此外还有一个强制性的必须如此的原因:客户应该一致地对待图元,而不应关心图元是否有边界。当客户画一个简单的、无边界的图元时,就不必对它作修饰。如果那个图元包含于一个边界对象中,客户应该以画出前面简单图元同样的方法画出这个边界对象,而不应该特殊对待该边界对象。这暗示了B o r d e r接口是与 G l y p h接口匹配的。我们将B o r d e r作为G l y p h的子类可以保证这种关系。我们根据这些得出了透明围栏(Transparent Enclosure)的概念。它结合了两个概念:1)单子女(单组件)组合;2)兼容的接口。客户通常分辨不出它们是在处理组件还是组件的围栏(即,这个组件的父组件),特别是当围栏只是代理组件的所有操作时更是如此。但是围栏也能通过在代理操作之前或之后添加一些自己的操作来修改组件的行为。围栏也能有效地为组件添加状态。2.4.2 MonoGlyph我们可以将透明围栏的概念用于所有的修饰其他图元的图元。为了使这个概念具体化,我们定义G l y p h的子类M o n o G l y p h作为所有像B o r d e r这样“起修饰作用的图元”的抽象类(见图2-7)。M o n o G l y p h保存了指向一个组件的引用并且传递所有的请求给这个组件。图2-7 MonoGlyph类关系这使得M o n o G l y p h缺省情况下对客户完全透明。例如,M o n o G l y p h实现D r a w操作如下:M o n o G l y p h的子类至少重新实现一个这样的传递操作,例如,B o r d e r:D r a w首先激活基于组件的父类操作 M o n o G l y p h:D r a w,让组件做部分工作即画出边界以外的其他东西。B o r d e r:D r a w通过调用私有操作D r a w B o r d e r来画出边界。细节我们这里不赘述了:3 0设计模式:可复用面向对象软件的基础第2章实例研究:设计一个文档编辑器3 1注意B o r d e r:D r a w是怎样有效扩展父类操作来画出边界的。这与忽略 M o n o G l y p h:D r a w的调用,而完全代替父类操作是截然不同的。另一个出现在图2-7中的M o n o G l y p h子类是S c r o l l e r,它根据作为修饰的两个滚动条的位置在不同的位置画出组件。当画它的组件时,它会告诉图形系统裁剪边界以外的部分,滚动出视图以外的部分是不会显示在屏幕上的。现在我们已经有了给 L e x i文本编辑区增加边界和滚动界面所需的一切准备。我们可以在一个S c r o l l e r实例中组合已存在的C o m p o s i t i o n实例以增加滚动界面,然后再把它组合到 B o r d e r实例中。结果对象结构如图2-8所示。图2-8 嵌入对象结构注意我们也可以交换组合顺序,把一个带有边界的组合放在 S c r o l l e r实例中。这样边界可以和文本一起滚动,但我们一般不要求这么做。关键在于,透明围栏使得试验不同的选择变得很容易,使得客户和修饰代码无关。还要注意B o r d e r是怎样组合一个而不是两个或多个 G l y p h对象的。这不同于我们迄今为止所定义的组合,在那些组合中父对象是允许有多个不确定的子对象的。这里讲给某物加上边界暗示了“某物”是单个的。我们可以定义同时修饰多个对象的行为,但那样我们就不得不将多种组合和修饰概念混合起来形成所谓的行修饰、列修饰等等。因为我们已经有许多类可用来做这些组合,所这种行为对我们并没帮助。我们最好使用已有的类去做组合的工作,并通过增加新类去修饰组合的结果。使修饰独立于其他组合,既可以简化修饰类又可以减少它们的数目,还可以保证我们不重复已有的组合功能。2.4.3 Decorator模式D e c o r a t o r(4.4)模式描述了以透明围栏来支持修饰的类和对象的关系。事实上术语“修饰”的含义比我们这里讨论的更广泛。在D e c o r a t o r模式中,修饰指给一个对象增加职责的事物。我们可以想到用语义动作修饰抽象语法树、用新的转换修饰有穷状态自动机或者以属性标签修饰持久对象网等例子。D e c o r a t o r一般化了我们在L e x i中使用的方法,而使它具有更广泛的实用性。2.5 支持多种视感标准获得跨越硬件和软件平台的可移植性是系统设计的主要问题之一。将 L e x i重新定位于一个新的平台不应当要求对 L e x i进行重大的修改,否则的话就失去了重新定位 L e x i的价值。我们应当使移植尽可能地方便。移植的一大障碍是不同视感标准之间的差异性。视感标准本是用来加强某一窗口平台上各个应用之间用户界面的一致性的。这些标准定义了应用应该怎样显示和对用户请求作出反映。虽然已有的标准彼此差别不大,但用户还是可以清楚地区分它们一个应用程序在M o t i f平台上的视感决不会与其他某个平台上的完全一样,反之亦然。一个运行于多个平台的应用程序必须符合各个平台的用户界面风格。我们的设计目标就是使 L e x i符合多个已存在的视感标准,并且在新标准出现时要能很容易地增加对新标准的支持。我们也希望我们的设计能支持最大限度的灵活性:运行时刻可以改变L e x i的外观和感觉。2.5.1 对象创建的抽象我们在L e x i用户界面看到的和操作的是一个图元,它被组合于诸如行和列等不可见的图元之中。而这些不可见图元又组合了按钮、字符等可见图元,并能正确的展现它们。界面风格关于所谓的“窗口组件”(Wi d g e t s)有许多视感规则。窗口组件是关于用户界面上作为控制元素的按钮、滚动条和菜单等可视图元的另一个术语。窗口组件可以使用像字符、圆、矩形和多边形等简单图元来表示数据。我们假定用两个窗口组件图元集合来实现多个视感标准:1)第一个集合是由抽象 G l y p h子类构成的,对每一种窗口组件图元都有一个抽象 G l y p h子类。例如,抽象子类 S c r o l l B a r放大了基本的G l y p h接口,以便增加通用的滚动操作;B u t t o n是用来增加按钮有关操作的抽象类;等等。2)另一个集合是与抽象子类对应的实现不同视感标准的具体的子类的集合。例如,S c r o l l B a r可能有M o t i f S c r o l l B a r和P M S c r o l l B a r两个子类以实现相应的 M o t i f和P M(P r e s e n t a t i o nM a n a g e r)风格的滚动条。L e x i必须区分不同视感风格的窗口组件图元之间的差异。例如,当 L e x i需要在界面上放一个按钮时,它必须实例化一个有正确按钮风格的G l y p h子类(M o t i f B u t t o n、P M B u t t o n或M a c B u t t o n等)。很明显L e x i的实现不能够直接通过调用C+构造器来做这些工作,那会把按钮硬性编定为3 2设计模式:可复用面向对象软件的基础一种特殊风格,而不能在运行时刻选择风格。当 L e x i要移植到其他平台时,我们还不得不进行代码搜索以改变所有这些构造器调用。并且按钮还仅仅是 L e x i用户界面上众多窗口组件之一。对特定视感类进行构造器调用会使代码混乱,产生维护困难只要稍有遗漏,你就可能在M a c应用程序中使用了M o t i f的菜单。L e x i需要一种方法来确定创建合适窗口组件所需的视感标准。我们不仅必须避免显式的构造器调用,还必须能够很容易地替换整个窗口组件集合。可以通过抽象对象创建过程来达到上述两个要求,我们将用一个例子来说明。2.5.2 工厂类和产品类通常我们可能使用下面的C+代码来创建一个M o t i f滚动条图元实例:ScrollBar*sb=new MotifScrollBar;但如果你想使L e x i的视感依赖性最小的话,这种代码要尽量避免。假如我们按如下方法初始化s b:ScollBar*sb=guiFactory-CreateScrollBar();这里g u i F a c t o r y是M o t i f F a c t o r y类的实例。C r e a t e S c r o l l B a r为所需要的视感返回一个合适的S c r o l l B a r子类的新的实例,如 M o t i f S c r o l l B a r。一旦跟客户相连,它就等价于直接调用一个M o t i f S c r o l l B a r的构造器。但是两者有本质区别:它不像使用直接构造器那样在程序代码中提及Motif 的名字。g u i F a c t o r y对象抽象了任何视感标准下的滚动条的创建过程,而不仅仅是M o t i f滚动条的。并且g u i F a c t o r y不局限于创建滚动条,它广泛适用于包括滚动条、按钮、输入域、菜单等窗口组件图元。上述办法是可行的,其原因在于M o t i f F a c t o r y是G U I F a c t o r y的子类,而G U I F a c t o r y是定义了创建窗口组件图元公共接口的抽象类,它包含了用以实例化不同窗口组件图元的像C r e a t e S c r o l l B a r和C r e a t e B u t t o n这样的操作。G u i F a c t o r y的子类实现这些操作,并返回像M o t i f S c r o l l B a r和P M B u t t o n这样实现特定视感的图元。图2-9显示了g u i F a c t o r y对象的结果类层次结构。图2-9 GUIFactory类层次第2章实例研究:设计一个文档编辑器3 3我们说工厂(F a c t o r y)创造了产品(P r o d u c t)对象。更进一步,工厂生产的产品是彼此相关的;这种情况下,产品是相同视感的所有窗口组件。图 2-1 0显示了这样一些产品类,工厂产生窗口组件图元时要用到它们。图2-10 抽象产品类和具体子类我们要回答的最后一个问题是:G U I F a c t o r y实例是从哪儿来的?答案是哪儿方便就从哪儿来。变量g u i F a c t o r y可以是全局变量、一个众所周知的类的静态成员,或者如果整个用户界面是在一个类或一个函数中创建的,它甚至可以是局部变量。甚至有一个设计模式S i n g l e t o n(3.5)专门用来管理这样的众所周知的、只能创建一次的对象。然而,重要的是在程序中某个合适的地方来初始化 g u i F a c t o r y,这要在它被用来创建窗口组件之前,而在所需的视感标准清楚确定下来之后。如果视感在编译时刻就知道了,那么 g u i F a c t o r y能够在程序开始的时候以一个新的工厂实例简单赋值来初始化:GUIFactory*guiFactory=new MotifFactory;如果用户能通过程序启动时候的字符串来指定视感,那么创建工厂的代码可能是:还有更高级的在运行时刻选择工厂的方法。例如,你可以维持一个登记表,将字符串映射给工厂对象。这允许你无需改变已有代码就能登记新的工厂子类实例,而前面的方法则要求你改变代码。并且这样你还不必将所有平台的工厂连接到应用中。这一点很重要,因为在一个不支持M o t i f的平台上连接一个M o t i f F a c t o r y是不太可能的。3 4设计模式:可复用面向对象软件的基础第2章实例研究:设计一个文档编辑器3 5但是关键还在于一旦我们给应用配置好了正确的工厂对象,它的视感从那时起就设定好了。而如果我们改变了主意,我们还能以一个不同的视感工厂重新初始化 g u i F a c t o r y,重新构造界面。我们知道,不管怎样、何时初始化 g u i F a c t o r y,一旦这么做了,应用就可以在不修改代码的前提下创建合适的外观。2.5.3 Abstract Factory 模式工厂(F a c t o r y)和产品(P r o d u c t)是Abstract Factory(3.1)模式的主要参与者。该模式描述了怎样在不直接实例化类的情况下创建一系列相关的产品对象。它最适用于产品对象的数目和种类不变,而具体产品系列之间存在不同的情况。我们通过实例化一个特定的具体工厂对象来选择产品系列,并且以后一直使用该工厂生产产品对象。我们也能够通过用一个不同的具体工厂实例来替换原来的工厂对象以改变整个产品系列。抽象工厂模式对产品系列的强调使它区别于其他只与一种产品对象有关的创建性模式。2.6 支持多种窗口系统视感只是众多移植问题之一。另一个移植性问题就是 L e x i所运行的窗口环境。一个平台将多个互相重叠的窗口展示在一个点阵显示器上。它管理屏幕空间和键盘、鼠标到窗口的输入通道。目前存在一些互不兼容的重要的窗口系统(如M a c i n t o s h、Presentation Manager、Wi n d o w s、X等)。我们希望L e x i可以在尽可能多的窗口系统上运行,这和 Le x i要支持多个视感标准是同样的道理。2.6.1 我们是否可以使用Abstract Factory模式乍一看,这似乎又是一个使用 Abstract Factory模式的情况。但是对窗口系统移植的限制条件与视感的独立性条件是有极大不同的。在使用Abstract Factory模式时,我们假设我们能为每一个视感标准定义具体的窗口组件类。这意味着我们能从一个抽象产品类(如 S c r o l l B a r),针对一个特定标准来导出每一个具体产品(如M o t i f S c r o l l B a r、M a c S c r o l l B a r等)。现在假设我们已经有一些不同厂家的类层次结构,每一个类层次对应一个视感标准。当然,这些类层次不太可能有太多兼容之处。因而我们无法给每个窗口组件(滚动条、按钮、菜单等)都创建一个公共抽象产品类。而没有这些类Abstract Factory模式无法工作。所以我们不得不根据抽象产品接口的共同集合来调整不同的窗口组件类层次结构。只有这样我们才能在我们的抽象工厂接口中定义合适的 C r e a t e.操作。对窗口组件,我们通过开发我们自己的抽象和具体的产品类来解决这个问题。现在当我们试图使L e x i工作在已有窗口的系统时,我们面对的是类似的问题。即不同的窗口系统有不兼容的程序设计接口。但这次的麻烦更大些,因为我们不能实现我们自己的非标准窗口系统。但是事情还是有挽回的余地。像视感标准一样,窗口系统的接口也并非截然不同。因为所有的窗口系统总的来说是做同一件事的。我们可对不同的窗口系统作一个统一的抽象,在对各窗口系统的实现作一些调整,使之符合公共的接口。2.6.2 封装实现依赖关系在2.2节中,我们介绍了用以显示一个图元或图元结构的 Wi n d o w类。我们并没有指定这个对象工作的窗口系统,因为事实上它并不来自于哪个特定的窗口系统。Wi n d o w类封装了窗口要各窗口系统都要做的一些事情:它们提供了画基本几何图形的操作。它们能变成图标或还原成窗口。它们能改变自己的大小。它们能够根据需要画出(或重画出)窗口内容。例如,当它们由图标还原为窗口时,或它们在屏幕空间上重叠、出界的部分重新显示时,都要重画,如表2-3所示。表2-3 Wi n d o w s类接口责任操作窗