《设计模式:可复用面向对象软件的基础(典藏版)》
埃里克·伽玛
180个笔记
1.2 Smalltalk MVC中的设计模式
MVC通过建立一个“订购/通知”协议来分离视图和模型。
MVC的主要关系还是由Observer、Composite和Strategy三个设计模式给出的。
1.4 设计模式的编目
Abstract Factory(3.1):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。Adapter(4.1):将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。Bridge(4.2):将抽象部分与它的实现部分分离,使它们都可以独立地变化。Builder(3.2):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。Chain of Responsibility(5.1):解除请求的发送者和接收者之间的耦合,使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。Command(5.2):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。Composite(4.3):将对象组合成树形结构以表示“部分–整体”的层次结构。Composite使得客户对单个对象和组合对象的使用具有一致性。
Decorator(4.4):动态地给一个对象添加一些额外的职责。就扩展功能而言,Decorator模式比生成子类方式更为灵活。Facade(4.5):为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。Factory Method(3.3):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。Flyweight(4.6):运用共享技术有效地支持大量细粒度的对象。Interpreter(5.3):给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。Iterator(5.4):提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。Mediator(5.5):用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
Memento(5.6):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
Observer(5.7):定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。Prototype(3.4):用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。Proxy(4.7):为其他对象提供一个代理以控制对这个对象的访问。Singleton(3.5):保证一个类仅有一个实例,并提供一个访问它的全局访问点。State(5.8):允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。Strategy(5.9):定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。Template Method(5.10):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤。Visitor(5.11):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
1.5 组织编目
- 模式依据其目的可分为创建型(creational)、结构型(structural)和行为型(behavioral)三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。
1.6.4 描述对象的实现
- C++中接口继承的标准方法是公有继承一个含(纯)虚成员函数的类。C++中纯接口继承接近于公有继承纯抽象类,纯实现继承或纯类继承接近于私有继承
1.6.5 运用复用机制
对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另外,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承。
委托的主要优点在于它便于运行时组合对象操作以及改变这些操作的组合方式。假定矩形对象和圆对象有相同的类型,我们只需要简单地用圆对象替换矩形对象,得到的窗口就是圆形的。
有一些模式使用了委托,如State(5.8)、Strategy(5.9)和Visitor(5.11)。在State模式中,一个对象将请求委托给一个描述当前状态的State对象来处理。在Strategy模式中,一个对象将一个特定的请求委托给一个描述请求执行策略的对象,一个对象只会有一个状态,但它对不同的请求可以有许多策略。这两个模式的目的都是通过改变受托对象来改变委托对象的行为。在Visitor中,对象结构的每个元素上的操作总是被委托到Visitor对象。
其他模式则没有这么多地用到委托。Mediator(5.5)引进了一个作为其他对象间通信的中介的对象。有时,Mediator对象只是简单地将请求转发给其他对象;有时,它沿着指向自己的引用来传递请求,使用真正意义的委托。Chain of Responsibility(5.1)通过将请求沿着对象链传递来处理请求,有时,这个请求本身带有一个接受请求对象的引用,这时该模式就使用了委托。Bridge(4.2)将实现和抽象分离开,如果抽象和一个特定实现非常匹配,那么这个实现可以代理抽象的操作。委托是对象组合的特例。它告诉你对象组合作为一个代码复用机制可以替代继承。
1.6.6 关联运行时和编译时的结构
- C++中,聚合可以通过定义表示真正实例的成员变量来实现,但更通常的是将这些成员变量定义为实例指针或引用;相识也是以指针或引用来实现的。从根本上讲,是聚合还是相识是由你的意图而不是显式的语言机制决定的。尽管它们之间的区别在编译时的结构中很难看出来,但这些区别还是很大的。聚合关系使用较少且比相识关系更持久;而相识关系则出现频率较高,但有时只存在于一个操作期间,相识也更具动态性,使得它在源代码中更难被辨别出来。
1.6.7 设计应支持变化
下面阐述了一些导致重新设计的一般原因,以及解决这些问题的设计模式:1)通过显式地指定一个类来创建对象 在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。设计模式:Abstract Factory(3.1),Factory Method(3.3),Prototype(3.4)。2)对特殊操作的依赖 当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。为避免把请求代码写死,你将可以在编译时或运行时很方便地改变响应请求的方法。设计模式:Chain of Resposibility(5.1),Command(5.2)。3)对硬件和软件平台的依赖 外部的操作系统接口和应用编程接口(API)在不同的软硬件平台上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至很难跟上本地平台的更新。所以设计系统时限制其平台相关性就很重要了。设计模式:Abstract Factory(3.1),Bridge(4.2)。
4)对对象表示或实现的依赖 知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化。设计模式:Abstract Factory(3.1),Bridge(4.2),Memento(5.6),Proxy(4.7)。5)算法依赖 算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。设计模式:Builder(3.2),Iterator(5.4),Strategy(5.9),Template Method(5.10),Visitor(5.11)。
6)紧耦合 紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,你必须理解和改变其他许多类。这样的系统是一个很难学习、移植和维护的密集体。松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。设计模式:Abstract Factory(3.1),Command(5.2),Facade(4.5),Mediator(5.5),Observer(5.7),Chain of Responsibility(5.1)。
7)通过生成子类来扩充功能 通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。例如,重定义一个操作可能需要重定义其他操作。一个被重定义的操作可能需要调用继承下来的操作。并且子类方法会导致类爆炸,因为即使对于一个简单的扩充,你也不得不引入许多新的子类。一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,可以定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。设计模式:Bridge(4.2),Chain of Responsibility(5.1),Composite(4.3),Decorator(4.4),Observer(5.7),Strategy(5.9)。
8)不能方便地对类进行修改 有时你不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。设计模式:Adapter(4.1),Decorator(4.4),Visitor(5.11)。
当框架和它所使用的设计模式一起写入文档时,我们可以得到另外一个好处[BJ94]。了解设计模式的人能较快地洞悉框架。甚至不了解设计模式的人也可以从产生框架文档的结构中受益。加强文档工作对于所有软件而言都是重要的,但对于框架其重要性显得尤为突出。学会使用框架常常是一个必须克服很多困难的过程。设计模式虽然无法彻底克服这些困难,但它通过对框架设计的主要元素做更显式的说明可以降低框架学习的难度。
当然可以有比我们的模式更特殊的设计模式(例如,分布式系统和并发程序的设计模式),尽管这些模式不会像框架那样描述应用的体系结构。
框架变得越来越普遍和重要。它们是面向对象系统获得最大复用的方式。较大的面向对象应用将会由多层彼此合作的框架组成。应用的大部分设计和代码将来自它所使用的框架或受其影响。
2.3 格式化
- 表示和格式化是不同的,记录文档物理结构的能力并没有告诉我们怎样得到一个特殊的格式化结构。
2.4.3 Decorator模式
- 在Decorator模式中,修饰指给一个对象增加职责的事物。我们可以想到用语义动作修饰抽象语法树、用新的转换修饰有穷状态自动机或者以属性标签修饰持久对象网络等例子。Decorator一般化了我们在Lexi中使用的方法,使它具有更广泛的实用性。
2.5.2 工厂类和产品类
- 还有更高级的在运行时选择工厂的方法。例如,你可以维护一个登记表,将字符串映射给工厂对象。这允许你无须改变已有代码就能登记新的工厂子类实例,而前面的方法则要求你改变代码。并且这样你还不必将所有平台的工厂连接到应用中。这一点很重要,因为在一个不支持Motif的平台上连接一个MotifFactory是不太可能的。
2.6.2 封装实现依赖关系
现在我们已经为Lexi定义了工作的窗口接口,那么真正与平台相关的窗口是从哪里来的?既然我们不能实现自己的窗口系统,那么这个窗口抽象必须用目标窗口系统平台来实现。怎样实现?一种方法是实现Window类和它的子类的多个版本,每个版本对应一个窗口平台。当我们在一给定平台上建立Lexi时,我们选择一个相应的版本。但想象一下,维护问题实在令人头疼,我们已经保存了多个名字都是“Window”的类,而每一个类实现于一个不同的窗口系统。另一种方法是为每一个窗口层次结构中的类创建特定实现的子类,但这同样会产生我们在试图增加修饰时遇到的子类数目爆炸问题。这两种方法还都有另一个缺点:没有在编译以后改变所用窗口系统的灵活性。所以我们还不得不保持若干不同的可执行程序。
既然这两种方法都没有吸引力,那么我们还能做些什么呢?那就是我们在讨论格式化和修饰时都做过的:对变化的概念进行封装。现在所变化的是窗口系统实现。如果我们能在一个对象中封装窗口系统的功能,那么就能根据对象接口实现Window类及其子类。更进一步讲,如果那个接口能够提供我们所感兴趣的所有窗口系统的服务,那么我们无须改变Window类或其子类,也能支持不同的窗口系统。我们可以通过简单地传递合适的窗口系统封装对象,给窗口系统配置窗口对象。我们甚至能在运行时配置窗口。
2.6.3 Window和WindowImp
- 2.用WindowImp来配置窗口我们还没有论述的一个关键问题是:怎样用一个合适的WindowImp子类来配置一个窗口?也就是说,什么时候初始化_imp,谁知道正在使用的是什么窗口系统(也就是哪一个WindowImp子类)?窗口在能做它所感兴趣的事情之前,都需要某种WindowImp。这些问题的答案存在很多种可能性,但我们只关注使用Abstract Factory(3.1)模式的情形。我们可以定义一个抽象工厂类WindowSystemFactory,它提供了创建与窗口系统有关的各种实现对象的接口:
2.7.1 封装一个请求
- 现在所缺少的是一种机制,即允许我们用菜单项所执行的请求对菜单项进行参数化。这种方法可以避免子类的剧增并可获得运行时更大的灵活性。我们可以调用一个函数来参数化一个MenuItem,但是至少由于以下三个原因,这还不是很完整的解决方案:1)它还没有解决撤销/重做问题。2)很难将状态和函数联系起来。例如,一个改变字体的函数需要知道是哪一种字体。3)函数很难扩充,并且很难部分地复用它们。以上这些表明,我们应该用对象而不是函数来参数化MenuItem。我们可以通过继承扩充和复用请求实现。我们也可以保存状态和实现撤销/重做功能。这里是另一个封装变化概念的例子,即封装请求。我们将在command对象中封装每一个请求。
2.7.3 撤销和重做
- 有时需要在运行时决定撤销和重做。如果选中文本的字体就是某个请求要修改的字体,那么这个请求是无意义的,它不会产生任何影响。假如选中了一些文字,然后发一个无意义的字体改变请求。那么接下来撤销该请求会产生什么结果呢?是不是一个无意义的字体改变操作,会引起撤销请求同样做一些无意义的事?应该不是这样的。如果用户多次重复无意义的字体改变操作,他应该不必执行相同数目的撤销操作才可以返回到上一次有意义的操作。如果执行一个命令不产生任何影响,那么就不需要相应的撤销操作。因此为了决定一个命令是否可以撤销,我们给Command接口增加了一个抽象的Reversible操作,它返回Boolean值。子类可以重定义这个操作,以根据运行时情况返回true或false。
2.7.4 命令历史记录
- 支持任意层次的撤销和重做命令的最后一步是定义一个命令历史记录(command history)或已执行命令的列表(或已被撤销的一些命令)。从概念上理解,命令的历史记录看起来如以下图形所示。
2.8.1 访问分散的信息
- 许多分析要求逐字检查文本,而我们需要分析的文本是分散在图元对象的层次结构中的。为了检查这种结构中的文本,我们需要一种访问机制以知道数据结构中所保存的图元对象。一些图元可能以链表保存它们的子图元,另一些可能用数组保存,还有一些可能使用更复杂的数据结构。我们的访问机制应该能处理所有这些可能性。此外,更为复杂的情况是,不同分析算法将会以不同方式访问信息。大多数分析算法总是从头到尾遍历文本,但也有一些恰恰相反——例如,逆向搜索的访问顺序是从后往前而不是从前往后。算术表达式的求值可能需要一个中序的遍历过程。所以我们的访问机制必须能适应不同的数据结构,并且我们还必须支持不同的遍历方法,如前序、后序和中序。
2.8.2 封装访问和遍历
- 注意我们已经放弃了图元接口的整数索引,这样就不会偏重于某种数据结构。我们也使得客户不必自己实现通用的遍历方法。但是该方法仍然有一些问题。举个例子,它在不扩展枚举值或增加新的操作的条件下,不能支持新的遍历方式。比方说,我们想要修改一下先序遍历,使它能自动跳过非文本图元。我们就不得不改变枚举类型Traversal,使它包含TEXTUAL_PREORDER这样的值。我们最好避免改变已有的声明。把遍历机制完全放到Glyph类层次中,将会导致修改和扩充时不得不改变一些类,也使得复用遍历机制来遍历其他对象结构很困难,并且在一个结构上不能同时进行多个遍历。再一次强调,一个好的解决方案是封装那些变化的概念,在本例中我们指的是访问和遍历机制。我们引入一类称为迭代器(iterator)的对象,它们的目的是定义这些机制的不同集合。我们可以通过继承来统一访问不同的数据结构和支持新的遍历方式,同时不改变图元接口或打乱已有的图元实现。
2.8.3 Iterator类及其子类
- 注意Iterator类层次结构是怎样允许我们不改变图元类而增加新的遍历方式的——如PreorderIterator所示,我们只需要创建Iteraror子类,并给它增加一个新的遍历算法即可。Glyph子类给客户提供相同的接口去访问它们的子女,并不揭示其底层的数据结构。由于Iterator保存了自己的遍历状态,所以我们能同时执行多个遍历,甚至可以对相同的结构进行同时遍历。尽管我们在本例中的遍历是针对图元结构的,但我们没有理由不可以将像PreorderIterator这样的类参数化,使其能遍历其他类型的对象结构。我们可以使用C++的模板技术来做这件事,这样我们在遍历其他结构时就能复用PreorderIterator的机制。
2.8.6 封装分析
- 这段代码相当拙劣。它依赖于比较高深的像类型的安全转换这样的能力,并且难以扩展。无论何时当我们改变Glyph类层次时,都要记住修改这个函数。事实上,这也是面向对象语言力图消除的那种代码。
2.8.8 Visitor模式
- 访问者所能访问的类之间无须通过一个公共父类关联起来。也就是说,访问者能跨越类层次结构。
第3章 创建型模式
随着系统演化得越来越依赖于对象组合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(hard-coding)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类。
如果CreateMaze调用虚函数而不是构造器来创建它需要的房间、墙壁和门,那么你可以创建一个MazeGame的子类并重定义这些虚函数,从而改变被实例化的类。这一方法是Factory Method(3.3)模式的一个例子。·如果传递一个对象给CreateMaze作为参数来创建房间、墙壁和门,那么你可以传递不同的参数来改变房间、墙壁和门的类。这是Abstract Factory(3.1)模式的一个例子。·如果传递一个对象给CreateMaze,这个对象可以在它所建造的迷宫中使用增加房间、墙壁和门的操作来全面创建一个新的迷宫,那么你可以使用继承来改变迷宫的一些部分或迷宫的建造方式。这是Builder(3.2)模式的一个例子。·如果CreateMaze由多种原型的房间、墙壁和门对象参数化,它复制并将这些对象增加到迷宫中,那么你可以用不同的对象替换这些原型对象以改变迷宫的构成。这是Prototype(3.4)模式的一个例子。
剩下的创建型模式Singleton(3.5)可以保证每个游戏中仅有一个迷宫而且所有的游戏对象都可以迅速访问它——不需要求助于全局变量或函数。Singleton也使得迷宫易于扩展或替换,且不需要变动已有的代码。
3.1 Abstract Factory(抽象工厂)——对象创建型模式
客户仅通过WidgetFactory接口创建窗口组件,而并不知道哪些类实现了特定视感风格的窗口组件。换言之,客户仅与抽象类定义的接口交互,而不使用特定的具体类的接口。
4)难以支持新种类的产品 难以扩展抽象工厂以生产新种类的产品。这是因为AbstractFactory接口确定了可以被创建的产品集合。支持新种类的产品就需要扩展该工厂接口,这将涉及AbstractFactory类及其所有子类的改变。我们会在实现一节讨论这个问题的一个解决办法。
该方法即使不需要强制类型转换,仍有一个本质的问题:所有的产品将返回类型所给定的相同的抽象接口返回给客户。客户将不能区分或对一个产品的类别进行安全的假定。如果一个客户需要进行与特定子类相关的操作,而这些操作则不能通过抽象接口得到。虽然客户可以实施一个向下类型转换(downcast)(例如在C++中用dynamic_cast),但这并不总是可行或安全的,因为向下类型转换可能会失败。这是一个典型的高度灵活和可扩展接口的权衡考虑。
3.2 Builder(生成器)——对象创建型模式
每种转换器类将创建和装配一个复杂对象的机制隐含在抽象接口的后面。转换器独立于阅读器,阅读器负责对一个RTF文档进行语法分析。Builder模式描述了所有这些关系。每一个转换器类在该模式中被称为生成器(builder),而阅读器则称为导向器(director)。在上面的例子中,Builder模式将分析文本格式的算法(即RTF文档的语法分析程序)与描述怎样创建和表示一个转换后格式的算法分离开来。这使我们可以复用RTFReader的语法分析算法,根据RTF文档创建不同的文本表示——仅需使用不同的TextConverter的子类配置该RTFReader即可。
6.协作·客户创建Director对象,并用它所想要的Builder对象进行配置。·一旦生成了产品部件,导向器就会通知生成器。·生成器处理导向器的请求,并将部件添加到该产品中。·客户从生成器中检索产品。下面的交互图说明了Builder和Director是如何与一个客户协作的。
3)它使你可对构造过程进行更精细的控制 Builder模式与一下子就生成产品的创建型模式不同,它是在导向器的控制下一步一步构造产品的。仅当该产品完成时导向器才从生成器中取回它。因此Builder接口相比其他创建型模式能更好地反映产品的构造过程。这使你可以更精细地控制构建过程,从而能更精细地控制所得产品的内部结构。
一个关键的设计问题在于构造和装配过程的模型。构造请求的结果只是被添加到产品中,通常这样的模型就已足够了。在RTF的例子中,生成器转换下一个标记并将它添加到它已经转换了的文本中。但有时你可能需要访问前面已经构造了的产品部件。我们在代码示例一节所给出的Maze例子中,MazeBuilder接口允许你在已经存在的房间之间增加一扇门。像语法分析树这样自底向上构建的树形结构就是另一个例子。在这种情况下,生成器会将子结点返回给导向器,然后导向器将它们回传给生成器去创建父结点。
3)在Builder中缺省的方法为空 C++中,生成方法故意不声明为纯虚成员函数,而是把它们定义为空方法,这使客户只重定义他们所感兴趣的操作。
10.已知应用RTF转换器应用来自ET++[WGM88]。它的文本生成模块使用一个生成器处理以RTF格式存储的文本。生成器在Smalltalk-80[Par90]中是一个通用的模式:·编译子系统中的Parser类是一个Director,它以一个ProgramNodeBuilder对象作为参数。每当Parser对象识别出一个语法结构时,它就通知它的ProgramNodeBuilder对象。当这个语法分析器做完时,它向该生成器请求它生成的语法分析树并将语法分析树返回给客户。·ClassBuilder是一个生成器,Class使用它为自己创建子类。在这个例子中,一个Class既是Director也是Product。·ByteCodeStream是一个生成器,它将一个被编译了的方法创建为字节数组。ByteCodeStream不是Builder模式的标准使用,因为它生成的复杂对象被编码为一个字节数组,而不是正常的Smalltalk对象。但ByteCodeStream的接口是一个典型的生成器,而且将很容易用一个将程序表示为组合对象的不同的类来替换ByteCode-Stream。
自适应通信环境(Adaptive Communications Environment)中的服务配置者(Service Configurator)框架使用生成器来构造运行时动态连接到服务器的网络服务构件[SS94]。这些构件使用一个被LALR(1)语法分析器进行语法分析的配置语言来描述。这个语法分析器的语义动作对将信息加载给服务构件的生成器进行操作。在这个例子中,语法分析器就是Director。
Abstract Factory(3.1)与Builder相似,因为它也可以创建复杂对象。主要的区别是Builder模式着重于一步步构造一个复杂对象。而Abstract Factory着重于多个系列的产品对象(简单的或是复杂的)。Builder在最后一步返回产品,而对于Abstract Factory来说,产品是立即返回的。Composite(4.3)通常是用Builder生成的。
3.3 Factory Method(工厂方法)——对象创建型模式
2)参数化工厂方法 该模式的另一种情况使得工厂方法可以创建多种产品。工厂方法采用一个标识要被创建的对象种类的参数。工厂方法创建的所有对象将共享Product接口。在Document的例子中,Application可能支持不同种类的Document。你给CreateDocument传递一个外部参数来指定将要创建的文档的种类。图形编辑框架Unidraw[VL90]使用这种方法来重构存储在磁盘上的对象。Unidraw定义了一个Creator类,该类拥有一个以类标识符为参数的工厂方法Create。类标识符指定要被实例化的类。当Unidraw将一个对象存盘时,它首先写类标识符,然后是它的实例变量。当它从磁盘中重构该对象时,它首先读取的是类标识符。一旦类标识符被读取,这个框架就将该标识符作为参数,调用Create。Create到构造器中查询相应的类并用它实例化对象。最后,Create调用对象的Read操作,读取磁盘上剩余的信息并初始化该对象的实例变量。一个参数化的工厂方法具有如下的一般形式,此处MyProduct和YourProduct是Product的子类:
只要你使用按需创建产品的访问者操作,很小心地访问产品,就可以避免这一点。构造器只是将产品初始化为0,而不是创建一个具体产品。访问者返回该产品。但首先它要检查确定该产品的存在,如果产品不存在,访问者就创建它。这种技术有时被称为惰性初始化(lazy initialization)。下面的代码给出了一个典型的实现:
4)使用模板以避免创建子类 正如我们已经提及的,工厂方法另一个潜在的问题是它们可能仅为了创建适当的Product对象而迫使你创建Creator子类。在C++中另一个解决方法是提供Creator的一个模板子类,它使用Product类作为模板参数:
使用这个模板,客户仅提供产品类——而不需要创建Creator的子类。
5)命名约定 使用命名约定是一个好习惯,它可以清楚地说明你正在使用工厂方法。例如,Macintosh的应用框架MacApp[App89]总是声明那些定义为工厂方法的抽象操作为Class*DoMakeClass(),此处Class是Product类。
工厂方法主要用于工具包和框架中。前面的文档例子是MacApp和ET++[WGM88]中的一个典型应用。操纵器的例子来自Unidraw。Smalltalk-80 Model/View/Controller框架中的类视图(Class View)有一个创建控制器的方法defaultController,它有点类似于一个工厂方法[Par90]。但是View的子类通过定义defaultControllerClass来指定它们默认的控制器的类。defaultControllerClass返回defaultController所创建实例的类,因此它才是真正的工厂方法,即子类应该重定义它。Smalltalk-80中一个更为深奥的例子是由Behavior(用来表示类的所有对象的超类)定义的工厂方法parserClass。这使得一个类可以对它的源代码使用一个定制的语法分析器。例如,一个客户可以定义一个类SQLParser来分析嵌入了SQL语句的类的源代码。Behavior类实现了parserClass,返回一个标准的Smalltalk Parser类。一个包含嵌入SQL语句的类重定义了该方法(以类方法的形式)并返回SQLParser类。
IONA Technologies的Orbix ORB系统[ION94]在对象给一个远程对象引用发送请求时,使用Factory Method生成一个适当类型的代理(参见Proxy(4.7))。Factory Method使得易于替换缺省代理。比如说,可以用一个使用客户端高速缓存的代理来替换。
Abstract Factory(3.1)经常用工厂方法来实现。Abstract Factory模式中动机一节的例子也对Factory Method进行了说明。工厂方法通常在Template Method(5.10)中被调用。在上面的文档例子中,NewDocument就是一个模板方法。Prototype(3.4)不需要创建Creator的子类。但是,它们通常要求一个针对Product类的Initialize操作。Creator使用Initialize来初始化对象,而Factory Method不需要这样的操作。
3.4 Prototype(原型)——对象创建型模式
我们甚至可以进一步使用Prototype模式来减少类的数目。我们使用不同的类来表示全音符和半音符,但可能不需要这么做。它们可以是使用不同位图和时延初始化的相同的类的实例。一个创建全音符的工具就是这样的GraphicTool,它的原型是一个被初始化成全音符的MusicalNote。这可以极大地减少系统中类的数目,同时也更易于在音乐编辑器中增加新的音符。
3)改变结构以指定新对象 许多应用由部件和子部件来创建对象。例如电路设计编辑器就是由子电路来构造电路的。[插图]为方便起见,这样的应用通常允许你实例化复杂的、用户定义的结构,比方说,一次又一次地重复使用一个特定的子电路。Prototype模式也支持这一点。我们仅需将这个子电路作为一个原型增加到可用的电路元素选择板中。只要组合电路对象将Clone实现为一个深拷贝(deep copy),具有不同结构的电路就可以是原型了。
5)用类动态配置应用 一些运行时环境允许你动态地将类装载到应用中。在像C++这样的语言中,Prototype模式是利用这种功能的关键。一个希望创建动态载入类的实例的应用不能静态引用类的构造器,而应该由运行环境在载入时自动创建每个类的实例,并用原型管理器来注册这个实例(参见实现一节)。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。ET++应用框架[WGM88]有一个运行系统就是使用这一方案的。Prototype的主要缺陷是每一个Prototype的子类都必须实现Clone操作,这可能很困难。例如,当所考虑的类已经存在时就难以新增Clone操作。当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难。
C++中的缺省拷贝构造器实现按成员拷贝,这意味着在拷贝的对象和原来的对象之间是共享指针的。但克隆一个结构复杂的原型通常需要深拷贝,因为复制对象和原对象必须相互独立。因此你必须保证克隆对象的构件也是对原型的构件的克隆。克隆迫使你决定如果所有东西都被共享了该怎么办。如果系统中的对象提供了Save和Load操作,那么你只需通过保存对象和立刻载入对象,就可以为Clone操作提供一个缺省实现。Save操作将该对象保存在内存缓冲区中,而Load则通过从该缓冲区中重构这个对象来创建一个副本。
etgdb是一个基于ET++的调试器前端,它为不同的行导向(line-oriented)调试器提供了一个点触式(point-and-click)接口。每个调试器有相应的DebuggerAdaptor子类。例如,GdbAdaptor使etgdb适应GNU的gdb命令语法,而SunDbxAdaptor则使etgdb适应Sun的dbx调试器。etgdb没有一组硬编码于其中的DebuggerAdaptor类。它从环境变量中读取要用到的适配器的名字,在一个全局表中根据特定名字查询原型,然后克隆这个原型。新的调试器通过与该调试器相对应的DebuggerAdaptor链接,可以被添加到etgdb中。Mode Composer中的“交互技术库”(interaction technique library)存储了支持多种交互技术的对象的原型[Sha90]。将Mode Composer创建的任一交互技术放入这个库中,它就可以被作为一个原型使用。Prototype模式使得Mode Composer可支持数目无限的交互技术。前面讨论过的音乐编辑器的例子是基于Unidraw绘图框架的[VL90]。
3.5 Singleton(单件)——对象创建型模式
5)比类操作更灵活 另一种封装单件功能的方式是使用类操作(即C++中的静态成员函数或者是Smalltalk中的类方法)。但这两种语言技术都难以改变设计以允许一个类有多个实例。此外,C++中的静态成员函数不是虚函数,因此子类不能多态地重定义它们。
一个更灵活的方法是使用一个单件注册表(registry of singleton)。可能的Singleton类的集合不是由Instance定义的,Singleton类可以根据名字在一个众所周知的注册表中注册它们的单件实例。
3.6 创建型模式的讨论
Prototype模式对绘图编辑器框架可能是最好的,因为它仅需要为每个Graphic类实现一个Clone操作。这就减少了类的数目,并且Clone可以用于其他目的而不仅仅是纯粹的实例化(例如,一个Duplicate菜单操作)。
Factory Method使一个设计可以定制且只略微有一些复杂。其他设计模式需要新的类,而Factory Method只需要一个新的操作。人们通常将Factory Method作为一种标准的创建对象的方法。但是当被实例化的类根本不发生变化或实例化出现在子类可以很容易重定义的操作(比如初始化操作)中时,这就不必要了。
第4章 结构型模式
结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可以在运行时改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
在Proxy(4.7)模式中,proxy对象作为其他对象的一个方便的替代或占位符。它的使用可以有多种形式。例如,它可以在局部空间中代表一个远程地址空间中的对象,也可以表示一个要求被加载的较大的对象,还可以用来保护对敏感对象的访问。Proxy模式还提供了对对象的一些特有性质的一定程度上的间接访问,从而它可以限制、增强或修改这些性质。
Decorator(4.4)模式描述了如何动态地为对象添加职责。Decorator模式是一种结构型模式。这一模式采用递归方式组合对象,从而允许你添加任意多的对象职责。例如,一个包含用户界面组件的Decorator对象可以将边框或阴影这样的装饰添加到该组件中,或者它可以将窗口滚动和缩放这样的功能添加到组件中。将一个Decorator对象嵌套在另一个对象中就可以很简单地增加两个装饰,添加其他的装饰也是如此。因此,每个Decorator对象必须与其组件的接口兼容并且保证将消息传递给它。Decorator模式在转发一条信息之前或之后都可以完成它的工作(比如绘制组件的边框)。
4.1 Adapter(适配器)——类对象结构型模式
考虑一个双向适配器,它将图形编辑框架Unidraw[VL90]与约束求解工具箱QOCA[HHMV92]集成起来。这两个系统都有一些类,这些类显式地表示变量:Unidraw含有类StateVariable,QOCA含有类ConstraintVariable,如下图所示。为了使Unidraw与QOCA协同工作,必须首先使类ConstraintVariable与类StateVariable相匹配;而为了将QOCA的求解结果传递给Unidraw,必须使StateVariable与ConstraintVariable相匹配。
这一方案中包含了一个双向适配器ConstraintStateVariable,它是类ConstraintVariable与类StateVariable的共同子类,ConstraintStateVariable使得两个接口互相匹配。在该例子中多重继承是一个可行的解决方案,因为被适配类的接口差异较大。双向适配器与这两个被匹配的类都兼容,在这两个系统中它都可以工作。
类适配器采用多重继承适配接口。类适配器的关键是用一个分支继承接口,而用另外一个分支继承接口的实现部分。通常C++中做出这一区分的方法是:用公共方式继承接口;用私有方式继承接口的实现。
模式Bridge(4.2)的结构与对象适配器类似,但是Bridge模式的出发点不同:Bridge的目的是将接口部分和实现部分分离,从而可以对它们较为容易也相对独立地加以改变。而Adapter则意味着改变一个已有对象的接口。Decorator(4.4)模式增强了其他对象的功能而同时又不改变它的接口,因此Decorator对应用程序的透明性比适配器要好。结果是Decorator支持递归组合,而纯粹使用适配器是不可能实现这一点的。模式Proxy(4.7)在不改变它的接口的条件下,为另一个对象定义了一个代理。
4.2 Bridge(桥接)——对象结构型模式
对Window子类的所有操作都是用WindowImp接口中的抽象操作实现的。这就将窗口的抽象与系统平台相关的实现部分分离开来。因此,我们将Window与WindowImp之间的关系称为桥接,因为它在抽象类与它的实现之间起到了桥梁作用,使它们可以独立地变化。
将Abstraction与Implementor分离有助于降低对实现部分编译时的依赖性,当改变一个实现类时,并不需要重新编译Abstraction类和它的客户程序。为了保证一个类库的不同版本之间的二进制兼容性,一定要有这个性质。
Carolan[Car89]用“常露齿嘻笑的猫”(Cheshire Cat)描述这一分离机制。在C++中,Implementor类的类接口可以在一个私有的头文件中定义,这个文件不提供给客户。这样你就对客户彻底隐藏了一个类的实现部分。
如果Abstraction知道所有的ConcreteImplementor类,它就可以在它的构造器中对其中的一个类进行实例化,它可以通过传递给构造器的参数确定实例化哪一个类。例如,如果一个collection类支持多重实现,就可以根据collection的大小决定实例化哪一个类。链表的实现可以用于较小的collection类,而hash表则可用于较大的collection类。另外一种方法是首先选择一个缺省的实现,然后根据需要改变这个实现。例如,如果一个collection的大小超出了一定的阈值,它将会切换它的实现,使之更适用于表目较多的collection。也可以代理给另一个对象,由它一次决定。在Window/WindowImp的例子中,我们可以引入一个factory对象(参见Abstract Factory(3.1)),该对象的唯一职责就是封装系统平台的细节。这个对象知道应该为所用的平台创建何种类型的WindowImp对象,Window仅需向它请求一个WindowImp,而它会返回正确类型的WindowImp对象。这种方法的优点是Abstraction类不和任何一个Implementor类直接耦合。
ET++的Window/WindowPort设计扩展了Bridge模式,因为WindowPort保留了一个指回Window的指针。WindowPort的Implementor类用这个指针通知Window对象发生了一些与WindowPort相关的事件,例如输入事件的到来、窗口调整大小等。
Abstract Factory(3.1)模式可以用来创建和配置一个特定的Bridge模式。Adapter(4.1)模式用来帮助无关的类协同工作,它通常在系统设计完成后才会被使用。然而,Bridge模式则是在系统开始时就被使用,它使得抽象接口和实现部分可以独立进行改变。
4.3 Composite(组合)——对象结构型模式
7)使用高速缓冲存储改善性能 如果你需要对组合进行频繁的遍历或查找,Composite类可以缓冲存储对它的子结点进行遍历或查找的相关信息。Composite可以缓冲存储实际结果或者仅仅是一些用于缩短遍历或查询长度的信息。例如,动机一节的例子中Picture类能高速缓冲存储其子部件的边界框,在绘图或选择期间,当子部件在当前窗口中不可见时,这个边界框使得Picture不需要再进行绘图或选择。一个组件发生变化时,它的父部件原先缓冲存储的信息也变得无效。在组件知道其父部件时,这种方法最为有效。因此,如果你使用高速缓冲存储,需要定义一个接口来通知组合组件它们所缓冲存储的信息无效。
几乎在所有面向对象的系统中都有Composite模式的应用实例。在Smalltalk的Model/View/Controller[KP88]结构中,原始View类就是一个Composite,几乎每个用户界面工具箱或框架都遵循这些步骤,其中包括ET++(用VObjects[WGM88])和InterViews(Style[LCI+92]、Graphics[VL88]和Glyphs[CL90])。很有趣的是Model/View/Controller中的原始View有一组子视图,换句话说,View既是Component类,又是Composite类。4.0版的Smalltalk-80用VisualComponent类修改了Model/View/Controller,VisualComponent类含有子类View和CompositeView。
通常,部件–父部件连接用于Responsibility of Chain(5.1)模式。Decorator(4.4)模式经常与Composite模式一起使用。当装饰和组合一起使用时,它们通常有一个公共的父类。因此装饰必须支持具有Add、Remove和GetChild操作的Component接口。Flyweight(4.6)让你共享组件,但不再能引用其父部件。Itertor(5.4)可用来遍历Composite。Visitor(5.11)将本来应该分布在Composite和Leaf类中的操作和行为局部化。
4.4 Decorator(装饰)——对象结构型模式
一种较为灵活的方式是将组件嵌入另一个对象中,由这个对象添加边框。我们称这个嵌入的对象为装饰。这个装饰与它所装饰的组件接口一致,因此它对使用该组件的客户透明。它将客户请求转发给该组件,并且可能在转发前后执行一些额外的动作(例如画一个边框)。透明性使得你可以递归地嵌套多个装饰,从而可以添加任意多的功能,如下图所示。
·Decorator将请求转发给它的Component对象,并有可能在转发请求前后执行一些附加的动作。
但我们想要一个有边界和可以滚动的TextView,因此我们在将它放入窗口之前对其进行装饰:
Stream抽象类维持了一个内部缓冲区并提供一些操作(PutInt、PutString)用于将数据存入流中。一旦这个缓冲区满了,Stream就会调用抽象操作HandleBufferFull进行实际数据传输。在FileStream中重定义了这个操作,将缓冲区中的数据传输到文件中去。这里的关键类是StreamDecorator,它维持了一个指向组件流的指针并将请求转发给它,StreamDecorator子类重定义HandleBufferFull操作并且在调用StreamDecorator的HandleBufferFull操作之前执行一些额外的动作。例如,CompressingStream子类用于压缩数据,而ASCII7Stream将数据转换成7位ASCII码。现在我们创建FileStream类,它首先将数据压缩,然后将压缩了的二进制数据转换成7位ASCII码,我们用CompressingStream和ASCII7Stream装饰FileStream:
Adapter(4.1):Decorator模式不同于Adapter模式,因为装饰仅改变对象的职责而不改变它的接口;而适配器将给对象一个全新的接口。Composite(4.3):可以将装饰视为一个退化的、仅有一个组件的组合。然而,装饰仅给对象添加一些额外的职责——它的目的不在于对象聚集。Strategy(5.9):用一个装饰可以改变对象的外表;而Strategy模式使得你可以改变对象的内核。这是改变对象的两种途径。
4.5 Facade(外观)——对象结构型模式
·当你需要构建一个层次结构的子系统时,使用Facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,可以让它们仅通过Facade进行通信,从而简化了它们之间的依赖关系。
2)它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。松耦合关系使得子系统的组件变化不会影响到它的客户。Facade模式有助于建立层次结构系统,也有助于对对象之间的依赖关系分层。Facade模式可以消除复杂的循环依赖关系。这一点在客户程序与子系统分别实现的时候尤为重要。在大型软件系统中降低编译依赖性至关重要。在子系统类改变时,希望尽量减少重编译工作以节省时间。用Facade可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。Facade模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。
ProgramNode的每个子类在实现Traverse时,对它的ProgramNode子对象调用Traverse。每个子类依次对它的子结点做同样的动作,这样一直递归下去。例如,Expression-Node像这样定义Traverse:
例如,虚拟存储框架将Domain作为其Facade。一个Domain代表一个地址空间。它提供了虚拟地址与到内存对象、文件系统或后备存储设备(backing store)的偏移量之间的一个映射。Domain支持在一个特定地址增加内存对象、删除内存对象以及处理页面错误。正如上图所示,虚拟存储子系统内部有以下组件:·MemoryObject表示数据存储。·MemoryObjectCache将MemoryObject数据缓存在物理存储器中。MemoryObjectCache实际上是Strategy(5.9)模式,由它定位缓存策略。·AddressTranslation封装了地址翻译硬件。当发生缺页中断时,调用RepairFault操作,Domain在引起缺页中断的地址处找到内存对象并将RepairFault操作代理给与这个内存对象相关的缓存。可以改变Domain的组件对Domain进行定制。
Abstract Factory(3.1)模式可以与Facade模式一起使用以提供一个接口,这一接口可用来以一种子系统独立的方式创建子系统对象。Abstract Factory也可以代替Facade模式隐藏那些与平台相关的类。Mediator(5.5)模式与Facade模式的相似之处是,它抽象了一些已有的类的功能。然而,Mediator的目的是对同事之间的任意通信进行抽象,通常集中不属于任何单个对象的功能。Mediator的同事对象知道中介者并与它通信,而不是直接与其他同类对象通信。相对而言,Facade模式仅对子系统对象的接口进行抽象,从而使它们更容易使用;它并不定义新功能,子系统也不知道Facade的存在。通常来讲,仅需要一个Facade对象,因此Facade对象通常属于Singleton(3.5)模式。
4.6 Flyweight(享元)——对象结构型模式
物理上每个字符共享一个flyweight对象,而这个对象出现在文档结构中的不同地方。一个特定字符对象的每次出现都指向同一个实例,这个实例位于flyweight对象的共享池中。
·flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于Concrete-Flyweight对象之中,而外部对象则由Client对象存储或计算。当用户调用flyweight对象的操作时,将该状态传递给它。·用户不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享。
Flyweight模式经常和Composite(4.3)模式结合起来表示一个层次式结构,这一层次式结构是一个共享叶结点的图。共享的结果是,flyweight的叶结点不能存储指向父结点的指针。而父结点的指针将传给flyweight作为它的外部状态的一部分。这将对该层次结构中对象之间相互通信的方式产生很大的影响。
遍历过程中,GlyphContext必须了解它在Glyph结构中的当前位置。随着遍历的进行,GlyphContext::Next增加_index的值。Glyph的子类(如Row和Column)对Next操作的实现必须使得它在遍历的每一点都调用GlyphContext::Next。GlyphContext::GetFocus将索引作为BTree结构的关键字,BTree结构存储Glyph到字体的映射。树中的每个结点都标有字符串的长度,而它给这个字符串字体信息。树中的叶结点指向一种字体,而内部的字符串分成了很多子字符串,每一个对应一个子结点。
ET++[WGM88]使用flyweight来支持视觉风格独立性。[插图]视觉风格标准影响用户界面各部分的布局(如滚动条、按钮、菜单——统称为“窗口组件”)和它们的修饰成分(如阴影、斜角)。widget将所有布局和绘制行为代理给一个单独的Layout对象。改变Layout对象会改变视觉风格,即使在运行时也是这样。
每一个widget类都有一个Layout类与之相对应(如ScollbarLayout、MenubarLayout等)。使用这种方法,一个明显的问题是,使用单独的Layout对象会使用户界面对象成倍增加,因为对每个用户界面对象,都会有一个附加的Layout对象。为了避免这种开销,可用flyweight实现Layout对象。用flyweight的效果很好,因为它主要处理行为定义,而且很容易将一些较小的外部状态传递给它,它需要用这些状态来安排一个对象的位置或者对它进行绘制。对象Layout由Look对象创建和管理。Look类是一个Abstract Factory(3.1),它用GetButtonLayout和GetMenuBarLayout这样的操作检索一个特定的Layout对象。对于每一个视觉风格标准,都有一个相应的Look子类(如MotifLook、OpenLook)提供相应的Layout对象。顺便提一下,Layout对象其实是策略(参见Strategy(5.9)模式)。它们是用flyweight实现的策略对象的一个例子。
Flyweight模式通常和Composite(4.3)模式结合起来,用共享叶结点的有向无环图实现一个逻辑上的层次结构。通常,最好用flyweight实现State(5.8)和Strategy(5.9)对象。
4.7 Proxy(代理)——对象结构型模式
在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用Proxy模式。下面是一些可以使用Proxy模式的常见情况:1)远程代理 (Remote Proxy)为一个对象在不同的地址空间提供局部代表。NEXTSTEP[Add94]使用NXProxy类实现了这一目的。Coplien[Cop92]称这种代理为“大使”(ambassador)。2)虚代理 (Virtual Proxy)根据需要创建开销很大的对象。在动机一节描述的ImageProxy就是这样一种代理的例子。3)保护代理 (Protection Proxy)控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。例如,在Choices操作系统[CIRM93]中KemelProxies为操作系统对象提供了访问保护。
4)智能指引 (Smart Reference)取代了简单的指针,它在访问对象时执行一些附加操作。它的典型用途包括:·对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为Smart Pointer[Ede92])。·当第一次引用一个持久对象时,将它装入内存。·在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
Proxy模式在访问对象时引入了一定程度的间接性。根据代理的类型,附加的间接性有多种用途:1)Remote Proxy可以隐藏一个对象存在于不同地址空间的事实。2)Virtual Proxy可以进行最优化,例如根据要求创建对象。3)Protection Proxies和Smart Reference都允许在访问一个对象时有一些附加的内务处理(housekeeping task)。
Proxy模式还可以对用户隐藏另一种称为copy-on-write的优化方式,该优化与根据需要创建对象有关。拷贝一个庞大而复杂的对象是一种开销很大的操作,如果这个拷贝根本没有被修改,那么这些开销就没有必要。用代理延迟这一拷贝过程,我们可以保证只有当这个对象被修改的时候才对它进行拷贝。在实现Copy-on-write时必须对实体进行引用计数。拷贝代理仅会增加引用计数。只有当用户请求一个修改该实体的操作时,代理才会真正地拷贝它。在这种情况下,代理还必须减少实体的引用计数。当引用的数目为零时,这个实体将被删除。copy-on-write可以大幅度地降低拷贝庞大实体时的开销
考虑在动机一节提到的虚代理的例子,图像应该在一个特定的时刻被装载——在Draw操作被调用时——而不是在只要引用这个图像就装载它。重载访问操作符不能做出这种区分。在这种情况下我们只能人工实现每一个代理操作,向实体转发请求。正如示例代码中所示的那样,这些操作之间非常相似。一般来说,所有的操作在向实体转发请求之前,都要检验这个要求是否合法,原始对象是否存在等。但重复写这些代码很麻烦,因此我们一般用一个预处理程序自动生成它。
3)Proxy并不总是需要知道实体的类型 若Proxy类能够完全通过一个抽象接口处理它的实体,则无须为每一个RealSubject类都生成一个Proxy类,Proxy可以统一处理所有的RealSubject类。但是如果Proxy要实例化RealSubject(例如在虚代理中),那么它们必须知道具体的类。另一个实现方面的问题涉及在实例化实体以前怎样引用它。有些代理必须引用它们的实体,无论它是在硬盘上还是在内存中。这意味着它们必须使用某种独立于地址空间的对象标识符。在动机一节中,我们采用一个文件名来实现这种对象标识符。
NEXTSTEP[Add94]使用代理(类NXProxy的实例)作为可分布对象的本地代表,当客户请求远程对象时,服务器为这些对象创建代理。收到消息后,代理对消息和它的参数进行编码,并将编码后的消息传递给远程实体。类似地,实体对所有的返回结果编码,并将它们返回给NXProxy对象。McCullough[McC87]讨论了在Smalltalk中用代理访问远程对象的问题。Pascoe[Pas86]讨论了如何用“封装器”(encapsulator)控制方法调用的副作用以及进行访问控制。
代理的实现与装饰的实现类似,但是在相似的程度上有所差别。Protection Proxy的实现可能与装饰的实现差不多。另外,Remote Proxy不包含对实体的直接引用,而只是一个间接引用,如“主机ID,主机上的局部地址”。Virtual Proxy开始的时候使用一个间接引用,例如一个文件名,但最终将获取并使用一个直接引用。
4.8.1 Adapter与Bridge
Adapter和Bridge模式通常被用于软件生命周期的不同阶段。当你发现两个不兼容的类必须同时工作时,就有必要使用Adapter模式,其目的一般是避免代码重复。此处耦合不可预见。相反,Bridge的使用者必须事先知道:一个抽象将有多个实现部分,并且抽象和实现两者是独立演化的。Adapter模式在类已经设计好后实施,而Bridge模式在设计类之前实施。这并不意味着Adapter模式不如Bridge模式,只是因为它们针对了不同的问题。
Adapter则复用一个原有的接口。记住,适配器使两个已有的接口协同工作,而不是定义一个全新的接口。
4.8.2 Composite、Decorator与Proxy
从Decorator模式的角度看,composite是一个ConcreteComponent。而从Composite模式的角度看,decorator则是一个Leaf。
像Decorator模式一样,Proxy模式构成一个对象并为用户提供一致的接口。但与Decorator模式不同的是,Proxy模式不能动态地添加或分离性质,它也不是为递归组合而设计的。它的目的是,当直接访问一个实体不方便或不符合需求时,为这个实体提供一个替代者,例如,实体在远程设备上使访问受到限制或者实体是持久存储的。
在Proxy模式中,实体定义了关键功能,而Proxy提供(或拒绝)对它的访问。在Decorator模式中,组件仅提供了部分功能,而一个或多个decorator负责完成其他功能。Decorator模式适用于编译时不能(至少不方便)确定对象的全部功能的情况。这种开放性使递归组合成为Decorator模式中一个必不可少的部分。而在Proxy模式中则不是这样,因为Proxy模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态地表达。
模式间的这些差异非常重要,因为它们针对了面向对象设计过程中一些特定的经常发生的问题的解决方法。但这并不意味着这些模式不能结合使用。可以设想有一个proxy-decorator用来给proxy添加功能,或是一个decorator-proxy用来修饰一个远程对象。尽管这种混合可能有用(我们手边还没有现成的例子),但它们可以分割成一些有用的模式。
第5章 行为型模式
Chain of Responsibility(5.1)提供更松的耦合。它让你通过一条候选对象链隐式地向一个对象发送请求。根据运行时情况任一候选者都可以响应相应的请求。候选者的数目是任意的,你可以在运行时决定哪些候选者参与到链中。
Strategy(5.9)模式将算法封装在对象中,这样可以方便地指定和改变一个对象所使用的算法。Command(5.2)模式将请求封装在对象中,这样它就可作为参数来传递,也可以被存储在历史列表里,或者以其他方式使用。State(5.8)模式封装一个对象的状态,使得当这个对象的状态对象变化时,该对象可改变它的行为。Visitor(5.11)封装分布于多个类之间的行为,而Iterator(5.4)则抽象了访问和遍历一个集合中的对象的方式。
5.1 Chain of Responsibility(职责链)——对象行为型模式
3)表示请求 可以有不同的方法表示请求。最简单的形式,比如在HandleHelp的例子中,请求是一个硬编码的(hard-coded)操作调用。这种形式方便而且安全,但你只能转发Handler类定义的固定的一组请求。另一选择是使用一个处理函数,这个函数以一个请求码(如一个整型常数或一个字符串)为参数。这种方法支持请求数目不限。唯一的要求是发送方和接收方在请求如何编码问题上应达成一致。这种方法更为灵活,但它需要用条件语句来区分请求代码以分派请求。另外,无法用类型安全的方法来传递请求参数,因此它们必须被手工打包和解包。显然,相对于直接调用一个操作来说它不太安全。为解决参数传递问题,我们可使用独立的请求对象来封装请求参数。Request类可明确地描述请求,而新类型的请求可用它的子类来定义。这些子类可定义不同的请求参数。处理者必须知道请求的类型(即它们正使用哪一个Request子类)以访问这些参数。为标识请求,Request可定义一个访问器(accessor)函数以返回该类的标识符。或者,如果实现语言支持的话,接收者可使用运行时的类型信息。
在这种情况下,按钮会立即处理该请求。注意任何HelpHandler类都可作为Dialog的后继者。此外,它的后继者可以被动态地改变。因此不管对话框被用在何处,你都可以得到正确的与上下文相关的帮助信息。
图形编辑器框架Unidraw定义了“命令”Command对象,它封装了发给Component和ComponentView对象[VL90]的请求。一个构件或构件视图可解释一个命令以进行一个操作,这里“命令”就是请求。这对应于在实现一节中描述的“对象作为请求”的方法。构件和构件视图可以组织为层次式的结构。一个构件或构件视图可将命令解释转发给它的父构件,而父构件依次将它转发给它的父构件,如此类推,就形成了一个职责链。
ET++使用职责链来处理图形的更新。当一个图形对象必须更新它的外观的一部分时,调用InvalidateRect操作。一个图形对象自己不能处理InvalidateRect,因为它对它的上下文了解不够。例如,一个图形对象可被包装在一些类似滚动条(scroller)或放大器(zoomer)的对象中,这些对象变换它的坐标系统。也就是说,对象可被滚动或放大以至于它有一部分在视区外。因此默认的InvalidateRect的实现转发请求给包装的容器对象。转发链中的最后一个对象是一个窗口(window)实例。当窗口收到请求时,保证失效矩形被正确变换。窗口通知窗口系统接口并请求更新,从而处理InvalidateRect。
5.2 Command(命令)——对象行为型模式
像上面讨论的MenuItem对象那样,抽象出待执行的动作以参数化某对象。你可用过程语言中的回调(callback)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Command模式是回调机制的一个面向对象的替代品。·在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那里实现该请求。
·支持取消操作。Command的Excute操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现重数不限的“撤销”和“重做”。·支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command接口中添加装载操作和存储操作,可以用来保持一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Execute操作重新执行它们。·用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务(transaction)的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。
有时可能不得不将一个可撤销的命令在它可以被放入历史列表之前先拷贝下来。这是因为执行原来的请求的命令对象将在稍后执行其他的请求。如果命令的状态在各次调用之间会发生变化,那就必须进行拷贝以区分相同命令的不同调用。例如,一个删除选定对象的删除命令(DeleteCommand)在它每次被执行时,必须存储不同的对象集合。因此该删除命令对象在执行后必须被拷贝,并且将该拷贝放入历史列表中。如果该命令的状态在执行时从不改变,则不需要拷贝,而仅需将一个对该命令的引用放入历史列表中。在放入历史列表之前必须被拷贝的那些Command起着原型(参见Prototype(3.4))的作用。
避免撤销操作过程中的错误积累 在实现一个可靠的、能保持原先语义的撤销/重做机制时,可能会遇到滞后影响问题。由于命令的重复执行、取消执行和重执行的过程中可能会积累错误,以致一个应用的状态最终偏离初始值。这就有必要在Command中存入更多的信息,以保证这些对象可被精确地复原成它们的初始状态。这里可使用Memento(5.6)来让该Command访问这些信息而不暴露其他对象的内部信息。
对于不能撤销和不需要参数的简单命令,可以用一个类模板来参数化该命令的接收者。我们将为这些命令定义一个模板子类SimpleCommand。用Receiver类型参数化SimpleCommand,并维护一个接收者对象和一个动作之间的绑定,而这一动作是用指向一个成员函数的指针存储的。
MacroCommand的关键是它的Execute成员函数。它遍历所有的子命令并调用其各自的Execute操作。
InterViews定义了一个Action抽象类,它提供命令功能。它还定义了一个ActionCallback模板,这个模板以Action方法为参数,可自动生成Command子类。
THINK类库[Sym93b]也使用Command模式支持可撤销的操作。THINK中的命令被称为“任务”(task)。任务对象沿着一个Chain of Responsiblity(5.1)传递以供消费(consumption)。
Unidraw的命令对象很特别,它的行为就像是一个消息。一个Unidraw命令可被送给另一个对象去解释,而解释的结果因接收的对象而异。此外,接收者可以委托另一个对象来进行解释,典型的情况是委托给一个较大的结构中(比如在一个职责链中)接收者的父构件。这样,Unidraw命令的接收者是计算出来的而不是预先存储的。Unidraw的解释机制依赖于运行时的类型信息。
Composite(4.3)可用来实现宏命令。Memento(5.6)可用来保持某个状态,命令用这一状态来取消它的效果。在被放入历史列表前必须被拷贝的命令起到一种原型(参见Prototype(3.4))的作用。
5.3 Interpreter(解释器)——类行为型模式
最高效的解释器通常不是通过直接解释语法分析树实现的,而是首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种情况下,转换器也可用解释器模式实现,该模式仍是有用的。
抽象语法树可用一个表驱动的语法分析程序来生成,也可用手写的(通常为递归下降法)语法分析程序创建,或直接由Client提供。
一个程序设计语言会有许多在抽象语法树上的操作,比如类型检查、优化、代码生成,等等。恰当的做法是使用一个访问者以避免在每一个类上都定义这些操作。
解释器模式在使用面向对象语言实现的编译器中得到了广泛应用,如Smalltalk编译器。SPECTalk使用该模式解释输入文件格式的描述[Sza92]。QOCA约束–求解工具使用它对约束进行计算[HHMV92]。
Composite(4.3):抽象语法树是一个组合模式的实例。Flyweight(4.6):说明了如何在抽象语法树中共享终结符。Iterator(5.4):解释器可用一个迭代器遍历该结构。Visitor(5.11):可用来在一个类中维护抽象语法树中各结点的行为。
5.4 Iterator(迭代器)——对象行为型模式
注意迭代器和列表是耦合在一起的,而且客户对象必须知道遍历的是一个列表而不是其他聚合结构。最好能有一种办法使得不需要改变客户代码即可改变该聚合类。可以通过将迭代器的概念推广到多态迭代(polymorphic iteration)来达到这个目标。
一个健壮的迭代器(robust iterator)保证插入和删除操作不会干扰遍历,且不需要拷贝该聚合。有许多方法来实现健壮的迭代器,其中大多数需要向聚合注册迭代器。当插入或删除元素时,该聚合要么调整迭代器的内部状态,要么在内部维护额外的信息以保证正确的遍历。Kofler在ET++[Kof93]中对如何实现健壮的迭代器做了很充分的讨论。Murray讨论了如何为USL StandardComponents列表类实现健壮的迭代器[Mur93]。
Proxy(4.4)模式提供了一个补救方法。我们可使用一个栈分配的Proxy作为实际迭代器的中间代理。该代理在其析构器中删除迭代器。这样当该代理的生命周期结束时,实际迭代器将同它一起被释放。即使是在发生异常时,该代理机制也能保证正确地清除迭代器对象。这就是著名的C++“资源分配即初始化”技术[ES90]的一个应用。后面的代码示例给出了一个例子。
但是,这样的特权访问可能使定义新的遍历变得很难,因为它将要求改变该聚合的接口增加另一个友元。为避免这一问题,迭代器类可包含一些protected操作来访问聚合类的重要的非公共可见的成员。迭代器子类(且只有迭代器子类)可使用这些protected操作来得到对该聚合的特权访问。
8)空迭代器 空迭代器(NullIterator)是一个退化的迭代器,它有助于处理边界条件。根据定义,NullIterator总是已经完成了遍历,即它的IsDone操作总是返回true。空迭代器使得更容易遍历树形结构的聚合(如组合对象)。在遍历过程中的每一个结点,都可向当前的元素请求遍历其各个子结点的迭代器。该聚合元素将返回一个具体的迭代器。但叶结点元素返回NullIterator的一个实例。这就使我们可以用一种统一的方式实现在整个结构上的遍历。
Composite(4.3):迭代器常被应用到像组合这样的递归结构上。Factory Method(4.3):多态迭代器靠Factory Method来实例化适当的迭代器子类。Memento(5.6):常与迭代器模式一起使用。迭代器可使用memento来捕获一个迭代的状态。迭代器在其内部存储memento。
5.5 Mediator(中介者)——对象行为型模式
注意导控者是如何在对话框和入口域间进行中介的。窗口组件间的通信都通过导控者间接地进行,它们不必互相知道,仅需知道导控者。而且,由于所有这些行为都局限于一个类中,只要扩展或替换这个类,就可以改变和替换这些行为。
中介者模式有以下优点和缺点:1)减少了子类生成 Mediator将原本分布于多个对象间的行为集中在一起。改变这些行为只需生成Meditator的子类即可,这样各个Colleague类可被复用。2)将各Colleague解耦 Mediator有利于各Colleague间的松耦合,你可以独立地改变和复用各Colleague类和Mediator类。3)简化了对象协议 用Mediator和各Colleague间的一对多交互来代替多对多交互。一对多的关系更易于理解、维护和扩展。4)对对象如何协作进行了抽象 将中介作为一个独立的概念并将其封装在一个对象中,使你将注意力从对象各自本身的行为转移到它们之间的交互上来。这有助于弄清楚一个系统中的对象是如何交互的。5)使控制集中化 中介者模式将交互的复杂性变为中介者的复杂性。因为中介者封装了协议,它可能变得比任一个Colleague都复杂。这可能使得中介者自身成为一个难于维护的庞然大物。
2)Colleague-Mediator通信 当一个感兴趣的事件发生时,Colleague必须与其Mediator通信。一种实现方法是使用Observer(5.7)模式,将Mediator实现为一个Observer,各Colleague作为Subject,一旦其状态改变就发送通知给Mediator。Mediator做出的响应是将状态改变的结果传播给其他的Colleague。
Windows下的Smalltalk/V使用某种形式的代理机制:当与Mediator通信时,Colleague将自身作为一个参数传递给Mediator,使其可以识别发送者。
另一个中介者模式的应用是用于协调复杂的更新。一个例子是在Observer(5.7)中提到的ChangeManager类。ChangeManager在subject和observer间进行协调以避免冗余的更新。当一个对象改变时,它通知ChangeManager,ChangeManager随即通知依赖于该对象的那些对象以协调更新。
类似的应用出现在Unidraw绘图框架[VL90]中,它使用一个称为CSolver的类来实现“连接器”间的连接约束。图形编辑器中的对象可用不同的方式表现出相互依附。连接器用于自动维护连接的应用中,如框图编辑器和电路设计系统。CSolver是连接器间的中介者,它解释连接约束并更新连接器的位置以反映这些约束。
Facade(4.5)与中介者的不同之处在于,它是对一个对象子系统进行抽象,从而提供了一个更为方便的接口。它的协议是单向的,即Facade对象对这个子系统类提出请求,但反之则不行。相反,Mediator提供了各Colleague对象不支持或不能支持的协作行为,而且协议是多向的。Colleague可使用Observer(5.7)模式与Mediator通信。
5.6 Memento(备忘录)——对象行为型模式
一个众所周知的保持对象间连接关系的方法是使用约束解释系统。我们可将这一功能封装在一个ConstraintSolver对象中。ConstraintSolver在连接生成时,记录这些连接并产生描述它们的数学方程。当用户生成一个连接或修改图形时,ConstraintSolver就求解这些方程。并根据它的计算结果重新调整图形,使各个对象保持正确的连接。
2)存储增量式改变 如果备忘录的创建及其返回(给它们的原发器)的顺序是可预测的,备忘录可以仅存储原发器内部状态的增量改变。例如,一个包含可撤销的命令的历史列表可使用备忘录,以保证命令被取消时它们可以恢复到正确的状态(参见Command(5.2))。历史列表定义了一个特定的顺序,按照这个顺序命令可以被撤销和重做。这意味着备忘录可以只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。在前面动机一节给出的例子中,约束解释器可以仅存储那些变化了的内部结构,以保持直线与矩形相连,而不是存储这些对象的绝对位置。
我们使用MoveCommand命令对象(参见Command(5.2))来执行(取消)一个图形对象从一个位置到另一个位置的移动变换。图形编辑器调用命令对象的Execute操作来移动一个图形对象,而用Unexecute来取消该移动。命令对象存储它的目标、移动的距离和一个ConstraintSolverMemento的实例,它是一个包含约束解释器状态的备忘录。
Dylan中的Collection[App92]提供了一个反映备忘录模式的迭代接口。Dylan的集合有一个“状态”对象的概念,它是一个表示迭代状态的备忘录。每一个集合可以按照它所选择的任意方式表示迭代的当前状态,该表示对客户完全不可见。Dylan的迭代方法转换为C++可表示如下
QOCA约束解释工具在备忘录中存储增量信息[HHMV92]。客户可得到刻画某约束系统当前解释的备忘录。该备忘录仅包括从上一次解释以来发生改变的那些约束变量。通常每次新的解释仅有一小部分解释器变量发生改变。这个发生变化的变量子集已足以将解释器恢复到先前的解释,恢复更前的解释要求经过中间的解释逐步地恢复。所以不能以任意的顺序设定备忘录,QOCA依赖一种历史机制来恢复到先前的解释。
5.7 Observer(观察者)——对象行为型模式
一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它,这样就保持了系统层次的完整。如果目标和观察者混在一块,那么得到的对象要么横贯两个层次(违反了层次性),要么必须放在这两层的某一层中(这可能会损害层次抽象)。
4)对已删除目标的悬挂引用 删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一种避免悬挂引用的方法是,当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一般来说,不能简单地删除观察者,因为其他的对象可能会引用它们,或者也可能它们还在观察其他的目标。
DAGChangeManager处理目标及其观察者之间依赖关系构成的无环有向图。当一个观察者观察多个目标时,DAGChangeManager要比SimpleChangeManager好一些。在这种情况下,两个或更多个目标中产生的改变可能会产生冗余的更新。DAGChangeManager保证观察者仅接收一个更新。当然,当不存在多重更新的问题时,SimpleChangeManager更好一些。
其他使用这一模式的用户界面工具有InterViews[LVC89]、Andrew Toolkit[P+88]和Unidraw[VL90]。InterViews显式地定义了Observer和Observable(目标)类。Andrew分别称它们为“视图”和“数据对象”。Unidraw将图形编辑器对象分割成View和Subject两部分。
Mediator(5.5):通过封装复杂的更新语义,ChangeManager充当目标和观察者之间的中介者。Singleton(3.5):ChangeManager可使用Singleton模式来保证它是唯一的并且是可全局访问的。
5.8 State(状态)——对象行为型模式
在下面两种情况下均可使用State模式:·一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。·一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常,有多个操作包含这一相同的条件结构。State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某个State子类中,所以通过定义新的子类可以很容易地增加新的状态和转换。另一个方法是使用数据值定义内部状态并且让Context操作来显式地检查这些数据。但这样将会使整个Context的实现中遍布看起来很相似的条件语句或case语句。增加一个新的状态可能需要改变若干个操作,这就使得维护变得复杂了。State模式避免了这个问题,但可能会引入另一个问题,因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是有许多状态时这样的分布实际上更好一些,否则需要使用巨大的条件语句。正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大块并且使得代码不够清晰,这又使得它们难以修改和扩展。State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的if或switch语句中,而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。
当一个对象仅以内部数据值来定义当前状态时,其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。而且,State对象可保证Context不会发生内部状态不一致的情况,因为从Context的角度看,状态转换是原子的——只需重新绑定一个变量(即Context的State对象变量),而无须为多个变量赋值[dCLF93]。
在C++Programming Style[Car92]中,Cargil描述了另一种将结构加载在状态驱动的代码上的方法:使用表将输入映射到状态转换。对每一个状态,一张表将每一个可能的输入映射到一个后继状态。实际上,这种方法将条件代码(和State模式下的虚函数)映射为一个查找表。表的主要好处是其规则性:你可以通过更改数据而不是更改程序代码来改变状态转换的准则。然而它也有一些缺点:·对表的查找通常不如(虚)函数调用效率高。·用统一的、表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解。·通常难以加入伴随状态转换的一些动作。表驱动的方法描述了状态和它们之间的转换,但必须扩充这个机制以便在每一个转换上能够进行任意的计算。
表驱动的状态机和State模式的主要区别可以被总结如下:State模式对与状态相关的行为进行建模,而表驱动的方法着重于定义状态转换。
大多数流行的交互式绘图程序提供了以直接操纵的方式进行工作的“工具”。例如,一个画直线的工具可以让用户通过点击和拖动来创建一条新的直线,一个选择工具可以让用户选择某个图形对象。通常有许多这样的工具放在一个选项板供用户选择。用户认为这一活动是选择一个工具并使用它,但实际上编辑器的行为随当前的工具而变:当绘制工具被激活时,我们创建图形对象;当选择工具被激活时,我们选择图形对象;等等。我们可以使用State模式来根据当前的工具改变编辑器的行为。我们可定义一个抽象的Tool类,再从这个类派生出一些子类,实现与特定工具相关的行为。图形编辑器维护一个当前Tool对象并将请求委托给它。当用户选择一个新的工具时,就将这个工具对象换成新的,从而使得图形编辑器的行为相应地发生改变。HotDraw[Joh92]和Unidraw[VL90]中的绘图编辑器框架都使用了这一技术。它使得客户可以很容易地定义新类型的工具。在HotDraw中,DrawingController类将请求转发给当前的Tool对象。在Unidraw中,相应的类是Viewer和Tool。下图简要描述了Tool和Drawing-Controller的接
Flyweight(4.6)解释了何时以及怎样共享状态对象。状态对象通常是Singleton(3.5)。
5.9 Strategy(策略)——对象行为型模式
在以下情况下使用Strategy模式:·许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。·需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时[HO87],可以使用策略模式。·算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。·一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
在用于编译器代码优化的RTL系统[JML92]中,Strategy定义了不同的寄存器分配方案(RegisterAllocator)和指令集调度策略(RISCscheduler,CISCscheduler)。这就为在不同的目标机器结构上实现优化程序提供了所需的灵活性。
Booch构件[BV90]将Strategy用作模板参数。Booch集合类支持三种不同的存储分配策略:管理的(从一个存储池中分配),控制的(分配/去分配由锁保护),无管理的(正常的存储分配器)。在一个集合类实例化时,将这些Strategy作为模板参数传递给它。例如,一个使用无管理策略的UnboundedCollection实例化为UnboundedCollection〈MyItemType*,Unmanaged〉。
RApp是一个集成电路布局系统[GA89,AG90]。RApp必须对连接电路中各子系统的线路进行布局和布线。RApp中的布线算法定义为一个抽象Router类的子类。Router是一个Strategy类。
5.10 Template Method(模板方法)——类行为型模式
下面的C++实例说明了一个父类如何强制其子类遵循一种不变的结构。这个例子来自于NeXT的AppKit[Add94]。考虑一个支持在屏幕上绘图的类View。一个视图在进入“焦点”(focus)状态时才可设定合适的特定绘图状态(如颜色和字体),因而只有成为“焦点”之后才能进行绘图。View类强制其子类遵循这个规则。我们用Display模板方法来解决这个问题。View定义两个具体操作SetFocus和ResetFocus,分别设定和清除绘图状态。View的DoDisplay钩子操作实施真正的绘图功能。Display在DoDisplay前调用SetFocus以设定绘图状态,Display此后调用ResetFocus以释放绘图状态。
Factory Method(3.3)常被模板方法调用。在动机一节的例子中,DoCreateDocu-ment就是一个Factory Method,它由模板方法OpenDocument调用。Strategy(5.9):模板方法使用继承来改变算法的一部分,Strategy使用委托来改变整个算法。
5.11 Visitor(访问者)——对象行为型模式
考虑一个编译器,它将源程序表示为一个抽象语法树。该编译器需要在抽象语法树上实施某些操作以进行“静态语义”分析,例如检查是否所有的变量都已经被定义了。它也需要生成代码。因此它可能要定义许多操作以进行类型检查、代码优化、流程分析,检查变量是否在使用前被赋初值,等等。此外,还可使用抽象语法树进行优美格式打印、程序重构以及对程序进行多种度量等。这些操作大多要求对不同的结点进行不同的处理。例如对代表赋值语句的结点的处理就不同于对代表变量或算术表达式的结点的处理。因此有用于赋值语句的类,有用于变量访问的类,还有用于算术表达式的类,等等。结点类的集合当然依赖于被编译的语言,但对于一个给定的语言其变化不大。
所以在应用访问者模式时考虑的关键问题是系统的哪个部分会经常变化,是作用于对象结构上的算法还是构成该结构的各个对象的类。如果总是有新的ConcretElement类加入进来的话,Vistor类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作可能更容易一些。如果Element类层次是稳定的,而你不断地增加操作或修改算法,访问者模式可以帮助你管理这些改动。
IRISInventor[Str93]是一个用于开发三维图形应用的工具包。Inventor将一个三维场景表示成一个结点的层次结构,每一个结点代表一个几何对象或其属性。诸如绘制一个场景或是映射一个输入事件之类的一些操作要求以不同的方式遍历这个层次结构。Inventor使用称为“action”的访问者来做到这一点。生成图像、事件处理、查询、填充和决定边界框等操作都有相应的访问者来处理。为使增加新的结点更容易一些,Inventor为C++实现了一个双分派方案。该方案依赖于运行时的类型信息和一个二维表,在这个二维表中行代表访问者而列代表结点类。表格中存储绑定于访问者和结点类的函数指针。
5.12 行为型模式的讨论
- 一个Strategy的代码可能会被嵌入其Context类中,而一个State的代码可能会在该状态的Context类中直接实现。但不是所有的对象行为模式都像这样分割功能。例如,Chain of Responsibility(5.1)可以处理任意数目的对象(即一个链),而所有这些对象可能已经存在于系统中了。职责链说明了行为模式间的另一个不同点:并非所有的行为模式都定义类之间的静态通信关系。职责链提供在数目可变的对象间进行通信的机制。其他模式涉及一些作为参数传递的对象。
5.12.2 对象作为参数
- 在Command模式中多态很重要,因为执行Command对象是一个多态的操作。相反,Memento接口非常小,以至于备忘录只能作为一个值传递,因此它很可能根本不给它的客户提供任何多态操作。
6.1.1 一套通用的设计词汇
- 计算机科学家对算法和数据结构进行命名和分类,但我们却很少为其他类型的模式命名。设计模式为设计者们交流讨论、书写文档以及探索各种不同设计提供了一套通用的设计词汇。设计模式使你可以在比设计表示或编程语言更高的抽象级别谈论一个系统,从而降低了其复杂度。设计模式提高了你的设计及你与同事讨论这些设计的层次。
6.1.3 现有方法的一种补充
这些设计模式展示了如何使用诸如对象、继承和多态等基本技术,也展示了如何以算法、行为、状态或者需生成的对象类型来将一个系统参数化。
一个成熟的设计方法不仅要有设计模式,还可有其他类型的模式,如分析模式、用户界面设计模式或者性能调节模式等。但是设计模式是最主要的部分,这在以前却被忽略了。
6.1.4 重构的目标
- 该软件若要继续演化就必须重新组织,这个过程称为重构(refactoring)。框架常常在这个阶段出现。重构工作包括将类拆分为专用和通用的构件,把各个操作在类层次上提或下放到合适的类中,并使各个类的接口合理化。这个巩固阶段将会产生许多新类型的对象,它们通常是通过分解而不是继承原有的对象而得到的。因而黑箱复用代替了白箱复用。满足更多需求和达到更高可复用性的要求推动面向对象软件不断重复扩展和巩固这两个阶段——扩展以满足新的需求,而巩固使软件更为通用(参见下图)。
附录A 词汇表
- dynamic binding(动态绑定)在运行时才将一个请求与一个对象及其一个操作关联起来。在C++中,只有虚函数可动态绑定。
来自微信读书
--End--