1. 什么是设计模式
一般而言,一个模式有四个基本要素:模式名、问题、解决方案、效果。
(1)模式名(pattern name):一个助记名,通过一两个词语来藐视模式的问题、解决方案和效果。模式名可以帮助我们思考,便于我们与其他人交流设计思想及设计结果。
(2)问题(problem):描述了应该在何时使用设计模式。它解释了设计问题和问题存在的前因后果。它可能描述了特定的设计问题,也可能描述了导致不灵活谁的类或对象结构。
(3)解决方案(solution):描述了设计的组成成分、它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题。
(4)效果(consequence):描述了模式应用的效果及使用模式应权衡的问题。尽管我们描述设计决策时并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价及好处具有重要意义。软件效果大多时候关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象设计的要素之一,所以模式效果包括它对系统的灵活性、扩展性或可移植性的影响。
设计模式是对用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。设计模式确定了所包含的类和实例,它们的角色、协作方式以及职责分配。每一个设计模式都集中于一个特定的面向对象设计问题或设计要点,描述了什么时候使用它,在另一些设计约束条件下是否能使用,以及使用的效果和如何取舍。
2. MVC中的设计模式
类的模型/视图/控制器(Model/View/Controller)三元组(MVC)被用来构建用户界面,透过MVC来看设计模式将帮助我们理解“模式”的含义。模型(Model)是应用对象,视图(View)是它在屏幕上的表示,控制器(Controller)定义用户界面对用户输入的响应方式。如果不使用MVC,用户界面的设计往往将这些对象混在一起,而MVC则将它们分离以提高灵活性和复用性。
MVC通过建立一个“订购/通知”协议来分离视图和模型。视图必须保证它的显示正确地反映了模型的状态。一旦模型的数据发生变化,模型将通知有关的视图,每个视图相应地得到刷新自己的机会。这种方法可以让你为一个模型提供不同的多个视图表现形式,也能够为一个模型创建新的视图而无须重写模型。
MVC的另一个特征是视图可以嵌套。MVC用View类的子类——CompositeView来支持嵌套视图,CompositeView类的对象行为类似于View类的对象行为,一个组合视图可用于任何视图可用的地方,但是它包含并管理嵌套视图。MVC还允许你在不改变视图外观的情况下改变视图对用户输入的响应方式。例如,你可能希望改变视图对键盘的响应方式,或希望使用弹出菜单而不是原来的命令键方式。MVC将响应机制封装在Controller对象中,存在着一个Controller的类层次结构,使得可以方便地对原有Controller做适当改变而创建新的Controller。
3. 如何描述设计模式
使用统一的格式描述设计模式,每一个模式根据一下模板被分成若干部分,如下:
• 模式名和分类
模式名简洁地描述了模式的本质,模式的分类反映了在第5章节介绍的方案。
• 意图
意图是回答下列问题的简单陈述:设计模式是做什么的?它的基本原理和意图是什么?它解决的是什么样的特定设计问题?
• 别名
模式的其他名称。
• 动机
用以说明一个设计问题以及如何用模式中的类、对象来解决问题的特定情景。该情景会帮助我们理解随后对模式更抽象的描述。
• 适用性
什么情况下可以使用该设计模式?该模式可以用来改进哪些不良设计?你怎样识别这些情况?
• 结构
采用基于对象建模技术(OMT)的表示法对模式中的类进行图形描述,也可使用交互图来说明对象之间的请求序列和协作关系。
• 参与者
指设计模式中的类和/或对象以及它们各自的职责。
• 协作
模式的参与者怎样协作以实现它们的职责。
• 效果
模式怎样支持它的目标?使用模式的效果和所需做的权衡是什么?系统结构的哪些方面可以独立改变?
• 实现
实现模式需要知道的一些提示、技术要点及应避免的缺陷,以及是否存在某些特定于实现语言的问题。
• 代码示例
用来说明怎样用C++或Smalltalk实现该模式的代码片段。
• 已知应用
实际系统中发现的模式的例子。每个模式至少包括两个不同领域的实例。
• 相关模式
与这个模式紧密相关的模式有哪些?其间重要的不同之处是什么?这个模式应与哪些其他模式一起使用?
4. 设计模式的目录
设计模式目录中一共包含23个设计模式,在后续的博客中我会一一进行详细介绍,它们的名称和意图列举如下:
名称 | 意图 |
---|---|
Abstract Factory | 抽象工厂模式,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。 |
Adapter | 适配器模式,将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 |
Bridge | 桥接模式,将抽象模式与它的实现部分分离,使它们都可以独立地变化。 |
Builder | 建造者模式,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 |
Chain of Responsibility | 职责链模式,解除请求的发送者和接收者之间的耦合,使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 |
Command | 命令模式,将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。 |
Composite | 组合模式 ,将对象组合成树形结构以表示“部分-整体”的层析结构,使得客户对单个对象和组合对象的使用具有一致性。 |
Decorator | 装饰模式,动态地给一个对象添加一些额外的职责,就扩展功能而言,Decorator模式比生成子类方式更为灵活。 |
Facade | 外观模式,为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 |
Factory Method | 工厂方法模式,定义一个用于创建对象的接口,让子类决定将哪一个类实例化,使一个类的实例化延迟到其子类。 |
Flyweight | 享元模式,运用共享技术有效地支持大量细粒度的对象。 |
Interpreter | 解释器模式,给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来皆是语言中的句子。 |
Iterator | 迭代器模式,提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。 |
Mediator | 中介者模式,用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 |
Memento | 备忘录模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到保存的状态。 |
Observer | 观察者模式,定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。 |
Prototype | 原型模式,用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。 |
Proxy | 代理模式,为其他对象提供一个代理以控制对这个对象的访问。 |
Singleton | 单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。 |
State | 状态模式,允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它所属的类。 |
Strategy | 策略模式,定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。该模式使得算法的变化可独立于使用它的客户。 |
Template Method | 模板方法模式,定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤。 |
Visitor | 访问者模式,表示一个作用于某对象结构中的各元素的曹组,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 |
5. 组织编目
本章节主要对设计模式进行分类,以便我们对各族相关的模式进行引用。同时分类有助于更快地学习目录中的模式,且对发现新的模式也有指导作用,具体如下表所示:
范围\目的 | 创建型 | 结构型 | 行为型 |
---|---|---|---|
类 | Factory Method | Adapter(类) | Interpreter Template Method |
对象 | Abstract Factory Builder Prototype Singleton |
Adapter(对象) Bridge Composite Decorator Facade Flyweight Proxy |
Chain of Responsibility Command Iterator Mediator Memento Observer State Strategy Visitor |
在上表的分类中,主要依据了两条准则:第一条是目的准则
,即模式是用来完成什么工作的。模式依据其目的可分为创建型(creational)、结构型(structural)和行为型(behavioral)三种,创建型模式与对象的创建有关,结构型模式处理类或对象的组合,行为型模式对类或对象怎样交互和怎样分配职责进行描述。第二条是范围准则
,指定模式主要是用于类还是用于对象。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时便确定下来了;而对象模式处理对象间的关系,这些关系在运行时可以变化的,更具动态性。
创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组装方式。行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述了一组对象怎样协作完成单个对象所无法完成的任务。
还有其他组织模式的方式。有些模式经常会绑在一起使用,例如:Composite常和Iterator或Visitor一起使用;有些模式是可替代的,例如:Prototype常用来替代Abstract Factory;有些模式尽管使用意图不同,但产生的设计结果是很相似的,例如:Composite和Decorator的结构图是相似的。
6. 设计模式怎样解决设计问题
6.1 寻找合适的对象
面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过程通常成为方法或操作。对象在收到客户的请求(或消息)后,执行相应的操作。客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。由于这些限制,对象的内部状态是被封装的,它不能被直接访问,它的表示对于对象外部是不可见的。
面向对象设计最困难的部分是将系统分解成对象集合,因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的。面向对象设计方法学支持许多设计方法。你可以写出一个问题描述,挑出名词和动词,进而创建相应的类和操作;或者,你可以关注系统的协作和职责关系;或者,你可以对现实世界建模,再将分析时发现的对象转化至设计中。至于哪一种方法最好,并无定论。
设计的许多对象来源于现实世界的分析模型,但是设计结果所得到的类通常在现实世界中并不存在,有些是像数组之类的底层类,而另一些则层次较高。例如,Composite模式引入了统一对待现实世界中并不存在的对象的抽象方法,严格反映当前现实世界的模型并不能产生也能反映将来世界的系统。设计中的抽象对于产生灵活的设计是至关重要的。设计模式帮助我们确定并不明显的抽象和描述这些抽象的对象,例如,Strategy模式描述了怎样实现可互换的算法族,State模式讲实体的每一个状态描述为一个对象。这些对象在分析阶段,甚至在设计阶段的早期并不存在,后来为使设计更灵活、复用性更好才将它们发掘出来。
6.2 决定对象的粒度
对象在大小和数目上变化极大,它们能表示下至硬件或上至整个应用的任何事物。那么我们怎样决定一个对象应该是什么呢?设计模式很好地讲述了这个问题。Facade模式描述了怎样用对象表示完整的子系统,Flyweight模式描述了如何支持大量的最小粒度的对象。其他一些设计模式描述了将一个对象分解成许多小对象的特定方法。Abstract Factory和Builder模式产生那些专门负责生成其他对象的对象。Visitor和Command模式生成的对象专门负责实现对其他对象或对象组的请求。
6.3 指定对象的接口
对象声明的每一个操作指定操作名、作为参数的对象和返回值,这就是所谓的操作的型构(signature)。对象操作所定义的所有操作型构的集合被成为该对象的接口(interface)。对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象。
类型(type)是一个用来表示特定接口的名字。如果一个对象接受“Windows”接口所定义的所有操作请求,那么就说该对象具有“Windows”类型。一个对象可以有许多类型,并且不同的对象可以共享同一个类型。对象接口的某部分可以用某个类型来刻画,而其他部分则可用其他类型刻画。两个类型相同的对象只需要共享它们的部分接口,接口可以包含其他接口作为子集。当一个类型的接口包含另一个类型的接口时,称它为另一个类型的子类型(subtype),而称另一个类型为它的超类型(supertype)。也就是我们常说的子类型继承了它的超类型的接口。
在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流,如果不通过对象的接口就无法知道对象的任何事情,也无法请求对象做任何事情。对象接口与其功能实现是分离的,不同对象可以对请求做不同的实现,也就是说两个有相同接口的对象可以有完全不同的实现。
当给对象发送请求时,所引起的具体操作既与请求本身有关,又与接受对象有关。支持相同请求的不同对象可能对请求激发的操作有不同的实现,发送给对象的请求和它的相应操作在运行时的连接就称为动态绑定(dynamic binding)。动态绑定是指发送的请求直到运行时才受到具体实现的约束。也就是说,动态绑定允许你在运行时彼此替换有相同接口的对象,这种可替换性就成为多态(polymorphism)。多态允许客户对象仅要求其他对象支持特定接口,简化了客户的定义,使得对象间彼此独立,并可以在运行时动态改变它们的相互关系。
设计模式通过确定接口的主要组成成分及经接口发送的数据类型来帮助定义接口,设计模式也许还会告诉我们接口中不应包括哪些东西。Memento模式是一个很好的例子,它描述了怎样封装和保存对象内部的状态,以便一段时间后对象能恢复到这一状态。它规定了Memento对象必须定义两个接口:一个允许客户保持和赋值memento的限制接口,一个只有原对象才能使用的用来存储和提取memento中状态的特权接口。设计模式也指定了接口之间的关系,特别是它们经常要求一些类具有相似的接口,或它们对一些类的接口做了限制。例如,Decorator和Proxy模式分别要求Decorator和Proxy对象的接口与被修饰的对象和委托的对象一致,而Visitor模式中接口必须反映出visitor能访问的对象的所有类。
6.4 描述对象的实现
对象的实现是由它的类决定的,类指定了对象的内部数据和表示,也定义了对象所能完成的操作。基于OMT(Object Modeling Technique)表示法,通常将类描述成一个矩形,其中的类名以黑体表示;操作在类名下方,以常规字体表示;类所定义的任何数据都在操作的下方。类名与操作之间以及操作与数据之间用横线分割,如下图所示(图中的返回类型和实例变量类型是可选的)。


新的类可以由已存在的类通过类继承(class inheritance)来定义。当子类(subclass)继承父类(parent class)时,子类包含了父类的所有数据和操作。子类的实例对象包括所以子类和父类定义的数据,且它们能完成子类和父类定义的所有操作。父类和子类关系的表示如下图所示:


补充:1)理解对象的类(class)与对象的类型(type)之间的差别非常重要。对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现,但是对象的类型只与它的接口有关,接口即对象能相应的请求的集合。一个对象可以有多个类型,不同类的对象可以有相同的类型。当然,对象的类和类型是有紧密关系的,因为类定义了对象所能执行的操作,也定义了对象的类型。当我们说一个对象是一个类的实例时,即指该对象支持类所定义的接口。2)理解类继承和接口继承(或子类型化)之间的差别也十分重要。类继承根据一个对象的实现定义了另一个对象的实现。简而言之,它是代码和表示的共享机制,然后,接口继承(或子类型化)描述了一个对象什么时候能被用来替代另一个对象。
6.5 运用复用机制
6.5.1 继承和组合的比较
面向对象系统中功能复用的两种最常用技术是,类继承和对象组合(object composition)。类继承允许你根据其他类的实现来定义一个类的实现,这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。这里“白箱”是相对可视性而言的,即在继承方式中,父类的内部细节对子类可见。新的更复杂的功能可以通过组装或组合对象来获得,对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的,对象只以“黑箱”的形式出现。
继承和组合各有优缺点。类继承是在编译时静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。类继承也存在不足之处,因为继承在编译时就定义了,所以无法在运行时改变从父类继承的实现;而且父类通常至少定义了部分子类的具体表示,因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性”。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。另外,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换,这种依赖关系就限制了灵活性并最终限制了复用性。常用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。
对象组合是通过获得对其他对象的引用而在运行时动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。对象组合对系统设计还有另一个作用,即优先使用对象组合有助于我们保持每个类被封装,并被集中到单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。基于对象组合的设计还会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。这里就突出了面向对象设计的第二个原则:优先使用对象组合,而不是类继承
。
6.5.2 委托
委托(delegation)是一种组合方法,它使组合具有与继承同样的复用能力。在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者(delegate)。这类似于子类将请求交给它的父类处理。使用继承时,被继承的操作总能引用到接受请求的对象,C++中通过this成员变量,Smalltalk中则通过self。委托方式为了得到同样的效果,接受请求的对象将自己传给被委托者(代理者),使被委托的操作可以引用接受请求的对象。
举例来说,我们可以在窗口类中保存一个矩形类的实例变量来代理矩形类的特定操作,这样窗口类可以复用矩形类的操作,而不必像继承时那样定义成矩形类的子类。也就是说,一个窗口拥有一个矩形,而不是一个窗口就是一个矩形。窗口现在必须显式地将请求转发给它的矩形实例,而不是像以前那样必须继承矩形的操作。下图就显示了窗口类将它的Area操作委托给一个矩形实例(图中的箭头线表示一个类对另一个类实例的引用关系):

委托的主要优点在于它便于运行时组合对象操作以及改变这些操作的组合方式。假定矩形对象和圆对象有相同的类型,我们只需要简单地用圆对象替换矩形对象,得到的窗口就是圆形的。当然,委托也具有不足之处:动态的、高度参数化的软件比静态软件更难于理解,以及存在运行低效的问题。委托最适用于符合特定程式的情形,即标准模式的情形。委托作为对象组合的特例,告诉我们对象组合作为一个代码复用机制可以代替继承。
6.5.3 继承和参数化类型的比较
另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是类属(generic)(Ada、Eiffel语言)或模板(C++语言)。它允许你在定义一个类型时不用指定该类型所引用到的其他所有类型。未经指定的类型在使用时以参数形式提供。例如,一个列表类能够以它所包含元素的类型来进行参数化。如果你想声明一个Integer列表,只需要将Integer类型作为列表参数化类型的参数值;同理要声明一个String列表,只需要提供一个String类型作为参数值。
参数化类型给我们能提供了类继承和对象组合外的第三种方法来组合面向对象系统中的行为。许多设计可以使用这三种技术中的任何一种来实现。实现一个以元素比较操作作为可变元的排序例程,可使用如下的方法:
(1)通过子类实现该操作;(2)实现要传给排序例程的职责;(3)作为C++模板或Ada类属的参数,以指定元素比较操作的名称。
这些技术存在着极大的不同之处。对象组合技术允许你在运行时改变被组合的行为,但是它存在间接性,比较低效。继承允许你提供操作的默认实现,并通过子类重定义这些操作。参数化类型允许你改变类所用到的类型,但是继承和参数化类型都不能在运行时改变。
6.6 关联运行时和编译时的结构
一个面向对象程序运行时的结构通常与它的代码结构相差较大。代码结构在编译时就被确定下来了,它由继承关系固定的类组成;而程序的运行时结构是由快速变化的通信对象网络组成的。事实上,这两个结构是彼此独立的,试图由一个去理解另一个就好像试图从静态的动植物分类去理解活生生的生态系统的动态性一样。
考虑聚合(aggregation)和相识(acquaintance)的差别以及它们在编译时和运行时的表示是多么不同。聚合意味着一个对象拥有另一个对象或对另一个对象负责。一般我们称一个对象包含另一个对象或者是另一个对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期。
相识意味着一个对象仅仅知道另一个对象。又是相识也被称为“关联”或“引用”关系。相识的对象可能请求彼此的操作,但是它们不为对方负责。相识是一种比聚合要弱的关系,它值表示了对象间较松散的耦合关系。比如下图所示的例子,普通的箭头表示相识,尾部带有菱形的箭头线表示聚合。

聚合和相识很容易混淆,因为它们通常以相同的方法实现。比如在C++中,聚合可以通过定义表示真正实例的成员变量来实现,但更通常的是将这些成员变量定义为实例指针或引用;相识也是以指针或引用来实现的。从根本上讲,是聚合还是相识是由你的意图而不是显式的语言机制决定的。尽管它们之间的区别在编译时的结构中很难看出来,但这些区别还是很大的。聚合关系使用较少且比相识关系更持久;而相识关系则出现频率较高,但有时只存在于一个操作期间,相识也更具动态性,使得它在源代码中更难被辨别出来。
程序的运行时结构和编译时结构存在这么大的差别,很明显代码不可能揭示关于系统如何工作的全部。系统的运行时结构更多地收到设计者而不是编程语言的影响。对象及其类型之间的关系必须更加仔细地设计,因为它们决定了运行时程序结构的好坏。
6.7 设计应支持变化
设计模式可以确保系统以特定方式变化,从而帮助你避免重新设计系统。每一个设计模式允许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某种特殊变化将更健壮。下面将介绍一些导致重新设计的一般原因,以及解决这些问题应该采用哪些设计模式:
(1)通过显式地指定一个类来创建对象
在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束,这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象,适合采用的设计模式有抽象工厂模式、工厂方法模式和原型模式。
(2)对特殊操作的依赖
当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了,为避免把请求代码写死,可以在编译时或运行时很方便地改变响应请求的方法,适合采用的设计模式有职责链模式和命令模式。
(3)对硬件和软件平台的依赖
外部的曹组系统接口和应用编程接口(API)在不同的软硬件平台上是不同的,依赖于特定平台的软件将很难移植到其他平台上,甚至很难跟上本地平台的更新,所以设计系统时限制其平台相关性就很重要了,适合采用的设计模式有抽象工厂模式和桥接模式。
(4)对对象表示或实现的依赖
知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化,对客户隐藏这些信息能阻止连锁变化,适合采用的设计模式有抽象工厂模式、桥接模式、备忘录模式和代理模式。
(5)算法依赖
算法在开发和复用时常常被扩展、优化和替代,依赖于摸个特定算法的对象在算法发生变化时不得不变化,因此有可能发生变化的算法应该被孤立起来,适合采用的设计模式有建造者模式、迭代器模式、桥接模式、备忘录模式和代理模式。
(6)紧耦合
紧耦合的类很难独立地被复用,因为它们通常相互依赖,这样的系统是i一个很难学习、移植和维护的密集体。松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展,适合采用的设计模式有抽象工厂模式、命令模式、外观模式、中介者模式、观察者模式和职责链模式。
(7)通过生成子类来扩充功能
通常很难通过定义子类来定制对象,每一个新类都有固定的实现开销(初始化、终止处理等),且定义子类还需要对父类有深入的了解。一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法,新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方法加到应用中去。另一方面,过多使用对象组合会使得设计难于理解。许多设计模式产生的设计中,可以定义一个子类,且将它的实例和已存在的实例将进行组合来引入定制的功能,适合采用的设计模式有桥接模式、职责链模式、组合模式、装饰模式、观察者模式和策略模式。
(8)不能方便地对类进行修改
有时不得不改变一个难以修改的类,可能需要源代码而又没有,或者可能对类的任何改变会要求修改许多已存在的其他子类。若要在这些情况下对类进行修改,适合采用的设计模式有适配器模式、装饰模式和访问者模式。
7. 怎样选择设计模式
针对特定设计问题,应该选择适合的模式,关于如何选择下面将提供几个不同的方法:
(1)考虑设计模式是怎样解决问题的。第6
节讨论了设计模式如何找到合适的对象、决定对象的粒度、指定对象接口以及设计模式解决设计问题的几个其他方法,参考这些将有助于找到合适的设计模式。
(2)浏览模式的意图部分。第4
节列出了所有设计模式的意图(intent),通过理解这些意图将可以找出问题相关的一个或多个设计模式,还可以参考下表缩小搜查范围。
(3)研究模式怎样互相关联。理解设计模式之间的关系,有助于获得合适的模式或模式组。
(4)研究模目的相似的模式。模式共分为三大类:创建型模式、结构型模式和行为型模式,需要去理解并分析具有相似目的模式之间的共同点和不同点。
(5)检查重新设计的原因。第6.7
节讨论了引起重新设计的各种原因,遇到问题先看是否与它们有关,然后再找出哪些设计模式能够提供帮助,避免重新设计。
(6)考虑设计中哪些是可变的。不是去考虑什么会迫使设计改变,而是考虑想要什么变化却又不会引起重新设计。最主要的一点是封装变化的概念,这是许多设计模式的主题,下表列出了设计模式允许独立变化的方面,即改变它们不会导致重新设计。
设计模式 | 可变的方面 |
---|---|
Abstract Factory | 产品对象家族 |
Adapter | 对象的接口 |
Bridge | 对象的实现 |
Builder | 如何创建一个组合对象 |
Chain of Responsibility | 满足一个请求的对象 |
Command | 何时、怎样满足一个请求 |
Composite | 一个对象的结构和组成 |
Decorator | 对象的职责,不生成子类 |
Facade | 一个子系统的接口 |
Factory Method | 被实例化的子类 |
Flyweight | 对象的存储开销 |
Interpreter | 一个语言的文法及解释 |
Iterator | 如何遍历、访问一个聚合的各元素 |
Mediator | 对象间怎样交互、和谁交互 |
Memento | 一个对象中哪些私有信息存放在该对象之外,以及在什么时候进行存储 |
Observer | 多个对象依赖于另一个对象,而这些对象又如何保持一致 |
Prototype | 被实例化的类 |
Proxy | 如何访问一个对象;该对象的位置 |
Singleton | 一个类的唯一示例 |
State | 对象的状态 |
Strategy | 算法 |
Template Method | 算法中的某些步骤 |
Visitor | 某些可作用于一个(组)对象上的操作,但不修改这些对象的类 |
8. 怎样使用设计模式
(1)大致浏览一遍模式。特别注意其适用性部分和效果部分,确定它适合解决遇到的问题。
(2)回头研究结构部分、参与者部分和协作部分。确保你理解这个模式的类和对象以及它们是怎样关联的。
(3)看代码示例部分,看看这个设计模式代码形式的具体例子。研究代码将有助于实现具体的模式。
(4)选择模式参与者的名字,使它们在应用上下文中有意义。设计模式参与者的名字通常国语抽象而不会直接出现在应用中,然而将参与者的名字和应用中出现的名字合并起来是很有用的。
(5)定义类。声明它们的接口,建立它们的继承关系,定义代表数据和对象引用的实例变量,识别模式会影响到应用中存在的类,并做出相应的修改。
(6)定义模式中专用与应用的操作名称。这里再一次体现出名字一般依赖于应用,使用与每一个操作相关联的责任和协作作为指导,并且名字约定要一致。
(7)实现执行模式中的责任和协作的操作。实现部分提供线索并指导进行实现,代码示例部分的例子也能提供帮助。

...
...