0%

创建型模式

创建型设计模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。

主要特征

  • 第一,它们都将关于该系统使用哪些具体的类的信息封装起来。
  • 第二,它们隐藏了这些类的实例是如何被创建和放在一起的。

常用的对象创建型模式有:

背景

游戏中的功能类

以一个地牢探索游戏创建房间为例,讨论 5 种模式。涉及的主要类有:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 房间的方向,北南东西
enum Direction {N,S,E,W}; 

// 创建迷宫的类
class MazeGame{
public:
	Maze* CreateMaze();
}

// 地图点的抽象类,有Room,Wall,Door3个子类
class MapSite{
public:
	virtual void Enter() = 0; // 进入该地点
}
// 墙
class Wall:public MapSite{
public:
	virtual void Enter();
}
// 门
class Door:public MapSite{  
public:
	Door(Room*=0,Room*=0);
	Room* OtherSideFrom(Room*);
	virtual void Enter();
private:
	Room *_room1,*_room2;
	bool _isOpen;
}
// 房间
class Room:public MapSite{
public:
	Room(int roomNo);
	MapSite *GetSide(Direction) const;
	virtual void Enter();
private:
	MapSite *_sides[4]; // 东西南北的地图点
	int _roomNumber;    // 房间编号
}

各类创建方法比较

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 硬编码
Maze* MazeGame::CreateMaze(){
	Maze* aMazenew Maze;
	Room* r1 = new Room(1); 
	Room* r2 new Room(2);
	Door* theDoor = new Door(rl, r2);
	
	aMaze->AddRoom (r1);
	aMaze->AddRoom (r2);
	
	r1->SetSide (North, new Wall); rl>SetSideEast.theDoor);
	rl->SetSide (South, new Wall); rl->SetSide (West, new Wall);
	r2->SetSide (North, new Wal1); r2->SetSide (East, new Wall): 
	r2->SetSide(South, new Wall); r2->SetSide (West, theDoor) 
	return aMaze;
}
  • 硬编码方式的主要缺点是不灵活,修改迷宫的布局意味着修改这个实例方法,这容易产生错误也不易于复用
  • Factory Method:CreateMaze 调用虚函数而不是构造器来创建它需要的房间、墙壁和门,那么你可以创建一个 MazeGame 的子类并重定义这些虚函数,从而改变被实例化的类。
  • Abstract Factory:传递一个对象给 CreateMaze 作为参数来创建房间、墙壁和门,那么你可以传递不同的参数来改变房间、墙壁和门的类
  • Builder:传递一个对象给 CreateMaze,这个对象可以在它所建造的迷宫中使用增加房间、墙壁和门的操作来全面创建一个新的迷宫,那么你可以使用继承来改变迷宫的一些部分或迷宫的建造方式。
  • Prototype:CreateMaze 由多种原型的房间、墙壁和门对象参数化,它复制并将这些对象增加到迷宫中,那么你可以用不同的对象替换这些原型对象以改变迷宫的构成。
  • Singleton:可以保证每个游戏中仅有一个迷宫而且所有的游戏对象都可以迅速访问它——不需要求助于全局变 量或函数。Singleton 也使得迷宫易于扩展或替换,且不需要变动已有的代码。

讨论

对系统进行参数化主要有 2 种方法:

又称 Kit

Abstract Factory 抽象工厂

用途

用途:提供一个接口以创建一系列相关或相互依赖的对象,而无须指定它们具体的类。

示例

示例: GUI 程序需要支持 Motif 和 PW 这 2 种视窗标准:

  • 抽象工厂提供 CreateXXX 接口生产抽象组件 XXX,用户调用该函数获取 XXX 组件
  • 抽象工厂的具象工厂子类实现 CreateXXX 接口,生产自己这种类型的具象组件 YYXXX(YYWindow,YYScrollBar,…)
  • 客户仅与抽象类定义的接口交互,而不使用特定的具体类的接口

结构

  • AbstractFactory:声明一个创建抽象产品对象的接口
  • ConcreteFactory:实现创建具体产品对象的操作
  • AbstractProduct:为一类产品对象声明一个接口
  • ConcreteProduct:定义具体的产品对象,实现 AbstractProduct 接口
  • Client:只使用 Abstract 的接口

协作过程:

  • 通常在运行时创建一个 ConcreteFactroy 类的实例。这一具体的工厂创建具有特定实现的产品对象。为创建不同的产品对象,客户应使用不同的具体工厂。
    • 说明:简单实现时,AbstractFactory 和 ConcreteFactory 可以合二为一
  • AbstractFactory 将产品对象的创建延迟到它的 ConcreteFactory 子类。

适用条件

  • 系统独立于产品的创建、组合和表示
  • 系统包含多个产品系列,用其中的一种进行表示
  • 强调一个系列的产品设计,以便联合使用
  • 提供产品类库,但是只显示接口而隐藏实现

优缺点

  • 优点:
    • 分离实现:工厂封装了创建产品对象的责任和过程,将客户和具体实现分离。客户只操作抽象接口,避免硬编码具体的类名
    • 易于切换产品系列
    • 有利于产品风格的一致性:应用一次只能使用同一个系列中的对象,这有利于维护一致性
  • 缺点:
    • 难以支持新种类的产品:AbstractFactory 定义了可被其创建的产品集合,创建新产品意味着需要扩展 AbstractFactory 和其所有子类的接口。

实现的注意事项

  • 工厂使用单例模式:一个应用通常每个产品系列只需要一个 ConcreteFactory 实例
  • 创建产品的方法
    • 常用方法:为每个产品定义一个工厂方法,具体的工厂为每个工厂重载该工厂方法以指定产品
    • 简化具体工厂类:可以使用 Prototype 模式实现具体工厂。具体工厂使用产品系列中每一个产品的原型实例来初始化,且它通过复制它的原型来创建新的产品。基于原型的方法使得并非每个新的产品系列都需要一个新的具体工厂类。
  • 可扩展的工厂
    • 通常增加一种新的产品要求改变 AbstractFactory 的接口以及所有与它相关的类。
    • 一个更灵活但不太安全的设计是给创建对象的操作增加一个参数(例如类标识符,字符串,整数索引等)该参数指定了将被创建的对象的种类。使用这种方法 AbstractFactory 只需要一个“Make”操作和一个指示要创建对象的种类的参数。
    • 客户需要使用执行向下类型转换(downcast,例如 cpp 中的 dynamic_cast)才能获得具体的子类型,而这会带来安全隐患

Builder

用途

将复杂对象的构建和表示分离,使得同样的构建过程可以创建不同的表示。

示例

考虑应用场景:将 RTF 文档转换为其他多种文本格式,对于任意结构的文档,可能的转换数目是无限的

  • RTFReader:分析文档,每次遇到一个标记时发送请求调用 TextConverter 转换该标记。

  • TextConverter 对象负责进行数据转换以及用特定格式表示该标记,其子类对不同转换和不同格式进行特殊处理。

  • 解释:Builder 模式描述了所有这些关系。

    • 每一个转换器类在该模式中被称为生成器(builder),而阅读器则称为导向器(director)
    • Builder 模式将分析文本格式的算法(即 RTF 文档的语法分析程序)与描述怎样创建和表示一个转换后格式的算法分离开来。这使我们可以复用 RTFReader 的语法分析算法,根据 RTF 文档创建不同的文本表示——仅需使用不同的 TextConverter 的子类配置该 RTFReader 即可。

适用情形

  • 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
  • 当构造过程必须允许被构造的对象有不同的表示时。

结构

  • Builder:创建 Product 的抽象接口
  • ConcreteBuilder:(ASCIIConverter、TeXConverter、 TextWidgetConverter)
    • 构造和装配产品的各个部件
    • 定义和跟踪自己创建的表示
    • 提供检索产品的接口(例如,GetASCIIText 和 GetTextWidget)
  • Director:(RTFReader)构造使用 builder 接口的对象
  • Product(ASCIIText、TeXText、TextWidget)
    • 被构建的复杂对象,具体生成器创建该产品的内部表示并定义装配过程
    • 包含定义组件部件的类,以及将这些部件装配成最终产品的接口

协作过程如下

  • 客户创建 Director 对象,并用它所想要的 Builder 对象进行配置。
  • 一旦生成了产品部件,导向器就会通知生成器。
  • 生成器处理导向器的请求,并将部件添加到该产品中。
  • 客户从生成器中获取最终产品。

优缺点

  • 可以改变一个产品的内部表示:因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定义一个新的生成器。
  • 将构造代码和表示代码分开:Builder 模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。构造代码只写一次,不同的 Director 可以反复复用构造代码在同一套部件集合上创建不同的表示
  • 可以精细控制构造过程:它是在导向器的控制下一步一步构造产品的。仅当该产品完成时导向器才从生成器中取回它。

实现

通常有一个抽象的 Builder 类为导向器可能要求创建的每一个构件定义一个操作(默认什么都不做)。 一个 ConcreteBuilder 类对它有兴趣创建的构件重定义这些操作。

又称虚构造器(virtual constructor)

用途

定义一个用于创建对象的接口,让子类决定实例化哪一个类。 Factory Method 使一个类的实例化延迟到其子类。

示例

框架用抽象类定义和维护对象之间的关系。 Eg:框架定义了 App 抽象类,它包含一个 Document 抽象内容。但是 App 类无法确认应当实例化哪个 Document 的具象子类:

  • 方法:
    • 抽象类 App 中提供接口 CreateDocument。
    • 具象 App 子类重载接口 CreateDocument,并创建自己要用到具象的 Document

结构

  • Product:定义工厂方法要创建的对象的类别
  • ConcreteProduct:
  • Creator:抽象工厂,定义用于返回 Product 的工厂方法(接口),该接口可以是抽象的,也可以提供一个默认实现返回缺省的 ConcreteProduct
  • ConcreteCreator:具象工厂,重载工厂接口

说明:

  • Creator 依赖子类重载工厂方法以返回合适的 ConcreteProduct

适用性

  • 一个类不确定自己要创建的对象的类
  • 一个类希望由子类指定自己创建的对象的类

优缺点

工厂方法的潜在缺点:客户可能只是为了创建一个特定的 ConcreteProduct 对象,就必须创建 Creator 的新的子类

优点:

  • 工厂方法不再将应用相关的类绑定到程序代码中。代码只处理 Product 接口
  • 可以为子类提供钩子 Hook(提供合理的缺省实现)
  • 可以连接平行的类层次:例如图形类和用户的操作是平行的类层次,而抽象父类 Figure 可以提供 CreateManipulator 方法连接 Manipulate 类层次,并为子类提供默认实现

实现

  • Creator 可以是抽象类,也可以是提供了缺省实现的具象类
    • 抽象类不提供默认实现,可以避免不得不实例化不可预见类的问题
    • 具象类:这种情况比直接 new 个实例具有更好的灵活性
  • 参数化工厂方法:这允许工厂方法创建多种类型的对象。工厂方法可以采用一个标识(指定要被创建的对象种类的参数)区分不同类型的对象(这些对象都必须是 Product 的子类型)
    • 重定义一个参数化的工厂方法使你可以简单而有选择性地扩展或改变一个 Creator 生产的产品。注意最后需要调用父类的 Create
  • 命名约定:使用命名约定是一个好习惯,它可以清楚地说明你正在使用工厂方法 600

Cpp 中的工厂方法

用途

用原型指定创建对象的种类,拷贝这些原型创建新的对象

示例

例如采用工具类操作图形元素:

  • 若为每个图形元素创建相应工具类,则过于冗杂
  • 采用原型方法:
    • 工具类拷贝一个图形元素的实例,添加到文档中

适用情形

  • 当一个系统应该独立于它的产品创建、构成和表示时。
  • 当要实例化的类是在运行时指定时,例如,通过动态装载。
  • 为了避免创建一个与产品类层次平行的工厂类层次时。
  • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

结构

  • Prototype:提供 clone 自身的接口
    • ConcretePrototype:实现 clone 的操作
  • Client:用原型 clone 自身从而创建新的对象

优缺点

Prototype 有许多与 Abstract Factory(3.1)和 Builder(3.2)一样的效果:

  • 它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目。
  • 此外,这些模式使客户无须改变自身代码即可使用与特定应用相关的类。

其他优点

  • 运行时增加和删除产品
  • 改变值以指定新对象:高度动态的系统允许你通过对象组合定义新的行为——例如,通过为一个对象变量指定值——并且不定义新的类。 克隆一个原型类似于实例化一个类。Prototype 模式可以极大地减少系统所需要的类的数目。
  • 改变结构以指定新对象许多应用由部件和子部件来创建对象。
    • 例如电路设计编辑器就是由子电路来构造电路的:这样的应用通常允许你实例化复杂的、用户定义的结构,比方说,一次又一次地重复使用一个特定的子电路。
    • Prototype 模式只要组合电路对象将 Clone 实现为一个深拷贝(deep copy),具有不同结构的电路就可以是原型了。
  • 减少子类的构造:Factory Method(3.3)经常产生一个与产品类层次平行的 Creator 类层次。Prototype 模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象,因此你根本不需要 Creator 类层次。
    • 主要适用于像 C++这样不将类作为一级类对象的语言(通常是静态语言)。这种语言中,类本身不是一种对象,而是一种特殊的数据类型或结构。这意味着,这种语言中,类不能被当作参数传递给函数,也不能被当作返回值返回,也不能被动态创建或销毁。例如,C++中的类只能在编译时静态定义,而不能在运行时动态生成或修改。
    • 相反,将类作为一级类对象的语言(通常是动态语言),则认为类本身也是一种对象,只不过是用来创建其他对象的对象。这意味着,这种语言中,类可以被当作参数传递给函数,也可以被当作返回值返回,也可以被动态创建或销毁。例如,Python 就是一种将类作为一级类对象的语言,因为 Python 中的类是由 type 这个元类创建的对象,而且可以在运行时动态生成或修改2。
  • 用类动态配置应用一些运行时环境允许你动态地将类装载到应用中。
    • 一个希望创建动态载入类的实例的应用不能静态引用类的构造器,而应该由运行环境在载入时自动创建每个类的实例,并用原型管理器来注册这个实例。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。

缺陷

  • 每个 Prototype 子类必须实现 clone 操作,而这可能非常困难

实现

在像 C++这样的静态语言中,类不是对象,并且运行时只能得到很少或者得不到任何类型信息,所以 Prototype 特别有用。 相反,在类似 Python 这样的语言中意义不大,语言自带等价于原型的东西(类对象、元类等)

用途

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

示例

让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法。

适用性

  • 唯一实例应该是通过子类化可扩展的,并且客户应该无须更改代码就能使用一个扩展的实例时。
  • 类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。

结构

  • Singleton:定义一个 Instance 操作,允许客户访问它的唯一实例。Instance 是 一个类操作(即 Java 中的一个类方法和 C++中的一个静态成员函数)。

客户只能通过 Singleton 的 Instance 操作访问一个 Singleton 的实例。

优缺点

  • 对唯一实例的受控访问:因为 Singleton 类封装它的唯一实例,所以它可以严格地控制客户怎样以及何时访问它。
  • 缩小名字空间:Singleton 模式是对全局变量的一种改进,它避免了那些存储唯一实例的全局变量污染名字空间。
  • 允许可变数目的实例:可以用相同的方法来控制应用所使用的实例的数目。
  • 比类操作更灵活:C++等语言不允许一个类有多个实例。此外,C++中的静态成员函数不是虚函数,因此子类不能多态地重定义它们。

实现

类型定义 方法实现:在 Instance 方法中懒初始化

  • 注意构造器是 protected 的。试图直接实例化 Singleton 的客户将得到一个编译时的错误信息。这就保证了仅有一个实例可以被创建。

  • 关于全局变量法实现单例的问题:在 C++中将单件定义为一个全局或静态的对象,然后依赖于自动初始化,这是不够的(应当使用静态成员函数)。这是因为:

    • 不能保证静态对象只有一个实例会被声明
    • 可能没有足够的信息在静态初始化时实例化每一个单件(例如初始化需要依赖后续计算的值)
    • C++没有定义转换单元(translation unit)上全局对象的构造器的调用顺序
  • 使用一个 Singleton 创建不同类型的单件

    • 由 Singleton 派生子类,不同子类用于创建不同单件
    • 对于 C++:在不同的文件中创建不同的 Singleton 的实现,然后链接时选择具体的实现。
    • 使用单件注册表:可能的 Singleton 类的集合不是由 Instance 定义的。Singleton 类可以根据名字在一个众所周知的注册表(存储实例名→单件实例的映射关系)中注册它们的单件实例;收到请求时查询相应的单件(如果存在的话)并返回它。
      • Singleton 类不再负责创建单件。它的主要职责是使得供选择的单件对象在系统中可以被访问
      • 所有可能的 Singleton 子类的实例都必须被创建,否则它们不会被注册

如何实现单件注册表:

结构型模式涉及如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。

  • Adapter(4.1)模式:适配器使得一个接口(adaptee 的接口)与其他接口兼容,从而给出多个不同接口的统一抽象。
  • Composite(4.3)模式是结构型对象模式的一个实例。它描述了如何构造一个类层次式结构,这一结构由两种类型的对象(基元对象和组合对象)所对应的类构成。
    • 组合对象可以组合基元对象和组合对象
  • Flyweight(4.6)模式为了共享对象定义了一个结构。
    • 为何要求对象共享:效率和一致性。使用对象共享而不是进行对象复制,可以节省大量的空间资源。
    • Flyweight 的对象共享机制主要强调对象的空间效率
  • Proxy(4.7)模式中,proxy 对象作为其他对象的一个方便的替代或占位符。
    • 它可以在局部空间中代表一个远程地址空间中的对象
    • 可以表示一个要求被加载的较大的对象

0.1.1 Adapter、Facade 与 Bridge

Adapter 与 Bridge共同点:

  • 都给另一对象提供了一定程度的间接性,因而有利于系统的灵活性
  • 都涉及从自身以外的一个接口向这个对象转发请求。

Adapter 与 Bridge不同点:

  • 用途:
    • Adapter 模式主要是为了解决两个已有接口之间不匹配的问题。它不考虑这些接口是怎样实现的,也不考虑它们各自可能会如何演化
    • Bridge 模式则对抽象接口与它的(可能是多个)实现部分进行桥接。虽然这一模式允许你修改实现它的类,但是它仍然为用户提供了一个稳定的接口。
  • 阶段:
    • Adapter 模式在类已经设计好后实施,其目的一般是避免代码重复。此处耦合不可预见。
    • Bridge 模式在设计类之前实施:一个抽象将有多个实现部分,并且抽象和实现两者是独立演化的

facade 定义一个新的接口,而 Adapter 则复用一个原有的接口(适配器使两个已有的接口协同工作而非定义新的接口)

0.1.2 Composite、Decorator

Composite(4.3)模式和 Decorator(4.4)

  • 相同点:仅限于都是递归组合
  • 不同点:
    • Decorator 用于添加功能而不产生新的子类
    • Composite 用于构造类

又称包装器(wrapper)

用途

将一个类的接口转换成客户希望的另外一个接口。

  • Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  • Adapter 经常还要负责提供那些被匹配的类所没有提供的功能

示例

已有类型 A 的接口和实现,考虑使之适配类型 B 的接口:

  • 类版本:继承 B 的接口和 A 的实现
  • 对象版本:类型 B 中存储一个类型 A 的实例,调用其接口实现类型 B 的自身接口

例如:有一个 TreeDisplay 窗口组件用于展示目录树,现在要展示继承层次树

结构

类适配器:Adapter 同时继承 Target 和 Adaptee 对象适配器

参与者

  • Target(Shape):定义 Client 使用的与特定领域相关的接口。
  • Client(DrawingEditor):与符合 Target 接口的对象协同。
  • Adaptee(TextView):定义一个已经存在的接口,这个接口需要适配。
  • Adapter(TextShape):对 Adaptee 的接口与 Target 接口进行适配。

Client 调用 Adapter 的接口,然后 Adapter 调用 Adaptee 接口实现请求

适用情形

  • 你想使用一个已经存在的类,而它的接口不符合你的需求。
  • 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
  • (仅适用于对象 Adapter)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。

优缺点

类适配器和对象适配器互有优缺点:

  • 类适配器
    • 优点:Adapter 可以重定义 Adaptee 的部分行为,因为 Adapter 是 Adaptee 的一个子类。
    • 缺点:无法兼容该 Adaptee 的子类型
  • 对象适配器
    • 可以兼容 Adaptee 的子类型
    • 重定义 Adaptee 的行为较为困难(需要构建 Adaptee 的新子类,并在 Adapter 中引用该类型)

实现

使用 C++实现适配器类

在使用 C++实现适配器类时,Adapter 类应该采用公共方式继承 Target 类,并且用私有方式继承 Adaptee 类。因此,Adapter 类应该是 Target 的子类型,但不是 Adaptee 的子类型。

又称 Handle/Body

0.1.1 用途

将抽象部分与它的实现部分分离,使它们可以独立地变化。 继承 VS Bridge:

0.1.2 示例

场景:一个 GUI 工具箱中的可移植窗口需要支持 XWindows 和 PM 系统 实现方法:

  • 继承:定义 Window 抽象类和它的两个子类 XWindow 与 PMWindow,由它们分别实现不同系统平台上的 Window 界面。
    • 缺陷:
      • 扩展 Window 抽象使之适用于不同种类的窗口或新的系统平台很不方便(需要为 Windows 的每个子类针对每个平台分别创建一个类)
      • 继承机制使得客户代码与平台相关。
  • Bridge 模式:将接口抽象和实现放在彼此独立的类层次结构中:
    • 对 Window 子类的所有操作都是用 WindowImp 接口中的抽象操作实现的
    • 将 Window 与 WindowImp 之间的关系称为桥接,它在抽象类与它的实现之间起到了桥梁作用,使它们可以独立地变化。

0.1.3 适用性

  • 不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如,这种情况可能是因为,在程序运行时实现部分应可以被选择或者切换。
  • 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时 Bridge 模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充
  • 对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译。
  • (C++)你想对客户完全隐藏抽象的实现部分。在 C++中,类的表示在类接口中是可见的。
  • 若采用继承模式会有许多类要生成。这样一种类层次结构说明你必须将一个对象分解成两个部分。
    • Rumbaugh 称这种类层次结构为“嵌套的泛化”(nested generalization) [RBP+91]。
  • 你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。

0.1.4 结构

  • Abstraction:抽象接口(例如示例中的 Window),维护一个指向实现(Implementor)的指针
  • RefinedAbstraction:精化的接口(例如示例中的 IconWindow)
  • Implementor:实现类的接口(示例中的 WindowImp)
    • 该接口无需和 Abstraction 的接口一致。一般来讲:
      • Implementor 接口仅提供基本操作
      • Abstraction 则定义了基于这些基本操作的较高层次的操作
  • ConcreteImplementor:实现 Implementor 的具体接口(示例中的 XWindowImp、PMWindowImp)

Abstraction 将 client 的请求转发给它的 Implementor 对象

目的

  • 将对象组合成树形结构以表示“部分–整体”的层次结构。
  • Composite 使得用户对单个对象和组合对象的使用具有一致性

示例

例如 GUI 框架允许用户可以组合多个简单组件以递归形成一些较大的组件。这有 2 种实现方式:

  • 使用图元类和容器类:使用这些类的代码必须区别对待图元对象与容器对象,而实际上大多数情况下用户认为它们是一样的。
  • 使用 Composite 模式递归构建

Composite 模式的关键是一个抽象类,它既可以代表图元,又可以代表图元的容器。其内部提供图元和图元容器所需的所有接口

  • 它的图元子类(树的叶结点,Line,Text 等)无需实现容器相关的操作
  • 聚合类 Picture 定义了一个 Graphic 的集合,它的 Draw 递归调用子部件的 Draw 操作。由于 Picture 接口与 Graphic 接口是一致的,因此 Picture 对象可以递归地组合其他 Picture 对象。

适用性

  • 你想表示对象的部分–整体层次结构。
  • 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

结构

组合结构如下: 继承结构如下:

参与者

  • Component:
    • 提供 Leaf 所需的操作,提供用于访问管理 Component 子组件所需的操作
    • 通常需要,实现所有类共有接口的默认行为
    • 如果需要,在 Component 类中提供访问父组件的接口,Leaf 和 Composite 按需实现这个接口。
      • 父部件引用可以简化结构的上移和组件的删除
  • Composite(Picture)
    • 定义有子部件的那些部件的行为。
    • 存储子部件。
    • 在 Component 接口中实现与子部件有关的操作。
  • Leaf:定义图元对象的行为
  • Client:通过 Component 接口操纵组合部件的对象

用户使用 Component 类接口与组合结构中的对象进行交互。