23 Design Patterns implemented by C++.
从本文开始,一系列的文章将揭开设计模式的神秘面纱。本篇博文是参考了《设计模式-可复用面向对象软件的基础》这本书,由于该书的引言
写的太好了,所以本文基本是对原书的摘抄。
0.前言
评估一个面向对象系统的质量时,方法之一就是要判断系统的设计者是否强调了对象之间的公共协同关系。在系统开发阶段强调这种机制的优势在于,它能使所生成的系统体系结构更加精巧、简洁和易于理解,其程度远远超过了未使用模式的体系结构。
设计模式描述了在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。设计模式捕获了随时间进化与发展的问题的求解方法,因此它们并不是人们从一开始就采用的设计方案。它们反映了不为人知的重新设计和重新编码的成果,而这些都来自软件开发者为了设计出灵活、可复用的软件而长时间进行的艰苦努力。设计模式捕获了这些解决方案,并用简洁易用的方式表达出来。
根据模式的性质可以将设计模式划分为三种类型:创建型(creational)、结构型(structural)和行为型(behavioral)。
1. 引言
设计面向对象软件比较困难,而设计可复用的面向对象软件就更加困难。你必须找到相关的对象,以适当的粒度将它们归类,再定义类的接口和继承层次,建立对象之间的基本关系。设计应该对手头的问题有针对性,同时对将来的问题和需求也要有足够的通用性。避免重复设计或尽可能少做重复设计。
设计模式使人们可以更加简单方便地复用成功的设计和体系结构。
1.1 什么是设计模式?
Christopher Alexander说过:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。
一般而言,一个模式有四个基本要素:
- 模式名(pattern name) :一个助记名,它用一两个词来描述模式的问题、解决方案和效果。
- 问题(problem) :描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果。
- 解决方案(solution) :描述了设计的组成成分、它们之间的相互关系及各自的职责和协作方式。
- 效果(consequence) :描述了模式应用的效果及使用模式应权衡的问题。
设计模式是对用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。
1.2 Smalltalk MVC 中的设计模式
在Smalltalk-80中,类的模型/视图/控制器(Model/View/Controller)三元组(MVC)被用来构建用户界面。MVC包括三类对象。模型(Model)是应用对象,视图(View) 是它在屏幕上的表示,控制器(Controller)定义用户界面对用户输入的响应方式。不使用MVC,用户界面设计往往将这些对象混在一起, 而MVC则将它们分离以提高灵活性和复用性。
视图必须保证它的显示正确地反映了模型的状态。一旦模型的数据发生变化, 模型将通知有关的视图,每个视图相应地得到刷新自己的机会。
下图显示了一个模型和三个视图(为了简单起见我们省略了控制器)。模型包含一些数据值,视图通过电子表格、柱状图、饼图等不同的方式来显示这些数据。当模型的数据发生变化时,模型就通知它的视图,而视图将与模型通信以获取这些数据值。
将对象分离,使得一个对象的改变能够影响另一些对象,而这个对象并不需要知道那些被影响的对象的细节。这个更一般的设计被描述成Observer模式。
MVC的另一个特征是视图可以嵌套。将一些对象划为一组,并将该组对象当作一个对象来使用。这个设计被描述为Composite模式,该模式允许你创建一个类层次结构,一些子类定义了原子对象(如Button)而其他类定义了组合对象(CompositeView),这些组合对象是由原子对象组合而成的更复杂的对象。
View使用Controller子类的实例来实现一个特定的响应策略。要实现不同的响应策略只要用不同种类的Controller实例替换即可。甚至可以在运行时通过改变View的Controller来改变View对用户输入的响应方式。例如,一个View可以被禁止接收任何输入,只需给它一个忽略输入事件的Controller。
View-Controller关系是Strategy模式的一个例子。一个策略是一个表述算法的对象。
1.3 描述设计模式
用统一的格式描述设计模式,每一个模式根据以下模板被分成若干部分。模板具有统一的信息描述结构,有助于更容易地学习、比较和实用设计模式。
- 模式名和分类:模式名简洁地描述了模式的本质。
- 意图:意图是回答下列问题的简单陈述:设计模式是做什么的?它的基本原理和意图是什么?它解决的是什么样的特定设计问题?
- 别名:模式的其他名称。
- 动机:用以说明一个设计问题以及如何用模式中的类、对象来解决该问题的特定情景。该情景会帮助你理解随后对模式更抽象的描述。
- 适用性:什么情况下可以使用该设计模式?该模式可用来改进哪些不良设计?你怎样识别这些情况?
- 结构:采用基于对象建模技术(OMT)[RBP+91]的表示法对模式中的类进行图形描述。我们也使用了交互图[JCJO92,BOO94]来说明对象之间的请求序列和协作关系。
- 参与者:指设计模式中的类和/或对象以及它们各自的职责。
- 协作:模式的参与者怎样协作以实现它们的职责。
- 效果:模式怎样支持它的目标?使用模式的效果和所需做的权衡是什么?系统结构的哪些方面可以独立改变?
- 实现:实现模式时需要知道的一些提示、技术要点及应避免的缺陷,以及是否存在某些特定于实现语言的问题。
- 代码示例:用来说明怎样用某种编程语言实现该模式的代码片段。
- 已知应用:实际系统中发现的模式的例子。每个模式至少包括两个不同领域的实例。
- 相关模式:与这个模式紧密相关的模式有哪些?其间重要的不同之处是什么?这个模式应与哪些其他模式一起使用?
1.4 设计模式的编目
23个设计模式的名字和意图列举如下:
- Abstract Factory:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
- Adapter:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
- Bridge:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- Builder:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
- Chain of Responsibility:解除请求的发送者和接收者之间的耦合,使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。
- Command:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。
- Composite:将对象组合成树形结构以表示“部分–整体”的层次结构。Composite使得客户对单个对象和组合对象的使用具有一致性。
- Decorator:动态地给一个对象添加一些额外的职责。就扩展功能而言,Decorator模式比生成子类方式更为灵活。
- Facade:为子系统中的一组接口提供一个一致的界面, Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- Factory Method:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。
- Flyweight:运用共享技术有效地支持大量细粒度的对象。
- Interpreter:给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
- Iterator:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
- Mediator:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- Memento:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
- Observer:定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
- Prototype:用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
- Proxy:为其他对象提供一个代理以控制对这个对象的访问。
- Singleton:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- State:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
- Strategy:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。
- Template Method:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤。
- Visitor:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
1.5 组织编目
根据两条准则对模式进行分类。
- 第一条是目的准则,即模式是用来完成什么工作的。模式依据其目的可分为创建型(creational)、**结构型(structural)和行为型(behavioral)**三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。
- 第二条是范围准则,指定模式主要是用于类还是用于对象。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时便确定下来了。对象模式处理对象间的关系,这些关系在运行时是可以变化的,更具动态性。创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组装方式。行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述了一组对象怎样协作完成单个对象所无法完成的任务。
根据以上两条准则,将 23 中设计模式进行如下:
还有一种方式是根据模式的“相关模式”部分所描述的它们怎样互相引用来组织设计模式。下图给出了模式关系的图形说明。
1.6 设计模式怎样解决设计问题?
设计模式采用多种方法解决面向对象设计者经常碰到的问题。如下给出几个问题以及使用设计模式解决它们的方法。
1.6.1 寻找合适的对象
面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过程通常称为方法或操作。对象在收到客户的请求(或消息) 后,执行相应的操作。客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。
面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的。
设计模式帮你确定并不明显的抽象和描述这些抽象的对象。例如,描述过程或算法的对象现实中并不存在,但它们却是设计的关键部分。
1.6.2 决定对象的粒度
对象在大小和数目上变化极大。它们能表示下至硬件或上至整个应用的任何事物。那么我们怎样决定一个对象应该是什么呢? 设计模式很好地讲述了这个问题。
- Facade 模式描述了怎样用对象表示完整的子系统;
- Flyweight 模式描述了如何支持大量的最小粒度的对象;
- Abstract Factory 和Builder 产生那些专门负责生成其他对象的对象;
- Visitor 和Command 生成的对象专门负责实现对其他对象或对象组的请求。
1.6.3 指定对象接口
对象声明的每一个操作指定操作名、作为参数的对象和返回值, 这就是所谓的操作的型构(signature)。对象操作所定义的所有操作型构的集合被称为该对象的接口(interface)。对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象。
类型(type)是一个用来标识特定接口的名字。一个对象可以有许多类型,并且不同的对象可以共享同一个类型。对象接口的某部分可以用某个类型来刻画,而其他部分则可用其他类型刻画。两个类型相同的对象只需要共享它们的部分接口。接口可以包含其他接口作为子集。当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型(subtype),而称另一个类型为它的超类型(supertype)。我们常说子类型继承了它的超类型的接口。
在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流,如果不通过对象的接口就无法知道对象的任何事情,也无法请求对象做任何事情。
当给对象发送请求时,所引起的具体操作既与请求本身有关又与接受对象有关。支持相同请求的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时的连接就称为动态绑定(dynamic binding)。
动态绑定是指发送的请求直到运行时才受你的具体实现的约束。因而,在知道任何有正确接口的对象都将接受此请求时,你可以写一个一般的程序,它期待着那些具有该特定接口的对象。进一步讲,动态绑定允许你在运行时彼此替换有相同接口的对象。这种可替换性就称为多态(polymorphism),它是面向对象系统中的核心概念之一。多态允许客户对象仅要求其他对象支持特定接口,除此之外对其假设几近于无。多态简化了客户的定义,使得对象间彼此独立,并可以在运行时动态改变它们相互的关系。
设计模式通过确定接口的主要组成成分及经接口发送的数据类型来帮助你定义接口。设计模式也许还会告诉你接口中不应包括哪些东西。Memento模式是一个很好的例子,它描述了怎样封装和保存对象内部的状态,以便一段时间后对象能恢复到这一状态。它规定了Memento对象必须定义两个接口:一个允许客户保持和复制memento的限制接口,一个只有原对象才能使用的用来储存和提取memento中状态的特权接口。
设计模式也指定了接口之间的关系。特别是,它们经常要求一些类具有相似的接口,或它们对一些类的接口做了限制。例如, Decorator和Proxy模式分别要求Decorator和Proxy对象的接口与被修饰的对象和受委托的对象一致。而Visitor模式中, Visitor接口必须反映出visitor能访问的对象的所有类。
1.6.4 描述对象的实现
对象的实现是由它的类决定的,类指定了对象的内部数据和表示,也定义了对象所能完成的操作,如下图所示:
基于OMT的表示法,将类描述成一个矩形,其中的类名以黑体表示。操作在类名下面,以常规字体表示。类所定义的任何数据都在操作的下面。类名与操作之间以及操作与数据之间用横线分隔。
返回类型和实例变量类型是可选的,因为我们并未假设一定要用具有静态类型的实现语言。
对象通过实例化类来创建,此对象被称为该类的实例。
-
类继承与接口继承的比较
对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。但是对象的类型只与它的接口有关,接口即对象能响应的请求的集合。一个对象可以有多个类型,不同类的对象可以有相同的类型。
对象的类和类型是有紧密关系的。因为类定义了对象所能执行的操作,也定义了对象的类型。当我们说一个对象是一个类的实例时,即指该对象支持类所定义的接口。
类继承根据一个对象的实现定义了另一个对象的实现。简而言之,它是代码和表示的共享机制。然而,接口继承(或子类型化)描述了一个对象什么时候能被用来替代另一个对象。
C++中接口继承的标准方法是公有继承一个含(纯)虚成员函数的类。C++中纯接口继承接近于公有继承纯抽象类,纯实现继承或纯类继承接近于私有继承。
很多设计模式依赖于这种差别。例如,Chain of Responsibility 模式中的对象必须有一个公共的类型,但一般情况下它们不具有公共的实现。在Composite 模式中,构件定义了一个公共的接口,但Composite通常定义一个公共的实现。
Command 、Observer 、State 和Strategy 通常纯粹作为接口的抽象类来实现。
-
对接口编程,而不是对实现编程
只根据抽象类中定义的接口来操纵对象有以下两个好处:
1)客户无须知道他们使用对象的特定类型,只需要知道对象有客户所期望的接口。
2)客户无须知道他们使用的对象是用什么类来实现的,只需要知道定义接口的抽象类。
这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如下原则:
针对接口编程,而不是针对实现编程
。不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口
1.6.5 运用复用机制
-
继承和组合的比较
面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)。类继承允许根据其它类的实现来定义一个类的实现,这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言的:在继承方式中,父类的内部细节对子类可见。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有定义良好的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。
继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性”。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。
因为对象只能通过接口访问,所以我们并不破坏封装性; 只要类型一致,运行时还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。
对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另外,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。
这导出了我们的面向对象设计的第二个原则:
优先使用对象组合,而不是类继承
。 -
委托
委托(delegation)是一种组合方法,它是组合具有与继承同样的复用能力。在委托方式下,有连个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者(delegate)。
委托的主要优点在于它便于运行时组合对象操作以及改变这些操作的组合方式。假定矩形对象和圆对象有相同的类型,我们只需要简单地用圆对象替换矩形对象,得到的窗口就是圆形的。不足之处:动态的、高度参数化的软件比静态软件更难于理解。还有运行低效问题,不过从长远来看人的低效才是更主要的。
有一些模式使用了委托,如State、Strategy和Visitor。在State模式中,一个对象将请求委托给一个描述当前状态的State对象来处理。在Strategy模式中,一个对象将一个特定的请求委托给一个描述请求执行策略的对象,一个对象只会有一个状态, 但它对不同的请求可以有许多策略。这两个模式的目的都是通过改变受托对象来改变委托对象的行为。在Visitor中,对象结构的每个元素上的操作总是被委托到Visitor对象。
其他模式则没有这么多地用到委托。Mediator引进了一个作为其他对象间通信的中介的对象。有时,Mediator对象只是简单地将请求转发给其他对象;有时,它沿着指向自己的引用来传递请求,使用真正意义的委托。Chain of Responsibility通过将请求沿着对象链传递来处理请求,有时,这个请求本身带有一个接受请求对象的引用,这时该模式就使用了委托。Bridge将实现和抽象分离开, 如果抽象和一个特定实现非常匹配,那么这个实现可以代理抽象的操作。
-
继承和参数化类型的比较
另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是类属(generic)(Ada、Eiffel)或模板(template)(C++)。它允许你在定义一个类型时不用指定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。例如,一个列表类能够以它所包含元素的类型来进行参数化。
对象组合技术允许你在运行时改变被组合的行为,但是它存在间接性,比较低效。继承允许你提供操作的默认实现,并通过子类重定义这些操作。参数化类型允许你改变类所用到的类型。但是继承和参数化类型都不能在运行时改变。哪一种方法最佳,取决于你设计和实现的约束条件。
1.6.6 关联运行时和编译时的结构
一个面向对象程序运行时的结构通常与它的代码结构相差较大。代码结构在编译时就被确定下来了,它由继承关系固定的类组成。而程序的运行时结构是由快速变化的通信对象网络组成的。
考虑对象**聚合(aggregation)和相识(acquaintance)的差别以及它们在编译时和运行时的表示是多么不同。聚合意味着一个对象拥有另一个对象或对另一个对象负责。一般我们称一个对象包含另一个对象或者是另一个对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期。相识意味着一个对象仅仅知道另一个对象。有时相识也被称为“关联”或“引用”**关系。相识的对象可能请求彼此的操作,但是它们不为对方负责。相识是一种比聚合要弱的关系,它只标识了对象间较松散的耦合关系。
在下图中,普通的箭头线表示相识,尾部带有菱形的箭头线表示聚合:
聚合和相识很容易混淆,因为它们通常以相同的方法实现。C++中,聚合可以通过定义表示真正实例的成员变量来实现,但更通常的是将这些成员变量定义为实例指针或引用;相识也是以指针或引用来实现的。
从根本上讲,是聚合还是相识是由你的意图而不是显式的语言机制决定的。尽管它们之间的区别在编译时的结构中很难看出来,但这些区别还是很大的。聚合关系使用较少且比相识关系更持久;而相识关系则出现频率较高,但有时只存在于一个操作期间,相识也更具动态性,使得它在源代码中更难被辨别出来。
1.6.7 设计应支持变化
获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,要求你的系统设计能够相应地改进。
为了设计适应这种变化且具有健壮性的系统,你必须考虑系统在它的生命周期内会发生怎样的变化。一个不考虑系统变化的设计在将来就有可能需要重新设计。这些变化可能是类的重新定义和实现,修改客户和重新测试。重新设计会影响软件系统的许多方面,并且未曾料到的变化总是代价巨大的。
设计模式可以确保系统以特定方式变化,从而帮助你避免重新设计系统。每一个设计模式允许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某种特殊变化将更健壮。
下面阐述了一些导致重新设计的一般原因,以及解决这些问题的设计模式:
- 通过显式地指定一个类来创建对象
- 在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。
- 设计模式:Abstract Factory,Factory Method, Prototype。
- 对特殊操作的依赖
- 当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。为避免把请求代码写死,你将可以在编译时或运行时很方便地改变响应请求的方法。
- 设计模式:Chain of Resposibility,Command。
- 对硬件和软件平台的依赖
- 外部的操作系统接口和应用编程接口(API)在不同的软硬件平台上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至很难跟上本地平台的更新。
- 设计模式:Abstract Factory,Bridge。
- 对对象表示或实现的依赖
- 知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化。
- 设计模式:Abstract Factory,Bridge, Memento,Proxy。
- 算法依赖
- 算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。
- 设计模式:Builder,Iterator,Strategy, Template Method),Visitor。
- 紧耦合
- 紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,你必须理解和改变其他许多类。这样的系统是一个很难学习、移植和维护的密集体。松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。
- 设计模式:Abstract Factory,Command, Facade,Mediator,Observer,Chain of Responsibility。
- 通过生成子类来扩充功能
- 通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象, 而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,可以定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。
- 设计模式:Bridge,Chain of Responsibility, Composite,Decorator,Observer, Strategy。
- 不能方便地对类进行修改
- 有时你不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。
- 设计模式:Adapter,Decorator,Visitor。
让我们看一看设计模式在开发如下三类主要软件中所起到的作用:应用程序、工具箱和框架。
-
应用程序
如果你将要建造像文档编辑器或电子制表软件这样的应用程序(application program),那么它的内部复用性、可维护性和可扩充性是要优先考虑的。内部复用性确保你不会做多余的设计和实现。设计模式通过减少依赖性来提高内部复用性。松散耦合也增强了一类对象与其他多个对象协作的可能性。例如,通过孤立和封装每一个操作, 以消除对特定操作的依赖,可使在不同上下文中复用一个操作变得更简单。消除对算法和表示的依赖可达到同样的效果。
当设计模式被用来对系统分层和限制对平台的依赖性时,它们还会使一个应用更具可维护性。通过显示怎样扩展类层次结构和怎样使用对象复用,它们可增强系统的可扩充性。同时,耦合程度的降低也会增强可扩充性。如果一个类不过多地依赖其他类,扩充这个孤立的类还是很容易的。
-
工具箱
一个应用经常会使用来自一个或多个被称为**工具箱(toolkit)**的预定义类库中的类。工具箱是一组相关的、可复用的类的集合,这些类提供了通用的功能。工具箱的一个典型例子就是列表、关联表单、堆栈等类的集合,C++的I/O流库是另一个例子。工具箱并不强制应用采用某个特定的设计,它们只是为你的应用提供功能上的帮助。工具箱强调的是代码复用,它们是面向对象环境下的“子程序库”。
工具箱的设计比应用设计要难得多,因为它要求对许多应用是可用的和有效的。再者,工具箱的设计者并不知道什么应用使用该工具箱及它们有什么特殊需求。这样,避免假设和依赖就变得很重要,否则会限制工具箱的灵活性,进而影响它的适用性和效率。
-
框架
框架(framework)是一种高级的程序设计结构,它为特定应用领域提供了一组协作的类和预设的体系结构。这种结构不仅定义了应用的总体架构、类的组织方式、对象间的责任划分和相互协作的方式,还预设了控制流程,从而使开发者能够专注于应用的具体业务逻辑。框架的核心特点在于它的反向控制(inversion of control)机制,即框架控制程序的流程,而不是传统的程序代码控制框架。这种方法显著降低了代码之间的耦合度,并增强了代码的重用性。
框架的设计通常围绕特定领域的常见问题和模式,如图形界面开发、网络通信或数据分析等。它们提供了一套通用的解决方案和设计决策,从而减轻了开发者在处理这些通用问题时的负担。此外,许多框架包含了一些现成的实现,如具体的类或子类,这些可以直接在应用中使用或作为扩展的起点。
尽管框架为软件开发带来了便利,但它们的设计和维护是一个复杂且富有挑战性的过程。框架设计者必须具备深入的领域知识和前瞻性思维,以确保框架的灵活性、可扩展性和适应性。框架的改变或演化可能会对基于它的应用产生深远影响,因此设计时必须考虑到未来可能的需求和变化。
框架的成功不仅依赖于其技术实现,还依赖于良好的文档和社区支持。明确的文档和示例可以极大地帮助开发者理解和使用框架,而一个活跃的社区则有助于框架的持续改进和扩展。设计模式的应用在框架设计中也扮演着关键角色,它们提供了一套经过验证的解决方案,能够提高框架的设计质量和代码复用率。因此,一个成功的框架通常是那些充分利用了设计模式,同时具备强大功能、灵活性和良好文档的框架。
由于框架和模式有些类似,人们常常对它们的区别感到疑惑,它们最主要的不同在于如下三个方面:
- 设计模式比框架更抽象:
- 设计模式和框架在软件开发中都扮演重要角色,但它们的抽象层次不同:设计模式比框架更抽象。设计模式是解决特定问题的一种方法论,通常在软件设计阶段被考虑,而框架则是具体的代码实现,提供了一套可以直接使用的代码结构。
- 框架可以用代码表示,并且可以直接执行和复用。框架的主要优势在于它们是以程序设计语言编写的,这意味着它们不仅可以被学习,还可以被直接使用和复用。框架提供了一个固定的结构和方法,开发者可以在这个结构上构建自己的应用程序。
- 设计模式则不同,它们通常不能直接用代码表示,而是需要在每次使用时实现。设计模式的实例才能转化为具体的代码。设计模式的作用在于提供一个解决问题的模板,它指导开发者如何在不同的情境中有效地解决问题。
- 设计模式还解释了它的意图、权衡和设计效果。这意味着设计模式不仅仅是代码的模板,它还包含了为什么选择这种模式、在使用该模式时需要考虑的权衡因素,以及使用该模式可能带来的设计上的好处。
- 设计模式是比框架更小的体系结构元素:
- 一个典型的框架包括了多个设计模式,而反之决非如此。
- 框架比设计模式更加特例化:
- 框架总是针对一个特定的应用领域。设计模式几乎能被用于任何应用。
1.7 怎样选择设计模式
给出如下几个方法,帮助你发现适合你手头问题的设计模式:
- 考虑设计模式是怎样解决设计问题的:上一节讨论了设计模式怎样帮助你找到合适的对象、决定对象的粒度、制定对象接口以及设计模式解决设计问题的几个其他方法。
- 浏览模式的意图部分:前面也列出了所有设计模式的意图(intent)部分。通读每个模式的意图,找出和你的问题相关的一个或多个模式。
- 研究模式怎样互相关联。
- 研究目的相似的模式:洞察创建型模式、结构型模式、行为型模式三种分类中模式之间的共同点和不同点。
- 检查重新设计的原因:看看你的问题是否与之前讨论的因其重新设计的各种原因有关,然后找出那些模式可以帮你避免这些会导致重新设计的因素。
- 考虑你的设计中哪些是可变的:这个方法与关注因其重新设计的原因刚好相反。最主要的一点是封装变化的概念,这是许多设计模式的主题。下表列出了设计模式允许你独立变化的方面,你可以变化他们而又不会导致重新设计。
1.8 怎样使用设计模式
一旦选择了设计模式,该怎样使用它呢?下面给出一个有效应用设计模式的循序渐进的方法:
- 大概浏览一遍设计模式:特别注意其使用性部分和效果部分,确定它适合你的问题。
- 研究结构部分、参与者部分和协作部分:确保你理解这个设计模式的类和对象以及它们是怎样关联的。
- 看代码示例部分,看看这个模式代码形式的具体例子:研究代码将有助于你实现模式。
- 选择模式参与者的名字,使它们在应用上下文中有意义:设计模式参与者的名字通常过于抽象而不会直接出现在应用中。然而,将参与者的名字和应用中出现的名字合并起来是很有用的。这会帮助你在实现中更显式地体现出模式来。例如, SimpleLayoutStrategy 或 TeXLayoutStrategy 这样的命名。
- 定义类:声明它们的接口,建立它们的继承关系,定义代表数据和对象引用的实例变量。
- 定义模式中专用于应用的操作名称:这里再一次体现出名字一般依赖于应用。使用与每一个操作相关联的责任和协作作为指导。例如,可以使用“Create-”前缀同意标记 Factory 方法。
- 实现执行模式中责任和协作的操作:实现部分提供线索指导你进行实现。