简介
- 设计模式:可复用面向对象软件的基础 阅读笔记
第一章 引言
- 设计面向对象软件比较困难,而设计可复用的面向对象软件就更加困难。你必须找到相关的对象,以适当的粒度将它们归类,再定义类的接口和继承层次,建立对象之间的基本关系
什么是设计模式
一般而言,一个模式有四个基本要素:
- 模式名称(pattern name),一个助记名,它用一两个词来描述模式的问题,解决方案和效果。
- 问题(problem),描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,它可能描述了特定的设计问题,例如怎样用对象表示算法等。也可能描述了导致了不灵活设计的类或对象结构。
- 解决方案(solution),描述了设计的组成成分,它们之间的相互关系以及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题
- 效果(consequences),描述了模式应用的效果以及使用模式应权衡的问题。尽管我们描述设计决策时,并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价和好处具有重要意义。软件效果大多关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象设计的要素之一,所以模式效果包括它对系统的灵活性,扩充性或者可移植性的映像。
本书中的设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述
组织编目
根据两条准则对模式进行分类。
第一是目的准则,即模式是用来完成什么工作的。
- 模式依据其目的可分为创建型(Creational),结构型(Structural),或行为型(Behavioral)三种。
- 创建型模式与对象的创建有关;
- 结构型模式处理类或对象的组合;
- 行为型模式对类或对象怎样交互和怎样分配职责进行描述
- 模式依据其目的可分为创建型(Creational),结构型(Structural),或行为型(Behavioral)三种。
第二是范围准则,指定模式主要用作于类还是用作于对象。
- 类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻便确定下来了。
- 对象模式处理对象间的关系,这些关系在运行时刻是可以变化的,更具有动态性。
- 从某种意义上来说,几乎所有模式都使用继承机制,所以 类模式 只指那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴
设计模式怎样解决设计问题
- 设计模式采用多种方法解决面向对象设计者经常碰到的问题
寻找合适的对象
面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过程通常称为方法或者操作。对象在收到客户的请求(或消息)后,执行相应的操作
客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。由于这些限制,对象的内部状态是被封装的,他不能被直接访问,它的表示对于对象外部是不可见的
面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装,粒度,依赖关系,灵活性,性能,演化,复用等等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的
设计模式帮你确定并不明显的抽象和描述这些抽象的对象
决定对象的粒度
- 对象在大小和数目上变化极大。它们能表示下自硬件或者上自整个应用的任何事物
指定对象接口
对象声明的每一个操作指定操作名,作为参数的对象和返回值,这就是所谓的操作的型构(signature)。
对象操作所定义的所有操作型构的集合被称为该对象的接口(interface)。
对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象
类型(type)是用来标识特定接口的一个名字。
接口可以包含其他接口作为子集。当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型(subtype),另一个类型称之为它的超类型(supertype)。
我们常说子类型继承了它的超类型的接口
当给对象发送请求时,所引起的具体操作即与请求本身有关又与接收对象有关。支持相同请求的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时刻的连接就称之为动态绑定(dynamice binding)
动态绑定是指发送的请求直到运行时刻才受到你的具体的实现的约束。
进一步将,动态绑定允许你在运行时刻彼此替换有相同接口的对象。这种可替换性就称为多态(polymorphism),它是面向对象系统中的核心概念之一
描述对象的实现
对象的实现是由它的类决定的,类指定了对象的内部数据和表示,也定义了对象所能够完成的操作。
对象通过实例化类来创建,此时对象被称为该类的实例。当实例化类时,要给对象的内部数据(由实例变量组成)分配存储空间,并将操作与这些数据联系起来。
新的类可以由已存在的类通过类继承(class inheritance)来定义。当子类(subclass)继承父类(parent class)时,子类包含了父类定义的所有数据和操作
抽象类(abstrace class)的主要目的是为它的子类定义公共接口。一个抽象类将把它的部分或者全部操作的实现延迟到子类中,因此,一个抽象类不能被实例化。在抽象类中定义却没有实现的操作被称为抽象操作(abstract operation)。
非抽象类称为具体类
子类能够改进和重新定义它们的父类的操作。更具体的说,类能够重定义(override)父类定义的操作,重定义使得子类能够接管父类对请求的处理操作。
混入类(mixin class)是给其他类提供可选择的接口或者功能的类。它与抽象类一样不能实例化。混入类要求多继承
类继承与接口继承的比较
理解对象的类(class)与对象的类型(type)之间的差别非常重要
一个对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。
但是对象的类型只与它的接口有关,接口即对象能够响应的请求的集合。
一个对象可以有多个类型,不同类的对象可以有相同的类型。
理解类继承和接口继承(或子类型化)之间的差别也十分重要。
类继承根据一个对象的实现定义了另一个对象的实现。简而言之,它是代码和表示的共享机制。
然而,接口继承(或子类型化)描述了一个对象什么时候能被用来替代另一个对象。
因为许多语言并不显式地区分这两个概念,所以容易被混淆。在C++ 和 Eiffel语言中,继承既指接口的继承又指实现的继承。
C++中接口继承的标准方法是公有继承一个含(纯)虚成员函数的类。
C++中纯接口继承接近于公有继承纯抽象类,纯实现继承或纯类继承接近于私有继承
对接口编程,而不是对实现编程
类继承是一个通过复用父类功能而扩展应用功能的基本机制。
然而,实现的复用只是成功的一半,继承所拥有的定义具有相同接口的对象族的能力也是很重要的(通常可以从抽象类来继承)。
为什么?因为多态依赖于这种能力
当继承被恰当使用时,所有从抽象类导出的类将共享该抽象类的接口。这意味着子类仅仅添加或重定义操作,而没有隐藏父类的操作。
这时,所有的子类都能响应抽象类接口中的请求,从而子类的类型都是抽象类的子类型。
只根据抽象类中定义的接口来操作对象有以下两个好处:
- 客户无须知道它们使用对象的特定类型,只须对象有客户所期望的接口
- 客户无须知道它们使用的对象是用什么类来实现的,它们只须知道定义接口的抽象类。
这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如下原则:
- 针对接口编程,而不是针对实现编程。
不将变量声明为某个特定的具体类的实例对象,而是让它遵循从抽象类所定义的接口。这是本书设计模式的一个常见主题。
运用复用机制
面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)
类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。
术语 白箱 是相对可视性而言:在继承方式中,父类的内部细节对子类可见
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。
对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现
类继承的优点:
- 在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。
- 较方便地改变被复用的实现。
- 当一个子类重定义一些而不是全部操作时,他也能影响他所继承的操作,只要在这些操作中调用了被重定义的操作。
类继承的缺点:
- 因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现
- 父类通常至少定义了部分子类的具体表示
- 因为继承对子类揭示了其父类的实现细节,所以继承常被认为破坏了封装性
- 子类中的实现与他的父类有紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类的变化
当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现
对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。
组合要求对象遵守彼此的接口约定,进而要求更仔细的定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。
这会产生良好的结果:
- 因为对象只能通过接口访问,所以我们并不破坏封装性
- 只要类型一致,运行时刻还可以用一个对象来替代另一个对象
- 更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系
对象组合对系统设计还有另一个作用,既优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。
另一方面,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中
这导出了我们的面向对象设计的第二个原则:
- 优先使用对象组合,而不是类继承。
理想情况下,你不应该为获得复用而去创建新的构件。你应该能够只使用对象组合技术,通过组装已有的构建就能获得你需要的功能。
但是事实很少如此,因为可用构建的集合实际上并不足够丰富。使用继承的复用使得创建新的构建要比组装旧的构建来的容易。
这样,继承和对象组合常一起使用。
委托(delegation)是一种组合方法,他使组合具有与继承同样的复用能力。在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给他的代理者(delegate)
这类似于子类将请求交给他的父类处理。使用继承时,被继承的操作总能引用接受请求的对象,C++中通过this成员变量
委托是对象组合的特例。它告诉你对象组合作为一个代码复用机制可以代替继承
另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是类属(generic)或者模板(templates (C++))。他允许你在定义一个类型时并不指定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。
参数化类型给我们提供了除了类继承和对象组合外的第三种方法来组合面向对象系统中的行为。许多设计可以使用这三种技术中的任何一种来实现。
实现一个以元素比较操作为可变元的排序例程,可有如下方法:
- 通过子类实现该操作
- 实现为传给排序例程的对象的职责
- 作为C++模板或Ada类属的参数,以指定元素比较操作的名称
关联运行时刻和编译时刻的结构
一个面向对象程序运行时刻的结构通常与他的代码结构相差很大。
代码结构在编译时刻就被确定下来了,他由继承关系固定的类组成。而程序的运行时刻结构是由快速变化的通信对象网格组成。
事实上,两个结构是彼此独立的,试图由一个区理解另一个就好像试图从静态的动植物分类去理解活生生的生态系统的动态性。反之亦然。
聚合(aggregation),意味着一个对象拥有另一个对象或对另一个对象负责。
一般我们称一个对象包含另一个对象或者另一个对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期
设计应支持变化
一些导致重新设计的一般原因,以及解决这些问题的设计模式:
- 通过显示的指定一个类来创建对象 : 在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象
- 设计模式: 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),那么它的内部复用性,可维护性和可扩充性是要优先考虑的。
内部复用性确保你不会做多余的设计和实现
设计模式通过减少依赖型来提高内部复用性。
松散耦合也增强了一类对象与其他多个对象协作的可能性。
怎样选择设计模式
考虑设计模式是怎样解决设计问题的
浏览模式的意图部分
研究模式怎样互相关联
研究目的相似的模式
检查重新设计的原因
考虑你的设计中那些是可变的
怎样使用设计模式
大致浏览一遍模式。
- 特别注意其适用性部分和效果部分,确定它适合你的问题
回头研究结构部分,参与者部分和协作部分
- 确保你理解这个模式的类和对象以及他们是怎样关联的
选择模式参与者的名字,使他们在应用上下文中有意义
定义类
- 声明它们的接口,建立他们的继承关系,定义代表数据和对象引用的实例变量。
- 识别模式会影响的你的应用中存在的类,作出相应的修改
定义模式中专用于应用的操作名称
- 名字一般依赖于应用。使用与每一个操作相关联的责任和写作作为指导
- 名字约定要一致
实现执行模式中责任和写作的操作
第二章 实例研究: 设计一个文档编辑器
递归组合
- 层次结构信息的表述通常是通过一种被称为递归组合(Recursive Composition)的技术来实现的。
- 递归组合可以由较简单的元素逐渐建立复杂的元素,是我们通过简单图形元素构造文档的方法之一。
第三章 创建型模式
创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建,组合和表示它的哪些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
创建型模式在什么被创建,谁创建它,它是怎样被创建的,以及何时创建这方面给予很大的灵活性。它们允许你用结构和功能差别很大的产品对象配置一个系统。配置可以是静态的(即在编译时指定的),也可以是动态的(在运行时)
ABSTRACT FACTORY(抽象工厂) – 对象创建型模式
意图:
- 提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类
别名:
- Kit
动机
- 考虑一个支持多种视感(look-and-feel)标准的用户界面工具包。不同的视感风格,为诸如滚动条,窗口和按钮等用户界面”窗口组件”定义不同的外观和行为。
- 为保证视感风格标准间的可移植性,一个应用不应该为一个特定的视感外观硬编码它的窗口组件。在整个应用中实例化特定视感风格的窗口组件类将使得以后很难改变视感风格
第五章 行为模式
5.1 CHAIN OF RESPON SIBILITY(责任链) – 对象行为型模式
5.2 COMMAND(命令) – 对象行为型模式
5.3 INTERPRETER(解释器) – 对象行为型模式
5.4 ITERATOR(迭代器) – 对象行为型模式
5.5 MEDIATOR(中介者) – 对象行为型模式
5.6 MEMENTO(备忘录) – 对象行为型模式
5.7 OBSERVER(观察者) – 对象行为型模式
5.8 STATE(状态) – 对象行为型模式
意图
- 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
别名
- 状态对象(Objects for States)
动机
- 考虑一个表示网络连接的类TCPConnection。一个TCPConnection对象的状态处于若干不同状态之一:连接已建立(Established),正在监听(Listening),连接已关闭(Closed)。当一个TCPConnection对象收到其他对象的请求时,它根据自身的当前状态做出不同的反应。例如:一个Open请求的结果依赖于该连接是处于连接已关闭状态还是连接已建立状态。
- State模式描述了TCPConnection如何在每一种状态下表现出不同的行为。
- 这一模式的关键思想是引入了一个称为TCPState的抽象类来表示网络的连接状态。TCPState类为各表示不同的操作状态的子类声明了一个公共接口。TCPState的子类实现与特定状态相关的行为。例如,TCPEstablished和TCPClosed类分别实现了特定于TCPConnection的连接已建立状态和连接已关闭状态的行为。
- TCPConnection类维护一个表示TCP连接当前状态的状态对象(一个TCPState子类的实例)。TCPConnection类将所有与状态相关的请求委托给这个状态对象。TCPConnection使用它的TCPState子类实例来执行特定于连接状态的操作。
- 一旦连接状态改变,TCPConnection对象就会改变它所使用的状态对象。例如当连接从已建立状态转为已关闭状态时,TCPConnection会用一个TCPClosed的实例来代替原来的TCPEstablished的实例。
适用性
- 在下面的两种情况下均可使用State模式
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常,有多个操作包含这一相同的条件结构。State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化
结构
参与者
- Context(环境,例如TCPConnection):
- 定义客户感兴趣的接口
- 维护一个ConcreteState子类的实例,这个实例定义当前状态。
- State(状态,例如TCPState)
- 定义一个接口以封装与Context的一个特定状态相关的行为
- ConcreteState subclasses(具体状态子类,例如TCPEstablished, TCPListen, TCPClosed)
- 每一子类实现一个与Context的一个状态相关的行为
协作
- Context将与状态相关的请求委托给当前的ConcreteState对象处理
- Context可将自身作为一个参数传递给处理该请求的状态对象。这使得状态对象在必要时可访问Context
- Context是客户使用的主要接口。客户可用状态对象来配置一个Context,一旦一个Context配置完毕,它的客户不再需要直接与状态对象打交道。
- Context或ConcreteState子类都可决定哪个状态是另外哪一个的后继者,以及是在何种条件下进行状态转换。
效果
- 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个State子类中,所以通过定义新的子类可以很容易的增加新的状态和转换。另一个方法是使用数据值定义内部状态并且让Context操作来显示的检查这些数据。但这样会使整个Context的实现中遍布看起来很相似的条件语句或case语句。增加一个新的状态可能需要改变若干个操作,这就使得维护变得复杂了。State模式避免了这个问题,但可能会引入另一个问题,因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些,否则需要使用巨大的条件语句。正如很长的过程一样,巨大的条件语句是不受欢迎的。他们形成一大整块并且使得代码不够清晰,这又使得它们难以修改和扩展。State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的if或switch语句中,而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。
- 它使得状态转换显示化。当一个对象仅以内部数据值来定义当前状态时,其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。而且,State对象可保证Context不会发生内部状态不一致的情况,因为从Context的角度看,状态转换是原子的–只需重新绑定一个变量(即Context的State对象变量),而无需多个变量赋值
- State对象可被共享。如果State对象没有实例变量–即它们表示的状态完全以它们的类型来编码,那么各Context对象可以共享一个State对象。当状态以这种方式被共享时,它们必然是没有内部状态,只有行为的轻量级对象。
实现
- 实现State模式有多方面的考虑
- 谁定义状态转换。State模式不指定哪一个参与者定义状态转换准则。如果该准则是固定的,那么它们可在Context中完全实现。然而若让State子类自身指定它们的后继状态以及何时进行转换,通常更灵活更何时。这需要Context增加一个接口,让State对象显示的设定Context的当前状态。用这种方法分散转换逻辑可以很容易的定义新的State子类来修改和扩展该逻辑。这样做的一个缺点是,一个State子类至少拥有一个其他子类的信息,这就在各子类之间产生了实现依赖。
- 基于表的另一种方法。在C++ Programming Style中,Cargil描述了另一种将结构加载在状态驱动的代码上的方法:他使用表将输入映射到状态转换。对每一个状态,一张表将每一个可能的输入映射到一个后继状态。实际上,这种方法将条件代码(和State模式下的虚函数)映射为一个查找表。表的主要好处是它们的规则性:你可以通过更改数据而不是更改程序代码来改变状态转换的准则。然而他也有一些缺点:
- 对表的查找通常不如函数调用效率高
- 用统一的,表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解
- 通常难以加入伴随状态转换的一些动作。表驱动的方法描述了状态和它们之间的转换,但必须扩充这个机制以便在每一个转换上能够进行任意的计算。
- 表驱动的状态机和State模式的主要区别可以被总结如下:State模式对与状态相关的行为进行建模,而表驱动的方法着重于定义状态转换。
- 创建和销毁State对象。一个常见的值的考虑的实现上的权衡是,究竟是 仅当需要State对象时才创建它们并随后销毁它们,还是提前创建它们并且始终不销毁它们。
- 当将要进入的状态在运行时是不可知的,并且上下文不经常改变状态时,第一种选择较为可取。这种方法避免创建不会被用到的对象,如果State对象存储大量的信息时这一点很重要。
- 当状态改变很频繁时,第二种方法较好。在这种情况下最好避免销毁状态,因为可能很快再次需要用到它们。此时可以预先一次付清创建各个状态对象的开销,并且在运行过程中根本不存在销毁状态对象的开销。但是这种方法可能不太方便,因为Context必须保存对所有可能进入的那些状态的引用。
- 使用动态继承。改变一个响应特定请求的行为可以用在运行时刻改变这个对象的类的办法实现,但这在大多数面向对象程序设计语言中都是不可能的。和其他一些基于委托的语言确实例外,它们提供这种机制,从而直接支持State模式。Self中的对象可将操作委托给其他对象以达到某种形式的动态继承。在运行时刻改变委托的目标有效的改变了继承的结构。这一机制允许对象改变它们的行为,也就是改变它们得类。