结构型模式
引言
结构型模式涉及如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。
- 一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。这一模式尤其有助于多个独立开发的类库协同工作。
- 另外一个例子是类形式的 Adapter(4.1)模式。一般来说,适配器使得一个接口(adapter 的接口)与其他接口兼容,从而给出多个不同接口的统一抽象。为此,类适配器对一个 adapter 类进行私有继承。这样,适配器就可以用 adapter 的接口表示它的接口。
结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可以在运行时改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
Composite(4.3)模式是结构型对象模式的一个实例。它描述了如何构造一个类层次式结构,这一结构由两种类型的对象(基元对象和组合对象)所对应的类构成。其中的组合对象使得你可以组合基元对象以及其他的组合对象,从而形成任意复杂的结构。
在 Proxy(4.7)模式中,proxy 对象作为其他对象的一个方便的替代或占位符。它的使用可以有多种形式。例如,它可以在局部空间中代表一个远程地址空间中的对象,也可以表示一个要求被加载的较大的对象,还可以用来保护对敏感对象的访问。Proxy 模式还提供了对对象的一些特有性质的一定程度上的间接访问,从而它可以限制、增强或修改这些性质。
Flyweight(4.6)模式为了共享对象定义了一个结构。至少有两个原因要求对象共享:效率和一致性。Flyweight 的对象共享机制主要强调对象的空间效率。Flyweight 的对象共享机制主要强调对象的空间效率。使用很多对象的应用必须考虑每一个对象的开销。使用对象共享而不是进行对象复制,可以节省大量的空间资源。但是仅当这些对象没有定义与上下文相关的状态时,它们才可以被共享。Flyweight 的对象没有这样的状态,任何执行任务时需要的其他信息仅当需要时才传递过去。由于不存在与上下文相关的状态,因此 Flyweight 对象可以被自由地共享。
如果说 Flyweight 模式说明了如何生成很多较小的对象,那么 Facade(4.5)模式则描述了如何用单个对象表示整个子系统。模式中的 facade 用来表示一组对象,facade 的职责是将消息转发给它所表示的对象。
Bridge(4.2)模式将对象的抽象和其实现分离,从而可以独立地改变它们。
Decorator(4.4)模式描述了如何动态地为对象添加职责。Decorator 模式是一种结构型模式。这一模式采用递归方式组合对象,从而允许你添加任意多的对象职责。例如,一个包含用户界面组件的 Decorator 对象可以将边框或阴影这样的装饰添加到该组件中,或者它可以将窗口滚动和缩放这样的功能添加到组件中。将一个 Decorator 对象嵌套在另一个对象中就可以很简单地增加两个装饰,添加其他的装饰也是如此。因此,每个 Decorator 对象必须与其组件的接口兼容并且保证将消息传递给它。Decorator 模式在转发一条信息 Decorator 模式在转发一条信息之前或之后都可以完成它的工作(比如绘制组件的边框)。
Adapter(适配器)——类对象结构型模式
意图
将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
别名
包装器(wrapper)。
动机
有时,为复用而设计的工具箱类不能够被复用仅仅是因为它的接口与专业应用领域所需要的接口不匹配。
适用性
- 你想使用一个已经存在的类,而它的接口不符合你的需求。
- 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
- (仅适用于对象 Adapter)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。
参与者
- Target: 定义 Client 使用的与特定领域相关的接口。
- Client: 与符合 Target 接口的对象协同。
- Adaptee: 定义一个已经存在的接口,这个接口需要适配。
- Adapter: 对 Adaptee 的接口与 Target 接口进行适配。
协作
Client 在 Adapter 实例上调用一些操作。接着适配器调用 Adaptee 的操作实现这个请求。
效果
类适配器和对象适配器有不同的权衡。
类适配器的权衡为:
- 用一个具体的 Adapter 类对 Adaptee 和 Target 进行匹配。结果是当我们想要匹配一个类以及所有它的子类时,类 Adapter 将不能胜任工作。
- 使得 Adapter 可以重定义 Adaptee 的部分行为,因为 Adapter 是 Adaptee 的一个子类。
- 仅仅引入了一个对象,并不需要额外的指针以间接得到 Adaptee。
对象适配器的权衡为:
- 允许一个 Adapter 与多个 Adaptee——Adaptee 本身以及它的所有子类(如果有子类的话)——同时工作。Adapter 也可以一次给所有的 Adaptee 添加功能。
- 使得重定义 Adaptee 的行为比较困难。这就需要生成 Adaptee 的子类并且使得 Adapter 引用这个子类而不是引用 Adaptee 本身。
使用 Adapter 模式时需要考虑的其他一些因素有:
- Adapter 的匹配程度: 对 Adaptee 的接口与 Target 的接口进行匹配的工作量,各个 Adapter 可能不一样。工作范围可能是从简单的接口转换(例如改变操作名)到支持完全不同的操作集合。Adapter 的工作量取决于 Target 接口与 Adaptee 接口的相似程度。
- 可插入的 Adapter: 当其他的类使用一个类时,所需的假定条件越少,这个类就更具可复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同的接口。也就是说,接口匹配使得我们可以将自己的类加入一些现有的系统中去,而这些系统对这个类的接口可能会有所不同。
- 考虑 TreeDisplay 窗口组件,它可以图形化显示树状结构。如果这是一个具有特殊用途的窗口组件,仅在一个应用中使用,我们可能要求它所显示的对象有一个特殊的接口,即它们都是抽象类 Tree 的子类。如果我们希望使 TreeDisplay 具有良好的复用性的话(比如说,我们希望将它作为可用窗口组件工具箱的一部分),那么这种要求将是不合理的。应用程序将自己定义树结构类,而不一定要使用我们的抽象类 Tree。不同的树结构会有不同的接口。
- 例如,在一个目录层次结构中,可以通过 GetSubdirectory 操作访问子目录,然而在一个继承式层次结构中,相应的操作可能被称为 GetSubclass。尽管这两种层次结构使用的接口不同,但一个可复用的 TreeDisplay 窗口组件必须能显示这两种结构。也就是说,TreeDisplay 应具有接口适配的功能。
- 使用双向适配器提供透明操作: 使用适配器的一个潜在问题是,它们不对所有的客户都透明。被适配的对象不再兼容 Adaptee 的接口,因此并不是所有 Adaptee 对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性。在两个不同的客户需要用不同的方式查看同一个对象时,双向适配器尤其有用。
实现
尽管 Adapter 模式的实现方式通常简单直接,但是仍需要注意以下一些问题:
- 应该是 Target 的子类型: Adapter 类应该采用公共方式继承 Target 类,并且用私有方式继承 Adaptee 类。因此,Adapter 类应该是 Target 的子类型,但不是 Adaptee 的子类型。
- 可插入的适配器: 对于它有三种实现方法:
- 首先(这也是所有这三种实现都要做的)是为 Adaptee 找到一个“窄”接口,即可用于适配的最小操作集,因为包含较少操作的窄接口相对包含较多操作的宽接口比较容易进行匹配。对这个窄接口,有以下三个实现途径:1. 使用抽象操作 2. 使用代理对象 3. 参数化适配器
相关模式
- 模式 Bridge(4.2)的结构与对象适配器类似,但是 Bridge 模式的出发点不同:Bridge 的目的是将接口部分和实现部分分离,从而可以对它们较为容易也相对独立地加以改变。而 Adapter 则意味着改变一个已有对象的接口。
- Decorator(4.4)模式增强了其他对象的功能而同时又不改变它的接口,因此 Decorator 对应用程序的透明性比适配器要好。结果是 Decorator 支持递归组合,而纯粹使用适配器是不可能实现这一点的。
- 模式 Proxy(4.7)在不改变它的接口的条件下,为另一个对象定义了一个代理。
Bridge(桥接)——对象结构型模式
意图
将抽象部分与它的实现部分分离,使它们可以独立地变化。
别名
Handle/Body。
动机
当一个抽象可能有多个实现时,通常用继承来协调它们。抽象类定义对该抽象的接口,而具体的子类则用不同方式加以实现。但是此方法有时不够灵活。继承机制将抽象部分与它的实现部分固定在一起,使得难以对抽象部分和实现部分独立地进行修改、扩充和复用。我们可以通过 Bridge 在抽象类与它的实现之间起到了桥梁作用,使它们可以独立地变化。
适用性
- 你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如,这种情况可能是因为,在程序运行时实现部分应可以被选择或者切换。
- 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时 Bridge 模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
- 对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译。
- (C++)你想对客户完全隐藏抽象的实现部分。在 C++中,类的表示在类接口中是可见的。
- 有许多类要生成。这样一种类层次结构说明你必须将一个对象分解成两个部分。Rumbaugh 称这种类层次结构为“嵌套的泛化
- 你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。一个简单的例子便是 Coplien 的 String 类,在这个类中多个对象可以共享同一个字符串表示(StringRep)。
参与者
- Abstraction:
- 定义抽象类的接口。
- 维护一个指向 Implementor 类型对象的指针。
- RefinedAbstraction: 扩充由 Abstraction 定义的接口。
- Implementor: 定义实现类的接口,该接口不一定要与 Abstraction 的接口完全一致,事实上这两个接口可以完全不同。一般来讲,Implementor 接口仅提供基本操作,而 Abstraction 则定义了基于这些基本操作的较高层次的操作。
- ConcreteImplementor: 实现 Implementor 接口并定义它的具体实现。
协作
Abstraction 将 client 的请求转发给它的 Implementor 对象。
效果
Bridge 模式有以下一些优点:
- 分离接口及其实现部分 一个实现未必不变地绑定在一个接口上。抽象类的实现可以在运行时进行配置,一个对象甚至可以在运行时改变它的实现。
- 将 Abstraction 与 Implementor 分离有助于降低对实现部分编译时的依赖性,当改变一个实现类时,并不需要重新编译 Abstraction 类和它的客户程序。为了保证一个类库的不同版本之间的二进制兼容性,一定要有这个性质。
- 另外,接口与实现分离有助于分层,从而产生更好的结构化系统,系统的高层部分仅需知道 Abstraction 和 Implementor。
- 提高可扩充性 你可以独立地对 Abstraction 和 Implementor 层次结构进行扩充。
- 实现细节对客户透明 你可以对客户隐藏实现细节,例如共享 Implementor 对象以及
实现
使用 Bridge 模式时需要注意以下一些问题:
- 仅有一个 Implementor 在仅有一个实现的时候,没有必要创建一个抽象的 Implementor 类。这是 Bridge 模式的退化情况,在 Abstraction 与 Implementor 之间有一种一对一的关系。尽管如此,当你希望改变一个类的实现不会影响已有的客户程序时,模式的分离机制还是非常有用的,也就是说,不必重新编译它们,仅需重新连接。
- 创建正确的 Implementor 对象 当存在多个 Implementor 类的时候,你应该用何种方法,何时在何处确定创建哪一个 Implementor 类呢?
- 如果 Abstraction 知道所有的 ConcreteImplementor 类,它就可以在它的构造器中对其中的一个类进行实例化,它可以通过传递给构造器的参数确定实例化哪一个类。例如,如果一个 collection 类支持多重实现,就可以根据 collection 的大小决定实例化哪一个类。链表的实现可以用于较小的 collection 类,而 hash 表则可用于较大的 collection 类。
- 另外一种方法是首先选择一个缺省的实现,然后根据需要改变这个实现。例如,如果一个 collection 的大小超出了一定的阈值,它将会切换它的实现,使之更适用于表目较多的 collection。
- 也可以代理给另一个对象,由它一次决定。我们可以引入一个 factory 对象(参见 Abstract Factory),该对象的唯一职责就是封装系统平台的细节。这个对象知道应该为所用的平台创建何种类型的实现对象,抽象类仅需向它请求一个实现类,而它会返回正确类型的实现类对象。这种方法的优点是 Abstraction 类不和任何一个 Implementor 类直接耦合。
- 共享 Implementor 对象
- 采用多重继承机制 在 C++中可以使用多重继承机制将抽象接口和它的实现部分结合起来。例如,一个类可以用 public 方式继承 Abstraction 而以 private 方式继承 ConcreteImplementor。但是由于这种方法依赖于静态继承,它将实现部分与接口固定不变地绑定在一起。因此不可能使用多重继承的方法实现真正的 Bridge 模式——至少用 C++不行
相关模式
- Abstract Factory(3.1)模式可以用来创建和配置一个特定的 Bridge 模式。
- Adapter(4.1)模式用来帮助无关的类协同工作,它通常在系统设计完成后才会被使用。然而,Bridge 模式则是在系统开始时就被使用,它使得抽象接口和实现部分可以独立进行改变。
Composite(组合)——对象结构型模式
意图
将对象组合成树形结构以表示“部分–整体”的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。
动机
Composite 模式实现将一些类组合成一个组件,然后递归调用。
比如我们有一个需求是实现用多个组件形成一些较大的组件,又可以通过这些较大的组件组成更大的组件,那么,Composite 模式就可以帮助我们递归调用子部件去实现功能。
适用性
- 你想表示对象的 部分–整体 层次结构。
- 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
参与者
- Component:
- 为组合中的对象声明接口。
- 在适当的情况下,实现所有类共有接口的缺省行为。
- 声明一个接口用于访问和管理 Component 的子组件。
- (可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
- Leaf:
- 在组合中表示叶结点对象,叶结点没有子结点。
- 在组合中定义图元对象的行为。
- Composite:
- 定义有子部件的那些部件的行为。
- 存储子部件。
- 在 Component 接口中实现与子部件有关的操作。
- Client: 通过 Component 接口操纵组合部件的对象。
协作
用户使用 Component 类接口与组合结构中的对象进行交互。如果接收者是一个叶结点,则直接处理请求。如果接收者是 Composite,它通常将请求发送给它的子部件,在转发请求之前和/或之后可能执行一些辅助操作。
效果
- 定义了包含基本对象和组合对象的类层次结构 基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断地递归下去。客户代码中,任何用到基本对象的地方都可以使用组合对象。
- 简化客户代码 客户可以一致地使用组合结构和单个对象。通常用户不知道(也不关心)处理的是一个叶结点还是一个组合组件。这就简化了客户代码,因为在定义组合的那些类中不需要写一些充斥着选择语句的函数。
- 使得更容易增加新类型的组件 新定义的 Composite 或 Leaf 子类自动地与已有的结构和客户代码一起工作,客户程序不需要因新的 Component 类而改变。
- 使你的设计变得更加一般化 容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。有时你希望一个组合只能有某些特定的组件。使用 Composite 时,你不能依赖类型系统施加这些约束,而必须在运行时进行检查。
实现
在实现 Composite 模式时需要考虑以下几个问题:
- 显式的父部件引用 保持从子部件到父部件的引用能简化组合结构的遍历和管理。
- 父部件引用可以简化结构的上移和组件的删除,同时父部件引用也支持 Chain of Responsibility(5.2)模式。
- 通常在 Component 类中定义父部件引用。Leaf 和 Composite 类可以继承这个引用以及管理这个引用的那些操作。
- 对于父部件引用,必须维护一个不变式,即一个组合的所有子结点以这个组合为父结点,反之,该组合以这些结点为子结点。保证这一点最容易的办法是,仅当在一个组合中增加或删除一个组件时,才改变这个组件的父部件。如果能在 Composite 类的 Add 和 Remove 操作中实现这种方法,那么所有的子类都可以继承这一方法,并且将自动维护这一不变式。
- 共享组件 共享组件是很有用的,比如它可以减少对存储的需求。但是当一个组件只有一个父部件时,很难共享组件。一个可行的解决办法是为子部件存储多个父部件,但当一个请求在结构中向上传递时,这种方法会导致多义性。Flyweight(4.6)模式讨论了如何修改设计以避免将父部件存储在一起的方法。如果子部件可以将一些状态(或是所有的状态)存储在外部,从而不需要向父部件发送请求,那么这种方法是可行的。
- 最大化 Component 接口
- Composite 模式的目的之一是使得用户不知道他们正在使用的具体的 Leaf 和 Composite 类。为了达到这一目的,Composite 类应为 Leaf 和 Composite 类尽可能多定义一些公共操作。Composite 类通常为这些操作提供缺省的实现,而 Leaf 和 Composite 子类可以对它们进行重定义。
- 然而,这个目标有时可能会与类层次结构设计原则相冲突,该原则规定:一个类只能定义那些对它的子类有意义的操作。有许多 Component 所支持的操作对 Leaf 类似乎没有什么意义,那么 Component 怎样为它们提供一个缺省的操作呢?
- 有时一点创造性可以使得一个看起来仅对 Composite 才有意义的操作,通过将它移入 Component 类中,就会对所有的 Component 都适用。例如,访问子结点的接口是 Composite 类的一个基本组成部分,但对 Leaf 类来说并不必要。但是如果我们把一个 Leaf 看成一个没有子结点的 Component,就可以在 Component 类中定义一个缺省的操作,用于对子结点进行访问,这个缺省的操作不返回任何一个子结点。Leaf 类可以使用缺省的实现,而 Composite 类则会重新实现这个操作以返回它们的子类。
- 声明管理子部件的操作: 虽然 Composite 类实现了 Add 和 Remove 操作用于管理子部件,但在 Composite 模式中一个重要的问题是:在 Composite 类层次结构中哪些类声明这些操作。我们是应该在 Component 中声明这些操作,并使这些操作对 Leaf 类有意义,还是只应该在 Composite 和它的子类中声明并定义这些操作呢 -- 这需要在安全性和透明性之间做出选择。
- 在类层次结构的根部定义子结点管理接口的方法具有良好的透明性,因为你可以一致地使用所有的组件,但是这一方法是以安全性为代价的,因为客户有可能会做一些无意义的事情,例如在 Leaf 中增加和删除对象等。
- 在 Composite 类中定义管理子部件的方法具有良好的安全性,因为在像 C++这样的静态类型语言中,在编译时任何从 Leaf 中增加或删除对象的尝试都将被发现。但是这又损失了透明性,因为 Leaf 和 Composite 具有不同的接口。
- 在这一模式中,相对于安全性,我们比较强调透明性。如果你选择了安全性,有时你可能会丢失类型信息,并且不得不将一个组件转换成一个组合。这样的类型转换必定不是类型安全的。
- 提供透明性的唯一方法是在 Component 中定义缺省的 Add 和 Remove 操作。这又带来了一个新的问题:Component::Add 的实现不可避免地会有失败的可能性。你可以不让 Component::Add 做任何事情,但这就忽略了一个很重要的问题:企图向叶结点中增加一些东西时可能会引入错误。这时 Add 操作会产生垃圾。你可以让 Add 操作删除它的参数,但可能客户并不希望这样。
- 如果该组件不允许有子部件,或者 Remove 的参数不是该组件的子结点,通常最好使用缺省方式(可能是产生一个异常)处理 Add 和 Remove 的失败。
- 另一个办法是对“删除”的含义做一些改变。如果该组件有一个父部件引用,我们可重新定义 Component::Remove,在它的父组件中删除掉这个组件。然而,对应的 Add 操作仍然没有合理的解释。
- Component 是否应该实现一个 Component 列表
- 你可能希望在 Component 类中将子结点集合定义为一个实例变量,而这个 Component 类中也声明了一些操作对子结点进行访问和管理。但是在基类中存放子类指针,对叶结点来说会导致空间浪费,因为叶结点根本没有子结点。只有当该结构中子类数目相对较少时,才值得使用这种方法。
- 子部件排序 许多设计指定了 Composite 的子部件顺序。如果 Composite 表示语法分析树,Composite 子部件的顺序必须反映程序结构,而组合语句就是这样一些 Composite 的实例。如果需要考虑子结点的顺序,必须仔细地设计对子结点的访问和管理接口,以便管理子结点序列,可以参考 Iterator 模式。
- 使用高速缓冲存储改善性能
- 如果你需要对组合进行频繁的遍历或查找,Composite 类可以缓冲存储对它的子结点进行遍历或查找的相关信息。Composite 可以缓冲存储实际结果或者仅仅是一些用于缩短遍历或查询长度的信息。
- 一个组件发生变化时,它的父部件原先缓冲存储的信息也变得无效。在组件知道其父部件时,这种方法最为有效。因此,如果你使用高速缓冲存储,需要定义一个接口来通知组合组件它们所缓冲存储的信息无效。
- 应该由谁删除 Component 在没有垃圾回收机制的语言中,当一个 Composite 被销毁时,通常最好由 Composite 负责删除其子结点。但有一种情况除外,即 Leaf 对象不会改变,因此可以被共享。
- 存储组件最好用哪种数据结构 Composite 可使用多种数据结构存储它们的子结点,包括连接列表、树、数组和 hash 表。数据结构的选择取决于效率。事实上,使用通用数据结构根本没有必要。有时对每个子结点 Composite 都有一个变量与之对应,这就要求 Composite 的每个子类都要实现自己的管理接口。参见 Interpreter(5.3)模式中的例子。
相关模式
- 通常,部件–父部件连接用于 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 类中的操作和行为局部化。
Decorator(装饰)——对象结构型模式
意图
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator 模式相比生成子类更为灵活。
别名
包装器(wrapper)。
动机
有时我们希望给某个对象而不是整个类添加一些功能。例如,一个图形用户界面工具箱允许你对任意一个用户界面组件添加一些特性(例如边框),或是一些行为(例如窗口滚动)。
使用继承机制是添加功能的一种有效途径,从其他类继承过来的边框特性可以被多个子类的实例所使用。但这种方法不够灵活,因为边框的选择是静态的,用户不能控制对组件加边框的方式和时机。
一种较为灵活的方式是将组件嵌入另一个对象中,由这个对象添加边框。我们称这个嵌入的对象为装饰。这个装饰与它所装饰的组件接口一致,因此它对使用该组件的客户透明。它将客户请求转发给该组件,并且可能在转发前后执行一些额外的动作(例如画一个边框)。透明性使得你可以递归地嵌套多个装饰,从而可以添加任意多的功能。
适用性
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 处理那些可以撤销的职责。
- 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是,类定义被隐藏,或类定义不能用于生成子类。
参与者
- Component: 定义一个对象接口,可以给这些对象动态地添加职责。
- ConcreteComponent: 定义一个对象,可以给这个对象添加一些职责。
- Decorator: 维持一个指向 Component 对象的指针,并定义一个与 Component 接口一致的接口。
- ConcreteDecorator: 向组件添加职责。
协作
Decorator 将请求转发给它的 Component 对象,并有可能在转发请求前后执行一些附加的动作。
效果
- 比静态继承更灵活 与对象的静态继承(多重继承)相比,Decorator 模式提供了更加灵活地向对象添加职责的方式。可以用添加和分离的方法,用装饰在运行时增加和删除职责。相比之下,继承机制要求为每个添加的职责创建一个新的子类。这会产生许多新的类,并且会增加系统的复杂度。此外,为一个特定的 Component 类提供多个不同的 Decorator 类,这就使得你可以对一些职责进行混合和匹配。使用 Decorator 模式可以很容易地重复添加一个特性。
- 避免在层次结构高层的类有太多的特征 Decorator 模式提供了一种“即用即付”的方法来添加职责。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可以定义一个简单的类,并且用 Decorator 类给它逐渐地添加功能。可以从简单的部件组合出复杂的功能。这样,应用程序不必为不需要的特征付出代价。同时也更易于不依赖于 Decorator 所扩展(甚至是不可预知的扩展)的类而独立地定义新类型的 Decorator。扩展一个复杂类的时候,很可能会暴露与添加的职责无关的细节。
- Decorator 与它的 Component 不一样 Decorator 是一个透明的包装。如果我们从对象标识的观点出发,一个被装饰了的组件与这个组件是有差别的,因此,使用装饰时不应该依赖对象标识。
- 有许多小对象 采用 Decorator 模式进行系统设计往往会产生许多看上去类似的小对象,这些对象仅仅在它们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。
实现
- 接口的一致性 装饰对象的接口必须与它所装饰的 Component 的接口是一致的,因此,所有的 ConcreteDecorator 类必须有一个公共的父类(至少在 C++中如此)。
- 省略抽象的 Decorator 类 当你仅需要添加一个职责时,没有必要定义抽象 Decorator 类。你常常需要处理现存的类层次结构而不是设计一个新系统,这时你可以把 Decorator 向 Component 转发请求的职责合并到 ConcreteDecorator 中。
- 保持 Component 类的简单性 为了保证接口的一致性,组件和装饰必须有一个公共的 Component 父类。因此保持这个类的简单性是很重要的,即它应集中于定义接口而不是存储数据。对数据表示的定义应延迟到子类中,否则 Component 类会变得过于复杂和庞大,因而难以大量使用。赋予 Component 太多的功能也使得具体的子类有一些它们并不需要的功能的可能性大大增加。
- 改变对象外壳与改变对象内核 我们可以将 Decorator 看作一个对象的外壳,它可以改变这个对象的行为。另外一种方法是改变对象的内核。例如,Strategy(5.9)模式就是一个用于改变内核的很好的模式。 当 Component 类原本就很庞大时,使用 Decorator 模式代价太高,Strategy 模式相对好一些。在 Strategy 模式中,组件将它的一些行为转发给一个独立的策略对象,我们可以替换策略对象,从而改变或扩充组件的功能。
相关模式
- Adapter(4.1):Decorator 模式不同于 Adapter 模式,因为装饰仅改变对象的职责而不改变它的接口;而适配器将给对象一个全新的接口。
- Composite(4.3):可以将装饰视为一个退化的、仅有一个组件的组合。然而,装饰仅给对象添加一些额外的职责——它的目的不在于对象聚集。
- Strategy(5.9):用一个装饰可以改变对象的外表;而 Strategy 模式使得你可以改变对象的内核。这是改变对象的两种途径。
## Facade(外观)——对象结构型模式
意图
为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
动机
将一个系统划分成若干个子系统有利于降低系统的复杂性。一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小。达到该目标的途径之一就是引入一个外观(facade)对象,它为子系统中较一般的设施提供了一个单一而简单的界面。
适用性
- 当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂,大多数模式使用时都会产生更多更小的类。这使得子系统更具可复用性,也更容易对子系统进行定制,但也给那些不需要定制子系统的用户带来一些使用上的困难。Facade 可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过 Facade 层。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 Facade 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
- 当你需要构建一个层次结构的子系统时,使用 Facade 模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,可以让它们仅通过 Facade 进行通信,从而简化了它们之间的依赖。
参与者
- Facade:
- 知道哪些子系统类负责处理请求。
- 将客户的请求代理给适当的子系统对象。
- SubsystemClasses:
- 实现子系统的功能。
- 处理由 Facade 对象指派的任务。
- 没有 Facade 的任何相关信息,即没有指向 Facade 的指针。
协作
- 客户程序通过发送请求给 Facade 的方式与子系统通信,Facade 将这些消息转发给适当的子系统对象。尽管是子系统中的有关对象在做实际工作,但 Facade 模式本身也必须将它的接口转换成子系统的接口。
- 使用 Facade 的客户程序不需要直接访问子系统对象。
效果
- 它对客户屏蔽子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。
- 它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的
- 松耦合关系使得子系统的组件变化不会影响到它的客户。Facade 模式有助于建立层次结构系统,也有助于对对象之间的依赖关系分层。Facade 模式可以消除复杂的循环依赖关系。这一点在客户程序与子系统分别实现的时候尤为重要。
- 在大型软件系统中降低编译依赖性至关重要。在子系统类改变时,希望尽量减少重编译工作以节省时间。用 Facade 可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。Facade 模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。
- 如果应用需要,它并不限制它们使用子系统类。因此你可以在系统易用性和通用性之间加以选择。
实现
- 降低客户–子系统之间的耦合度
- 用抽象类实现 Facade 而它的具体子类对应于不同的子系统实现,这可以进一步降低客户与子系统的耦合度。这样,客户就可以通过抽象的 Facade 类接口与子系统通信。这种抽象耦合关系使得客户不知道它使用的是子系统的哪个实现。
- 除生成子类的方法以外,另一种方法是用不同的子系统对象配置 Facade 对象。为定制 Facade,仅需对它的子系统对象(一个或多个)进行替换。
- 公共子系统类与私有子系统类
- 一个子系统与一个类的相似之处是,它们都有接口并且它们都封装了一些东西——类封装了状态和操作,而子系统封装了一些类。考虑一个类的公共和私有接口是有益的,我们也可以考虑子系统的公共和私有接口。
- 子系统的公共接口包含所有的客户程序可以访问的类,私有接口仅用于对子系统进行扩充。当然,Facade 类是公共接口的一部分,但它不是唯一的部分,子系统的其他部分通常也是公共的。例如,编译子系统中的 Parser 类和 Scanner 类就是公共接口的一部分。
相关模式
- Abstract Factory(3.1)模式可以与 Facade 模式一起使用以提供一个接口,这一接口可用来以一种子系统独立的方式创建子系统对象。Abstract Factory 也可以代替 Facade 模式隐藏那些与平台相关的类。
- Mediator(5.5)模式与 Facade 模式的相似之处是,它抽象了一些已有的类的功能。然而,Mediator 的目的是对同事之间的任意通信进行抽象,通常集中不属于任何单个对象的功能。Mediator 的同事对象知道中介者并与它通信,而不是直接与其他同类对象通信。相对而言,Facade 模式仅对子系统对象的接口进行抽象,从而使它们更容易使用;它并不定义新功能,子系统也不知道 Facade 的存在。
- 通常来讲,仅需要一个 Facade 对象,因此 Facade 对象通常属于 Singleton(3.5)模式。
Flyweight(享元)——对象结构型模式
意图
运用共享技术有效地支持大量细粒度的对象。
动机
有些应用程序得益于在其整个设计过程中采用对象技术,但简单化的实现代价极大。Flyweight 模式描述了如何共享对象,使得可以细粒度地使用它们而不需要高昂的代价。
flyweight 是一个共享对象,它可以同时在多个场景(context)中使用,并且在每个场景中 flyweight 都可以作为一个独立的对象——这一点与非共享对象的实例没有区别。flyweight 不能对它所运行的场景做出任何假设,这里的关键概念是内部状态和外部状态之间的区别。内部状态存储于 flyweight 中,它包含了独立于 flyweight 场景的信息,这些信息使得 flyweight 可以被共享。而外部状态取决于 flyweight 场景,并根据场景而变化,因此不可共享。用户对象负责在必要的时候将外部状态传递给 flyweight。
Flyweight 模式对那些通常由于数量太大而难以用对象来表示的概念或实体进行建模。
适用性
- 一个应用程序使用了大量的对象。
- 完全由于使用大量的对象造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
- 应用程序不依赖于对象标识。由于 Flyweight 对象可以被共享,因此对于概念上明显有别的对象,标识测试将返回真值。
参与者
- Flyweight: 描述一个接口,通过这个接口 flyweight 可以接受并作用于外部状态。
- ConcreteFlyweight: 实现 Flyweight 接口,并为内部状态(如果有的话)增加存储空间。Concrete-Flyweight 对象必须是可共享的。它所存储的状态必须是内部的,即它必须独立于 ConcreteFlyweight 对象的场景。
- UnsharedConcreteFlyweight: 所有的 Flyweight 子类都需要被共享。Flyweight 接口使共享成为可能,但它并不强制共享。在 flyweight 对象结构的某些层次,UnsharedConcreteFlyweight 对象通常将 ConcreteFlyweight 对象作为子结点(Row 和 Column 就是这样)。
- FlyweightFactory:
- 创建并管理 flyweight 对象。
- 确保合理地共享 flyweight。当用户请求一个 flyweight 时,FlyweightFactory 对象提供一个已创建的实例或者创建一个(如果不存在的话)。
- Client:
- 维持一个对 flyweight 的引用。
- 计算或存储一个(多个)flyweight 的外部状态。
协作
flyweight 执行时所需的状态必定是内部的或外部的。内部状态存储于 Concrete-Flyweight 对象之中,而外部对象则由 Client 对象存储或计算。当用户调用 flyweight 对象的操作时,将该状态传递给它。
用户不应直接对 ConcreteFlyweight 类进行实例化,而只能从 FlyweightFactory 对象得到 ConcreteFlyweight 对象,这可以保证对它们适当地进行共享。
效果
使用 Flyweight 模式时,传输、查找和/或计算外部状态都会产生运行时开销,尤其当 flyweight 原先被存储为内部状态时。然而,空间上的节省抵消了这些开销。共享的 flyweight 越多,空间节省也就越大。
存储节约由以下几个因素决定:
- 由于共享带来的实例总数减少的数目
- 对象内部状态的平均数目
- 外部状态是计算的还是存储的
共享的 flyweight 越多,存储节约也就越多。节约量随着共享状态的增多而增大。当对象使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最大。所以,可以用两种方法来节约存储:用共享减少内部状态的消耗,用计算时间换取对外部状态的存储。
Flyweight 模式经常和 Composite(4.3)模式结合起来表示一个层次式结构,这一层次式结构是一个共享叶结点的图。共享的结果是,flyweight 的叶结点不能存储指向父结点的指针。而父结点的指针将传给 flyweight 作为它的外部状态的一部分。这将对该层次结构中对象之间相互通信的方式产生很大的影响。
实现
- 删除外部状态: 该模式的可用性在很大程度上取决于是否容易识别外部状态并将它从共享对象中删除。如果不同种类的外部状态和共享前对象的数目相同的话,删除外部状态不会降低存储消耗。理想的状况是,外部状态可以由一个单独的对象结构计算得到,且该结构的存储要求非常小。
- 管理共享对象:
- 因为对象是共享的,用户不能直接对它进行实例化,所以 Flyweight-Factory 可以帮助用户查找某个特定的 flyweight 对象。FlyweightFactory 对象经常使用关联存储帮助用户查找感兴趣的 flyweight 对象。
- 共享还意味着某种形式的引用计数和垃圾回收,这样当一个 flyweight 不再使用时,可以回收它的存储空间。然而,当 flyweight 的数目固定而且很小的时候(例如,用于 ACSII 码的 flyweight),这两种操作都不必要。在这种情况下,flyweight 完全可以永久保存。
相关模式
- Flyweight 模式通常和 Composite(4.3)模式结合起来,用共享叶结点的有向无环图实现一个逻辑上的层次结构。
- 通常,最好用 flyweight 实现 State(5.8)和 Strategy(5.9)对象。
Proxy(代理)——对象结构型模式
意图
为其他对象提供一种代理以控制对这个对象的访问。
别名
Surrogate。
动机
对一个对象进行访问控制的一个原因是只有在我们确实需要这个对象时才对它进行创建和初始化。
适用性
- **远程代理 (Remote Proxy)**为一个对象在不同的地址空间提供局部代表。
- **虚代理 (Virtual Proxy)**根据需要创建开销很大的对象。
- **保护代理 (Protection Proxy)**控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。
- **智能指引 (Smart Reference)**取代了简单的指针,它在访问对象时执行一些附加操作。
- 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为 Smart Pointer)。
- 当第一次引用一个持久对象时,将它装入内存。
- 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
参与者
- Proxy:
- 保存一个引用使得代理可以访问实体。若 RealSubject 和 Subject 的接口相同,Proxy 会引用 Subject。
- 提供一个与 Subject 的接口相同的接口,这样代理就可以用来替代实体。
- 控制对实体的存取,并可能负责创建和删除它。
- 其他功能依赖于代理的类型:
- Remote Proxy负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求。
- Virtual Proxy可以缓存实体的附加信息,以便延迟对它的访问。
- Protection Proxy检查调用者是否具有实现一个请求所必需的访问权限。
- Subject: 定义 RealSubject 和 Proxy 的共用接口,这样就在任何使用 RealSubject 的地方都可以使用 Proxy。
- RealSubject: 定义 Proxy 所代表的实体。
协作
代理根据其种类,在适当的时候向 RealSubject 转发请求。
效果
Proxy 模式在访问对象时引入了一定程度的间接性。根据代理的类型,附加的间接性有多种用途:
- Remote Proxy可以隐藏一个对象存在于不同地址空间的事实。
- Virtual Proxy可以进行最优化,例如根据要求创建对象。
- Protection Proxies和Smart Reference都允许在访问一个对象时有一些附加的内务处理(housekeeping task)。
Proxy 模式还可以对用户隐藏另一种称为 copy-on-write
的优化方式,该优化与根据需要创建对象有关。拷贝一个庞大而复杂的对象是一种开销很大的操作,如果这个拷贝根本没有被修改,那么这些开销就没有必要。用代理延迟这一拷贝过程,我们可以保证只有当这个对象被修改的时候才对它进行拷贝。
在实现 Copy-on-write 可以大幅降低开销
实现
Proxy 并不总是需要知道实体的类型
- 若 Proxy 类能够完全通过一个抽象接口处理它的实体,则无须为每一个 RealSubject 类都生成一个 Proxy 类,Proxy 可以统一处理所有的 RealSubject 类。但是如果 Proxy 要实例化 RealSubject(例如在虚代理中),那么它们必须知道具体的类。
- 另一个实现方面的问题涉及在实例化实体以前怎样引用它。有些代理必须引用它们的实体,无论它是在硬盘上还是在内存中。这意味着它们必须使用某种独立于地址空间的对象标识符。
相关模式
- Adapter(4.1):适配器为它所适配的对象提供了一个不同的接口。相反,代理提供了与它的实体相同的接口。然而,用于访问保护的代理可能会拒绝执行实体会执行的操作,因此,它的接口实际上可能只是实体接口的一个子集。
- Decorator(4.4):尽管装饰的实现部分与代理相似,但装饰的目的不一样。装饰为对象添加一个或多个功能,而代理则控制对对象的访问。
- 代理的实现与装饰的实现类似,但是在相似的程度上有所差别。Protection Proxy 的实现可能与装饰的实现差不多。另外,Remote Proxy 不包含对实体的直接引用,而只是一个间接引用,如“主机 ID,主机上的局部地址”。Virtual Proxy 开始的时候使用一个间接引用,例如一个文件名,但最终将获取并使用一个直接引用。
讨论
Adapter 与 Bridge
Adapter(4.1)模式和 Bridge(4.2)模式具有一些共同的特征。它们都给另一对象提供了一定程度的间接性,因而有利于系统的灵活性。它们都涉及从自身以外的一个接口向这个对象转发请求。
这两个模式的不同之处主要在于它们各自的用途。Adapter 模式主要是为了解决两个已有接口之间不匹配的问题。它不考虑这些接口是怎样实现的,也不考虑它们各自可能会如何演化。这种方式不需要对两个独立设计的类中的任一个进行重新设计,就能够使它们协同工作。另外,Bridge 模式则对抽象接口与它的(可能是多个)实现部分进行桥接。虽然这一模式允许你修改实现它的类,但是它仍然为用户提供了一个稳定的接口。Bridge 模式也会在系统演化时适应新的实现。
由于这些不同点,Adapter 和 Bridge 模式通常被用于软件生命周期的不同阶段。“当你发现两个不兼容的类必须同时工作时,就有必要使用 Adapter 模式,其目的一般是避免代码重复。此处耦合不可预见。相反,Bridge 的使用者必须事先知道:一个抽象将有多个实现部分,并且抽象和实现两者是独立演化的。Adapter 模式在类已经设计好后实施,而 Bridge 模式在设计类之前实施。这并不意味着 Adapter 模式不如 Bridge 模式,只是因为它们针对了不同的问题。
你可能认为 facade(参见 Facade(4.5))是另外一组对象的适配器。但这种解释忽视了一个事实:facade 定义一个新的接口,而 Adapter 则复用一个原有的接口。记住,适配器使两个已有的接口协同工作,而不是定义一个全新的接口。
Composite、Decorator 与 Proxy
Composite(4.3)模式和 Decorator(4.4)模式具有类似的结构图,这说明它们都基于递归组合来组织可变数目的对象。这一共同点可能会使你认为 decorator 对象是一个退化的 composite,但这一观点没有领会 Decorator 模式的要点。相似点仅止于递归组合,同样,这是因为这两个模式的目的不同。
Decorator 旨在使你不需要生成子类即可给对象添加职责。这就避免了静态实现所有功能组合,从而导致子类急剧增加。Composite 则有不同的目的,它旨在构造类,使多个相关的对象能够以统一的方式处理,而多个对象可以被当作一个对象来处理。它的重点不在于修饰,而在于表示。
尽管它们的目的截然不同,但却具有互补性。因此 Composite 和 Decorator 模式通常协同使用。在使用这两种模式进行设计时,我们无须定义新的类,仅需要将一些对象插接在一起即可构建应用。“这时系统中将会有一个抽象类,它有一些 composite 子类和 decorator 子类,还有一些实现系统的基本构建模块。此时,composite 和 decorator 将拥有共同的接口。从 Decorator 模式的角度看,composite 是一个 ConcreteComponent。而从 Composite 模式的角度看,decorator 则是一个 Leaf。当然,它们不一定要同时使用,正如我们所见,它们的目的有很大的差别。
另一种与 Decorator 模式结构相似的模式是 Proxy(4.7)。这两种模式都描述了怎样为对象提供一定程度上的间接引用,proxy 和 decorator 对象的实现部分都保留了指向另一个对象的指针,它们向这个对象发送请求。同样,它们具有不同的设计目的。
像 Decorator 模式一样,Proxy 模式构成一个对象并为用户提供一致的接口。但与 Decorator 模式不同的是,Proxy 模式不能动态地添加或分离性质,它也不是为递归组合而设计的。它的目的是,当直接访问一个实体“不方便或不符合需求时,为这个实体提供一个替代者,例如,实体在远程设备上使访问受到限制或者实体是持久存储的。
在 Proxy 模式中,实体定义了关键功能,而 Proxy 提供(或拒绝)对它的访问。在 Decorator 模式中,组件仅提供了部分功能,而一个或多个 decorator 负责完成其他功能。Decorator 模式适用于编译时不能(至少不方便)确定对象的全部功能的情况。这种开放性使递归组合成为 Decorator 模式中一个必不可少的部分。而在 Proxy 模式中则不是这样,因为 Proxy 模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态地表达。
模式间的这些差异非常重要,因为它们针对了面向对象设计过程中一些特定的经常发生的问题的解决方法。但这并不意味着这些模式不能结合使用。可以设想有一个 proxy-decorator 用来给 proxy 添加功能,或是一个 decorator-proxy 用来修饰一个远程对象。尽管这种混合可能有用(我们手边还没有现成的例子),但它们可以分割成一些有用的模式。.