组合模式
组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构以表示“部分-整体”的层次结构。通过让客户端以统一的方式处理单个对象和组合对象,实现了对树形结构中所有节点的透明化操作。
特点 (Characteristics)
- 统一接口:客户端可以一致地使用简单元素(叶子)和复杂元素(容器),无需关心处理的是单个对象还是对象的组合。
- 形成树形结构:能够清晰地表示出对象之间的层级关系,如文件系统、组织架构、菜单系统等。
- 递归组合:容器对象可以包含其他容器对象或叶子对象,形成递归的树状结构。
- 易于扩展:可以方便地在树中增加新的组件(叶子或容器),而不需要修改现有客户端代码。
- 符合开闭原则:对扩展开放(增加新类型的组件),对修改关闭(客户端代码通常无需修改)。
核心组成 (Core Components)
- 组件 (Component):
- 为组合中的对象声明统一的接口。
- 可以定义一些默认行为(可选)。
- 声明访问及管理子组件的方法(如
add()
,remove()
,getChild()
),通常在抽象类或接口中声明,但在叶子节点中可能为空实现或抛出异常。
- 叶子 (Leaf):
- 表示组合中的叶节点对象,即没有子节点的对象。
- 实现
Component
接口,但不提供添加、删除子组件等管理子节点的方法的具体实现(因为叶子不能有子节点)。 - 定义了叶子对象的行为。
- 容器 (Composite):
- 表示组合中的分支节点对象,即可以包含子节点的对象。
- 实现
Component
接口,并在内部维护一个子组件(Component
)的集合。 - 实现管理子组件的方法(
add
,remove
,getChild
等),并通常通过递归调用其子组件的相应方法来实现自己的功能。
核心思想 (Core Idea)
“让部分与整体具有相同的接口”。组合模式的核心在于,无论是单个对象(叶子)还是由多个对象组成的复合对象(容器),它们都实现了同一个接口。客户端在使用这些对象时,不需要区分它们是“原子”还是“容器”,可以统一地调用接口方法。对于容器,该方法的实现通常是递归地调用其所有子组件的同一方法。
使用场景 (Use Cases)
场景描述 | 说明 |
---|---|
表示部分-整体的树形结构 | 当需要表示对象的层次结构,且该结构呈现出“整体由部分组成,部分又可能由更小的部分组成”的特点时。 |
希望客户端统一处理单个对象和组合对象 | 客户端代码希望以相同的方式处理树中的任何节点,无论它是叶子还是分支。 |
文件系统 | 目录(容器)包含文件(叶子)和子目录(容器),文件和目录都可以有名称、大小、路径等属性,也可以进行删除、复制等操作。 |
图形用户界面 (GUI) | 窗口(容器)包含按钮、文本框(叶子)和面板(容器),它们都可以被绘制、移动、处理事件。 |
组织架构 | 公司(容器)包含部门(容器)和员工(叶子),它们都可以有名称、负责人、计算成本等。 |
菜单系统 | 菜单(容器)包含菜单项(叶子)和子菜单(容器),它们都可以被点击、显示、启用/禁用。 |
实现示例
文件组件-抽象组件
java
public interface FileComponent {
void addFile(FileComponent fileComponent);
void removeFile(FileComponent fileComponent);
String getName();
void reName(String newName);
}
文件-叶子节点
java
@Slf4j
public class File implements FileComponent {
private String fileName;
public File(String fileName) {
this.fileName = fileName;
}
@Override
public void addFile(FileComponent fileComponent) {
log.info("文件不支持添加子文件");
}
@Override
public void removeFile(FileComponent fileComponent) {
log.info("文件不支持删除子文件");
}
@Override
public String getName() {
return this.fileName;
}
@Override
public void reName(String newName) {
this.fileName = newName;
}
}
文件夹-复合节点
java
@Slf4j
public class FileFolder implements FileComponent {
private String folderName;
private List<FileComponent> fileComponentList = new ArrayList<>();
public FileFolder(String folderName) {
this.folderName = folderName;
}
@Override
public void addFile(FileComponent fileComponent) {
fileComponentList.add(fileComponent);
}
@Override
public void removeFile(FileComponent fileComponent) {
fileComponentList.remove(fileComponent);
}
@Override
public String getName() {
return this.folderName;
}
@Override
public void reName(String newName) {
this.folderName = newName;
}
public void ls() {
log.info("-{}", this.folderName);
for (FileComponent fileComponent : fileComponentList) {
if(fileComponent instanceof FileFolder) {
((FileFolder) fileComponent).ls();
}else {
log.info("- {}", fileComponent.getName());
}
}
}
}
调用-客户端
java
public class Main {
public static void main(String[] args) {
FileFolder fileFolder = new FileFolder("/");
FileFolder images = new FileFolder("images");
FileFolder docs = new FileFolder("docs");
File img1 = new File("img1.png");
File img2 = new File("img2.jpg");
File doc1 = new File("doc1.txt");
File doc2 = new File("doc2.pdf");
fileFolder.addFile(images);
fileFolder.addFile(docs);
images.addFile(img1);
images.addFile(img2);
docs.addFile(doc1);
docs.addFile(doc2);
fileFolder.ls();
}
}
打印
bash
18:10:24.176 [main] INFO com.lj.composite.FileFolder -- -/
18:10:24.179 [main] INFO com.lj.composite.FileFolder -- -images
18:10:24.179 [main] INFO com.lj.composite.FileFolder -- - img1.png
18:10:24.179 [main] INFO com.lj.composite.FileFolder -- - img2.jpg
18:10:24.179 [main] INFO com.lj.composite.FileFolder -- -docs
18:10:24.179 [main] INFO com.lj.composite.FileFolder -- - doc1.txt
18:10:24.179 [main] INFO com.lj.composite.FileFolder -- - doc2.pdf