初探组合模式
代码链接
koonchen/design-patterns/composite
要点
将对象组合成树形结构以表示 部分—整体 的层次结构。 Composite 使得用户对单个对象和组合对象的使用具有一致性。
我们假设存在一个图形编辑器,用户可以组合多个简单组件以形成一个较大的组件,这些组件又可以组合成更大的组件。一个简单的想法是使用 Text
和 Line
这样的图元来定义一些类,与此同时,定义另一些类作为图元的容器类。
然而,这样做的后果是我们将区别对待图元对象和容器对象,而实际上大多数情况下用户认为他们是一样的。区别对待将使得程序更加复杂。 Composite 模式描述了如何使用递归组合,使得用户不必对这些类进行区别对待,如下图:
Composite 模式的关键是一个抽象类,它既可以代表图元也可以代表图元的容器。在图形系统中这个类就是 Graphic
,它声明了一些与特定图形对象相关的操作。在其子类如 Line
、 Rectangle
、 Text
中定义图元,并实现一些图元操作。而子类如 Picture
定义了图元容器,实现了图元容器的方法。而因为图元和容器使用了相同的父类 ( 接口 ) ,因此容器对象可以递归组合其他 Graphic
子类。其结构如下图:
什么情况下适用这种模式:
- 想表示对象的 部分—整体 层次结构。
- 希望用户忽略组合对象与单个对象的不同,用户将统一地适用组合结构中的所有对象。
他的参与者如下:
- Component ( Graphic ) — 为组合中的对象声明的接口。
- Leaf ( Rectangle 等 ) — 定义图元对象。
- Composite ( Picture 等 ) — 定义容器 ( 部件 ) 对象。
- Client — 通过 Composite 接口操纵组合容器的对象。
现在 composite 结构就很明显了:
composite 模式具有以下特点:
- 定义了包含基本对象和组合对象的类层次结构:基本对象可以被组合成更复杂的组合对象,而这个组合对象可以被二次组合,这样不断递归下去。
- 简化客户代码:客户可以一致地使用组合结构和单个对象。客户也无需知道使用的子节点还是组合组件。
- 使得更容易增加新类型的组件:新定义的
Composite
或Leaf
自动地与已有的结构和客户代码一起工作,无需对原有组件进行更改。- 使得设计变得更加一般化:容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。比如你希望只是用某些特定的组件来组合一个
Composite
,但是不能通过类型系统来进行约束,而是在运行时进行检查。
实现
在实现 composite 模式时,有以下几点需要注意:
- 显式的父部件引用:保持从子部件到父部件的引用能简化组合结构的遍历和管理。
- 共享组件:减少对存储的需求。
- 最大化
Component
接口: composite 模式的目的之一是使用户不知道他们使用的是Leaf
还是Composite
。为了达到这个目的,公共接口应该尽量多定义操作,通常实现缺省。- 声明管理子部件的操作:虽然
Composite
类实现了部分操作,但现在我们要作抉择:应该在Component
中声明这些操作,并使这些操作对Leaf
有意义呢?还是只在Composite
和它的子类中声明并定义这些操作?这需要在安全性和透明性之间做出抉择:
- 在类层次结构的根部定义子节点管理结构的方法,具有良好的透明性,因为你一致地使用所有的组件,但是这一方法是以安全性为代价的,因为客户有可能会做一些无意义的事情,例如在
Leaf
中增加和删除对象等。- 在
Composite
类中定义管理子部件的方法具有良好的安全性,因为在 C++ 这样的静态语言中,在编译时任何任何从Leaf
中增加或删除对象的尝试都将被发现。但是这损失了透明性,因为现在Leaf
和Composite
相当于使用了不同的接口。推荐的是选择透明性,即选择第一种方式。因为第二种方式可能会丢失类型信息,以至于我们不得不将一个组件转换成一个组件,这样的类型转换必定不是类型安全的。
java 实现
1 | import java.util.ArrayList; |
java 的实现比较统一,实现如上。
python 实现
1 | class Component(object): |
其他实现也大同小异。
从其他模式出发:
- 部件—父部件 的连接可用于 responsibility of chain 模式。
- decorator 模式经常和 composite 模式一起使用。
- flyweight 模式可以共享组件,但不再能引用他们的父组件。
- iterator 可用于遍历 composite 。
- visitor 将本来应该分布在
Composite
和Leaf
类中的操作和行为局部化。