本文已参加【新人创作礼】活动,一起开启掘金创作之路。
概念
组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“部分—整体”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,又可以称为“部分—整体”(Part-Whole)模式,它是一种对象结构型模式。
角色
- Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。
- Leaf(叶子构件):它在组合模式结构中表示叶子节点对象。叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过捕获异常等方式进行处理。
- Composite(容器构件):它在组合模式结构中表示容器节点对象。容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点。它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
实例代码
以文件系统为例,文件简单分为2类,一个是文件夹Folder,一个具体文件LeafFile(例如文本文件、可执行文件等)。其中文件夹又可以包含若干个具体文件。抽象一个文件父类File,让文件夹和具体文件继承抽象父类。对于File中包含的增加、删除和获取子文件只对Folde有效,LeafFile需要通过捕获异常等方式进行处理这几个方法。
// 文件抽象类
abstract class File {
public abstract void add(File f); // 增加成员
public abstract void remove(File f); // 删除成员
public abstract File getChild(int i); // 获取成员
public abstract void operation(); // 业务方法
}
// 文件具体子类:文件夹类
class Folder extends File {
private ArrayList<File> list = new ArrayList<>();
public void add(File f) {
list.add(f);
}
public void remove(File f) {
list.remove(f);
}
public File getChild(int i) {
return (File) list.get(i);
}
public void operation() {
// 具体业务
for (File f: list) {
f.operation();
}
}
}
// 具体文件
class LeafFile extends File {
public void add(File f) {
// 异常处理或错误提示
}
public void remove(File f) {
// 异常处理或错误提示
}
public File getChild(int i) {
// 异常处理或错误提示
return null;
}
public void operation() {
// 具体文件业务方法实现
}
}
透明组合模式与安全组合模式
透明组合模式
概念文件系统的案例就是透明组合模式,抽象文件File中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。
上面案例中,具体文件LeafFile只有一种,如果扩展开来可以细分为文本文件、图片文件等,这样每个具体文件都需要去处理add()、remove()以及getChild()方法的异常。针对这种情况,可以直接在抽象类File中统一处理add()、remove()以及getChild()等访问及管理子构件的方法异常。
abstract class File {
public void add(File f) {
// 异常处理或错误提示
}
public void remove(File f) {
// 异常处理或错误提示
}
public File getChild(int i) {
// 异常处理或错误提示
return null;
}
public abstract void operation(); // 业务方法
}
对于文件夹Folder子类再覆盖其中add()、remove()以及getChild()方法即可。 透明组合模式总会存在一个问题:具体叶子类中add()、remove()以及getChild()方法是没有意义的。
安全组合模式
将抽象父类中的访问及管理子构件的方法放到对应的子类中去。
abstract class File {
public abstract void operation(); // 业务方法
}
// 文件具体子类:文件夹类
class Folder extends File {
private ArrayList<File> list = new ArrayList<>();
public void add(File f) {
list.add(f);
}
public void remove(File f) {
list.remove(f);
}
public File getChild(int i) {
return (File) list.get(i);
}
public void operation() {
// 具体业务
for (File f: list) {
f.operation();
}
}
}
这样做也存在一个问题:在客户端必须使用Folder类生命构建对象,否者无法调用add()、remove()等方法。
问题记录
- 在组合模式结构图中,如果聚合关联关系不是从Composite到Component的,而是从Composite到Leaf的,会产生怎样的结果? 如果聚合关联关系是从Composite到Leaf,对应文件的案例,就意味着文件夹下只能是具体文件,不能嵌套文件夹。
总结
优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次。它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合开闭原则
缺点
- 在增加新构件时很难对容器中的构件类型进行限制。有时希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件。使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自相同的抽象层。在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。
参考资料
[1] 刘伟. 设计模式的艺术[M]. 清华大学出版社, 2020.