组合模式
组合模式(Composite Pattern)属于结构设计模式,将对象组合成树形结构以表示部分-整体的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。从而使客户端代码能够以统一的方式处理它们,而无需关心处理的是单个对象还是整个组合结构。
模式的意图
组合模式旨在:
- 让客户端可以忽略单个对象和组合对象之间的差异,以统一的方式对待它们。
- 定义包含基本对象和组合对象的类层次结构,使得客户端可以使用一致的接口来操作它们。
- 提供一种方法来构建复杂的对象结构,同时允许用户以递归方式遍历这些结构。
类图
主要角色
- Component(组件):定义了所有组件共有的操作接口,可以是抽象类或接口。它声明了一个接口用于访问和管理Component子部件。
- Leaf(叶子):代表树结构中的叶子节点对象,没有子节点。叶子实现了Component接口中的具体操作。
- Composite(组合体):代表可以包含其他组件的组件(即树枝节点)。Composite实现了Component接口,并定义了将子组件添加到组合中、从组合中移除子组件以及遍历组合的方法。
适用场景
- 当你想表示对象的部分-整体层次结构时,可以考虑使用组合模式,特别是当这个结构需要被客户端以统一的方式处理时。
- 当你希望用户能以一致的方式处理单个对象和组合对象时。
- 在构建一个允许动态构建对象结构(比如图形界面中的菜单项可以是单个菜单项,也可以是包含多个菜单项的菜单)的应用程序时。
优点
- 灵活性高:可以以递归方式构建复杂的对象结构,同时保持接口的一致性。
- 易于增加新的类型:可以容易地在组合结构中添加新的组件类型,而不会影响现有代码。
- 简化客户端代码:客户端不需要知道它处理的是单个对象还是组合对象,简化了客户端代码。
缺点
- 设计复杂度增加:虽然模式提供了灵活性,但同时也增加了系统的理解和设计复杂度。
- 性能问题:深度递归遍历组合结构可能会导致性能下降。
示例
以文件系统为例,一个文件系统可以包含文件(叶子)和目录(组合体),每个目录又可以包含文件和子目录。在这个场景中,Component
是基类,定义了所有文件系统对象的共同操作(如显示名称);File
是叶子类,实现了具体的显示操作;Directory
是组合类,除了实现自己的显示操作外,还维护了一个子组件列表,并提供了添加、删除子组件的方法。
java
//安全方式
//组件(Component)
//接口/抽象类
interface Component {
void display(int depth); // 显示组件信息,depth表示层级
}
// 叶子(Leaf)类 - 文件(File)
class File implements Component {
private String name;
public File(String name) {
this.name = name;
}
@Override
public void display(int depth) {
System.out.println(StringUtils.repeat(" ", depth * 2) + "- " + name);
}
}
// 组合(Composite)类 - 目录(Directory)
class Directory implements Component {
private String name;
private List<Component> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void add(Component component) {
children.add(component);
}
public void remove(Component component) {
children.remove(component);
}
@Override
public void display(int depth) {
System.out.println(StringUtils.repeat(" ", depth * 2) + "+ " + name);
for (Component child : children) {
child.display(depth + 1);
}
}
}
public class CompositePatternDemo {
public static void main(String[] args) {
Directory rootDir = new Directory("根目录");
Directory subDir1 = new Directory("子目录1");
Directory subDir2 = new Directory("子目录2");
File file1 = new File("文件1.txt");
File file2 = new File("文件2.txt");
rootDir.add(subDir1);
rootDir.add(subDir2);
subDir1.add(file1);
subDir2.add(file2);
rootDir.display(0);
}
}
其他
了解(透明和安全)
- 透明方式追求最大程度的接口一致性,使得客户端可以不区分操作的是单个组件还是组合体。这意味着组合模式中的所有组件(包括叶子和容器)都实现了相同的接口。在透明方式下,即使叶子对象没有子组件,也会实现添加、删除子组件的接口,通常这些操作在叶子对象中会被实现为空操作或抛出异常(表明该操作在此上下文中无效)
- 安全方式通过区分容器组件和叶子组件的接口来提高代码的清晰度和安全性。在安全方式下,只在组合体(容器)中定义添加、删除子组件的方法,叶子组件则不包含这些方法。这样,客户端在调用这些操作前,需要先检查对象是否为组合体,避免了对叶子对象进行无效操作。
- 在实际应用中,选择透明方式还是安全方式取决于具体需求,如果追求接口的一致性和更高的灵活性,可能会倾向于透明方式;如果更重视代码的清晰度和安全性,那么安全方式可能更为合适。