Skip to content

引言

什么是设计模式

本书中的设计模式事对用来在特定场景下解决一半设计问题的累和相互通信的对象的描述

一般而言,一个模式有四个基本要素

  • 模式名:一个助记名,用一两个词描述模式的问题、解决方案和效果。
  • 问题: 描述了应该在何时使用模式,解释了设计问题和问题存在的前因后果。
  • 解决方案:描述了设计的组成成分、它们之间的相互关系及各自职责和协作方式。
  • 效果:描述了模式应用的效果及使用模式应该权衡的问题,模式效果包括它对系统的灵活性、扩充性或可移植性的影响,显式地列出这些效果对理解和评价这些模式很有帮助。

MVC

MVC 包括三类对象。模型(Model)是应用对象,视图(View)是它在屏幕上的表示,控制器(Controller)定义用户界面对用户输入的响应方式。不使用 MVC,用户界面设计往往将这些对象混在一起,而 MVC 则将它们分离以提高灵活性和复用性。

Observer

MVC 通过建立一个“订购/通知”协议来分离视图和模型。视图必须保证它的显示正确地反映了模型的状态。一旦模型的数据发生变化,模型将通知有关的视图,每个视图相应地得到刷新自己的机会。这种方法可以让你为一个模型提供不同的多个视图表现形式,也能够为一个模型创建新的视图而无须重写模型。

举一个例子:

模型包含一些数据值,视图通过电子表格、柱状图、饼图等不同的方式来显示这些数据。当模型的数据发生变化时,模型就通知它的视图,而视图将与模型通信以获取这些数据值。

表面上看,这个例子反映了将视图和模型分离的设计,然而这个设计还可用于解决更一般的问题:将对象分离,使得一个对象的改变能够影响另一些对象,而这个对象并不需要知道那些被影响的对象的细节。这个更一般的设计被描述成 Observer(5.7)模式。

Composite

MVC 的另一个特征是视图可以嵌套。

例如,按钮控制面板可以用一个嵌套了按钮的复杂视图来实现。对象查看器的用户界面可由嵌套的视图构成,这些视图又可复用于调试器。MVC 用 View 类的子类——CompositeView 类来支持嵌套视图。CompositeView 类的对象行为类似于 View 类的对象行为,一个组合视图可用于任何视图可用的地方,但是它包含并管理嵌套视图。

这反映了可以将组合视图与其构件平等对待的设计,同样,该设计也适用于更一般的问题:将一些对象划为一组,并将该组对象当作一个对象来使用。这个设计被描述为 Composite(4.3)模式,该模式允许你创建一个类层次结构,一些子类定义了原子对象(如 Button)而其他类定义了组合对象(CompositeView),这些组合对象是由原子对象组合而成的更复杂的对象

Strategy

MVC 允许你在不改变视图外观的情况下改变视图对用户输入的响应方式。例如,你可能希望改变视图对键盘的响应方式,或希望使用弹出菜单而不是原来的命令键方式。MVC 将响应机制封装在 Controller 对象中。存在着一个 Controller 的类层次结构,使得可以方便地对原有 Controller 做适当改变而创建新的 Controller。

View 使用 Controller 子类的实例来实现一个特定的响应策略。要实现不同的响应策略只要用不同种类的 Controller 实例替换即可。甚至可以在运行时通过改变 View 的 Controller 来改变 View 对用户输入的响应方式。例如,一个 View 可以被禁止接收任何输入,只需给它一个忽略输入事件的 Controller。

View-Controller 关系是 Strategy(5.9)模式的一个例子。一个策略是一个表述算法的对象。当你想静态或动态地替换一个算法,或你有很多不同的算法,或算法中包含你想封装的复杂数据结构时,策略模式是非常有用的。

描述设计模式

  • 设计名和分类
  • 意图:设计模式是做什么,基本原理和意图是什么,解决什么特定问题
  • 别名
  • 动机:说明一个设计问题和如何使用该模式来解决问题
  • 适用性:什么情况下使用,可用来改进哪些不良设计
  • 结构
  • 参与者:指设计模式中的类和对象以及它们各自的职责
  • 协作:模式的参与者如何协作以实现功能
  • 效果:模式怎样支持目标,使用模式的效果和所需做的权衡是什么,系统结构哪些方面可以独立改变。
  • 实现:实现模式时需要知道的一些提示、技术要点以及应该避免的缺陷。
  • 代码示例
  • 已知应用:实际系统中发现的模式的例子
  • 相关模式

设计模式的编目

从第 3 章开始的模式目录中共包含 23 个设计模式。它们的名字和意图列举如下,以使你有个基本了解。每个模式名后的括号中标出模式所在的章节(整()本书都将遵从这个约定)。

  • 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-1)对模式进行分类。第一条是目的准则,即模式是用来完成什么工作的。模式依据其目的可分为创建型(creational)、结构型(structural)和行为型(behavioral)三种。

  • 创建型模式与对象的创建有关;
  • 结构型模式处理类或对象的组合;
  • 行为型模式对类或对象怎样交互和怎样分配职责进行描述。

第二条是范围准则,指定模式主要是用于类还是用于对象。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时便确定下来了。对象模式处理对象间的关系,这些关系在运行时是可以变化的,更具动态性。从某种意义上来说,几乎所有模式都使用继承机制,所以“类模式”只指那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴。

  • 创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。
  • 结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组装方式。
  • 行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述了一组对象怎样协作完成单个对象所无法完成的任务。

设计模式怎样解决设计问题

寻找合适的对象

面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过程通常称为方法或操作。对象在收到客户的请求或者消息之后,执行相应的操作。

客户请求时对象执行操作的唯一方法,操作是改变内部数据的唯一方法。

面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的。

面向对象设计方法学支持许多设计方法。你可以写出一个问题描述,挑出名词和动词,进而创建相应的类和操作;或者,你可以关注系统的协作和职责关系;或者,你可以对现实世界建模,再将分析时发现的对象转化至设计中。至于哪一种方法最好,并无定论。

决定对象的粒度

对象在大小和数目上变化极大。它们能表示下至硬件或上至整个应用的任何事物。那么我们怎样决定一个对象应该是什么呢?

设计模式很好地讲述了这个问题。Facade(4.5)模式描述了怎样用对象表示完整的子系统,Flyweight(4.6)模式描述了如何支持大量的最小粒度的对象。其他一些设计模式描述了将一个对象分解成许多小对象的特定方法。Abstract Factory(3.1)和 Builder(3.2)产生那些专门负责生成其他对象的对象。Visitor(5.10)和 Command(5.2)生成的对象专门负责实现对其他对象或对象组的请求。

指定对象的接口

对象声明的每一个操作指定操作名、作为参数的对象和返回值,这就是所谓的操作的型构(signature)。对象操作所定义的所有操作型构的集合被称为该对象的接口(interface)。对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象。

类型(type)是一个用来标识特定接口的名字。如果一个对象接受 “Window” 接口所定义的所有操作请求,那么我们就说该对象具有 “Window” 类型。一个对象可以有许多类型,并且不同的对象可以共享同一个类型。对象接口的某部分可以用某个类型来刻画,而其他部分则可用其他类型刻画。两个类型相同的对象只需要共享它们的部分接口。接口可以包含其他接口作为子集。当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型(subtype),而称另一个类型为它的超类型(supertype)。我们常说子类型继承了它的超类型的接口。

在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流,如果不通过对象的接口就无法知道对象的任何事情,也无法请求对象做任何事情。对象接口与其功能实现是分离的,不同对象可以对请求做不同的实现,也就是说,两个有相同接口的对象可以有完全不同的实现。

当给对象发送请求时,所引起的具体操作既与请求本身有关又与接受对象有关。支持相同请求的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时的连接就称为动态绑定(dynamic binding)。

动态绑定是指发送的请求直到运行时才受你的具体实现的约束。因而,在知道任何有正确接口的对象都将接受此请求时,你可以写一个一般的程序,它期待着那些具有该特定接口的对象。进一步讲,动态绑定允许你在运行时彼此替换有相同接口的对象。这种可替换性就称为多态(polymorphism),它是面向对象系统中的核心概念之一。多态允许客户对象仅要求其他对象支持特定接口,除此之外对其假设几近于无。多态简化了客户的定义,使得对象间彼此独立,并可以在运行时动态改变它们相互的关系。

设计模式通过确定接口的主要组成成分及经接口发送的数据类型来帮助你定义接口。设计模式也许还会告诉你接口中不应包括哪些东西。Memento(5.6)模式是一个很好的例子,它描述了怎样封装和保存对象内部的状态,以便一段时间后对象能恢复到这一状态。它规定了 Memento 对象必须定义两个接口:一个允许客户保持和复制 memento 的限制接口,一个只有原对象才能使用的用来储存和提取 memento 中状态的特权接口。

设计模式也指定了接口之间的关系。特别是,它们经常要求一些类具有相似的接口,或它们对一些类的接口做了限制。例如,Decorator(4.4)和 Proxy(4.7)模式分别要求 Decorator 和 Proxy 对象的接口与被修饰的对象和受委托的对象一致。而 Visitor(5.11)模式中,Visitor 接口必须反映出 visitor 能访问的对象的所有类

描述对象的实现

对象的实现是由它的类决定的,类指定了对象的内部数据和表示,也定义了对象所能完成的操作。

对象通过实例化类来创建,此对象被称为该类的实例。当实例化类时,要给对象的内部数据(由实例变量组成)分配存储空间,并将操作与这些数据联系起来。对象的许多类似实例是由实例化同一个类来创建的。

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

**抽象类(abstract class)**的主要目的是为它的子类定义公共接口。抽象类将把它的部分或全部操作的实现延迟到子类中,因此,抽象类不能被实例化。在抽象类中定义却没有实现的操作被称为抽象操作(abstract operation)。非抽象类称为具体类(concrete class)。

子类能够改进和重新定义它们父类的操作。更具体地说,类能够**重定义(override)**父类定义的操作,重定义使得子类能接管父类对请求的处理操作。类继承允许你只需要简单地扩展其他类就可以定义新类,从而可以很容易地定义具有相近功能的对象族。

**混入类(mixin class)**是给其他类提供可选择的接口或功能的类。它与抽象类一样不能实例化。混入类要求多继承。

类继承与接口继承的比较

理解对象的类(class)与对象的类型(type)之间的差别非常重要。

对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。但是对象的类型只与它的接口有关,接口即对象能响应的请求的集合。一个对象可以有多个类型,不同类的对象可以有相同的类型。

理解类继承和接口继承(或子类型化)之间的差别也十分重要。类继承根据一个对象的实现定义了另一个对象的实现。简而言之,它是代码和表示的共享机制。然而,接口继承(或子类型化)描述了一个对象什么时候能被用来替代另一个对象。

对接口编程而不是实现编程

当继承被恰当使用时,所有从抽象类导出的类将共享该抽象类的接口。这意味着子类仅仅添加或重定义操作,而没有隐藏父类的操作。这时,所有的子类都能响应抽象类接口中的请求,从而子类的类型都是抽象类的子类型。

只根据抽象类中定义的接口来操纵对象有以下两个好处:

  1. 客户无须知道他们使用对象的特定类型,只需要知道对象有客户所期望的接口。
  2. 客户无须知道他们使用的对象是用什么类来实现的,只需要知道定义接口的抽象类。

这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如下原则:

针对接口编程,而不是针对实现编程。

不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口。这是本书设计模式常见的主题。

运用复用机制

继承和组合比较

面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)。

  • 正如我们已解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言的:在继承方式中,父类的内部细节对子类可见。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

继承和组合各有优缺点。

继承

类继承是在编译时静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。

但是类继承也有一些不足之处。首先,因为继承在编译时就定义了,所以无法在运行时改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性”。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。

当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

组合

对象组合是通过获得对其他对象的引用而在运行时动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。

对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另外,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。

这导出了我们的面向对象设计的第二个原则:

优先使用对象组合,而不是类继承。

委托

委托(delegation)是一种组合方法,它使组合具有与继承同样的复用能力。在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者(delegate)。这类似于子类将请求交给它的父类处理。使用继承时,被继承的操作总能引用接受请求的对象。委托方式为了得到同样的效果,接受请求的对象将自己传给被委托者(代理者),使被委托的操作可以引用接受请求的对象。

委托的主要优点在于它便于运行时组合对象操作以及改变这些操作的组合方式。假定矩形对象和圆对象有相同的类型,我们只需要简单地用圆对象替换矩形对象,得到的窗口就是圆形的。

委托与那些通过对象组合取得软件灵活性的技术一样,具有如下不足之处:动态的、高度参数化的软件比静态软件更难于理解。还有运行低效问题,不过从长远来看人的低效才是更主要的。只有当委托使设计比较简单而不是更复杂时,它才是好的选择。要给出一个能确切告诉你什么时候可以使用委托的规则是很困难的。因为委托可以得到的效率是与上下文有关的,并且还依赖于你的经验。委托最适用于符合特定程式的情形,即标准模式的情形。

继承和参数化类型的比较

另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是类属(generic)(Ada、Eiffel)或模板 (template) (C++)。它允许你在定义一个类型时不用指定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。例如,一个列表类能够以它所包含元素的类型来进行参数化。如果你想声明一个 Integer 列表,只需要将 Integer 类型作为列表参数化类型的参数值;声明一个 String 列表,只需要提供 String 类型作为参数值。语言的实现将会为各种元素类型创建相应的列表类模板的定制版本。

参数化类型给我们提供除了类继承和对象组合外的第三种方法来组合面向对象系统中的行为。许多设计可以使用这三种技术中的任何一种来实现。实现一个以元素比较操作为可变元的排序例程,可使用如下方法:

  1. 通过子类实现该操作(Template Method(5.10)的一个应用)。
  2. 实现要传给排序例程的对象的职责(Strategy(5.9))。
  3. 作为 C++模板或 Ada 类属的参数,以指定元素比较操作的名称。

这些技术存在着极大的不同之处。对象组合技术允许你在运行时改变被组合的行为,但是它存在间接性,比较低效。继承允许你提供操作的默认实现,并通过子类重定义这些操作。参数化类型允许你改变类所用到的类型。但是继承和参数化类型都不能在运行时改变。哪一种方法最佳,取决于你设计和实现的约束条件。

关联运行时和编译时的结构

一个面向对象程序运行时的结构通常与它的代码结构相差较大。

  • 代码结构在编译时就被确定下来了,它由继承关系固定的类组成。
  • 而程序的运行时结构是由快速变化的通信对象网络组成的。

事实上两个结构是彼此独立的,试图由一个去理解另一个就好像试图从静态的动植物分类去理解活生生的生态系统的动态性一样。反之亦然。

考虑对象**聚合(aggregation)相识(acquaintance)**的差别以及它们在编译时和运行时的表示是多么不同。

  • 聚合意味着一个对象拥有另一个对象或对另一个对象负责。一般我们称一个对象包含另一个对象或者是另一个对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期。
  • 相识意味着一个对象仅仅知道另一个对象。有时相识也被称为“关联”或“引用”关系。相识的对象可能请求彼此的操作,但是它们不为对方负责。相识是一种比聚合要弱的关系,它只标识了对象间较松散的耦合关系。

从根本上讲,是聚合还是相识是由你的意图而不是显式的语言机制决定的。尽管它们之间的区别在编译时的结构中很难看出来,但这些区别还是很大的。

  • 聚合关系使用较少且比相识关系更持久;
  • 而相识关系则出现频率较高,但有时只存在于一个操作期间,相识也更具动态性,使得它在源代码中更难被辨别出来。

程序的运行时结构和编译时结构存在这么大的差别,很明显代码不可能揭示关于系统如何工作的全部。系统的运行时结构更多地受到设计者而不是编程语言的影响。对象及其类型之间的关系必须更加仔细地设计,因为它们决定了运行时程序结构的好坏。

设计应支持变化

获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,要求你的系统设计能够相应地改进。

为了设计适应这种变化且具有健壮性的系统,你必须考虑系统在它的生命周期内会发生怎样的变化。一个不考虑系统变化的设计在将来就有可能需要重新设计。这些变化可能是类的重新定义和实现,修改客户和重新测试。重新设计会影响软件系统的许多方面,并且未曾料到的变化总是代价巨大的。

设计模式可以确保系统以特定方式变化,从而帮助你避免重新设计系统。每一个设计模式允许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某种特殊变化将更健壮。

下面阐述了一些导致重新设计的一般原因,以及解决这些问题的设计模式:

通过显式地指定一个类来创建对象

在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。这会使未来的变化更复杂。

要避免这种情况,应该间接地创建对象。

设计模式:Abstract Factory(3.1),Factory Method(3.3),Prototype(3.4)。

对特殊操作的依赖

当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。

为避免把请求代码写死,你将可以在编译时或运行时很方便地改变响应请求的方法。

设计模式:Chain of Resposibility(5.1),Command(5.2)。

对硬件和软件平台的依赖

外部的操作系统接口和应用编程接口(API)在不同的软硬件平台上是不同的。

依赖于特定平台的软件将很难移植到其他平台上,甚至很难跟上本地平台的更新。所以设计系统时限制其平台相关性就很重要了。

设计模式:Abstract Factory(3.1),Bridge(4.2)。

对对象表示或实现的依赖

知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。

对客户隐藏这些信息能阻止连锁变化。

设计模式:Abstract Factory(3.1),Bridge(4.2),Memento(5.6),Proxy(4.7)。

算法依赖

算法在开发和复用时常常被扩展、优化和替代。

依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。

设计模式:Builder(3.2),Iterator(5.4),Strategy(5.9),Template Method(5.10),Visitor(5.11)。

紧耦合

紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,你必须理解和改变其他许多类。这样的系统是一个很难学习、移植和维护的密集体。

松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。

设计模式:Abstract Factory(3.1),Command(5.2),Facade(4.5),Mediator(5.5),Observer(5.7),Chain of Responsibility(5.1)。

通过生成子类来扩充功能

通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。例如,重定义一个操作可能需要重定义其他操作。一个被重定义的操作可能需要调用继承下来的操作。并且子类方法会导致类爆炸,因为即使对于一个简单的扩充,你也不得不引入许多新的子类。

一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,可以定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。

设计模式:Bridge(4.2),Chain of Responsibility(5.1),Composite(4.3),Decorator(4.4),Observer(5.7),Strategy(5.9)。

不能方便地对类进行修改

有时你不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。

设计模式提供在这些情况下对类进行修改的方法。

设计模式:Adapter(4.1),Decorator(4.4),Visitor(5.11)。

由于模式和框架有些类似,人们常常对它们有怎样的区别和它们是否有区别感到疑惑。它们最主要的不同在于如下三个方面:

  1. 设计模式比框架更抽象 框架能够用代码表示,而设计模式只有其实例才能表示为代码。框架的威力在于它们能够使用程序设计语言写出来,它们不仅“它们不仅能被学习,也能被直接执行和复用。而本书中的设计模式在每一次被复用时,都需要实现。设计模式还解释了它的意图、权衡和设计效果。
  2. 设计模式是比框架更小的体系结构元素 一个典型的框架包括了多个设计模式,而反之决非如此。
  3. 框架比设计模式更加特例化 框架总是针对一个特定的应用领域。一个图形编辑器框架可能被用于一个工厂模拟,但它不会被错认为是一个模拟框架。而本书收录的设计模式几乎能被用于任何应用。当然可以有比我们的模式更特殊的设计模式(例如,分布式系统和并发程序的设计模式),尽管这些模式不会像框架那样描述应用的体系结构。

怎样选择设计模式

  1. 考虑设计模式是怎样解决设计问题的
  2. 浏览模式的意图部分
  3. 研究模式怎样互相关联
  4. 研究目的相似的模式
  5. 检查重新设计的原因
  6. 考虑你的设计哪些是可变法

怎样使用设计模式

  1. 大致浏览一遍模式
  2. 回头研究结构部分、参与者部分和协作部分
  3. 看代码示例部分,看看这个模式代码形式的具体例子
  4. 选择模式参与者的名字,使它们在应用上下文中有意义
  5. 定义类
  6. 定义模式中专用于应用的操作名称
  7. 实现执行模式中责任和协作的操作

备案号:闽ICP备2024028309号-1