一、从一个问题开始
假设你要开发一个文件系统,文件和文件夹需要统一管理:
java
arduino
// 文件
public class File {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public void display() {
System.out.println("文件:" + name + " (" + size + "KB)");
}
}
// 文件夹
public class Folder {
private String name;
private List<File> files = new ArrayList<>();
private List<Folder> subFolders = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addFile(File file) {
files.add(file);
}
public void addFolder(Folder folder) {
subFolders.add(folder);
}
public void display() {
System.out.println("文件夹:" + name);
for (File file : files) {
file.display();
}
for (Folder folder : subFolders) {
folder.display();
}
}
}
使用代码:
java
ini
public class Client {
public static void main(String[] args) {
// 创建文件
File file1 = new File("a.txt", 10);
File file2 = new File("b.txt", 20);
File file3 = new File("c.txt", 30);
// 创建文件夹
Folder folder1 = new Folder("文档");
folder1.addFile(file1);
folder1.addFile(file2);
Folder root = new Folder("根目录");
root.addFile(file3);
root.addFolder(folder1);
// 显示
root.display();
}
}
问题:客户端需要区分文件和文件夹,用不同的方式处理。
| 问题 | 说明 |
|---|---|
| 处理方式不统一 | 文件和文件夹有不同的操作方式 |
| 递归操作复杂 | 每次都要写递归遍历代码 |
| 扩展困难 | 增加新类型的节点(如快捷方式),所有地方都要改 |
组合模式的解决方案 :让文件和文件夹实现同一接口,客户端可以统一处理。
二、组合模式是什么?
一句话定义 :将对象组合成树形结构 以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
通俗理解:
- 就像文件系统:文件和文件夹都可以"显示"、"重命名"、"删除"
- 就像公司组织架构:员工和部门都可以"发工资"、"汇报工作"
- 就像菜单系统:菜单项和子菜单都可以"显示"
角色结构:
text
scss
┌─────────────────────────────────────────────────────────┐
│ Component(抽象组件) │
│ + operation() │
│ + add(Component) // 可选 │
│ + remove(Component) // 可选 │
│ + getChild(int) // 可选 │
└─────────────────────────────────────────────────────────┘
△
┌───────────────┴───────────────┐
│ │
┌─────────────────────────┐ ┌─────────────────────────┐
│ Leaf(叶子节点) │ │ Composite(容器节点) │
│ - 没有子节点 │ │ - 有子节点 │
│ + operation() │ │ + operation() │
└─────────────────────────┘ │ + add(Component) │
│ + remove(Component) │
│ + getChild(int) │
└─────────────────────────┘
三、两种实现方式
3.1 透明组合模式(安全组合模式)
在抽象组件中声明所有方法,叶子节点和容器节点实现同一接口。
java
csharp
/**
* 抽象组件:文件系统节点
*/
public abstract class FileSystemNode {
protected String name;
public FileSystemNode(String name) {
this.name = name;
}
// 公共方法
public abstract void display(int indent);
public abstract int getSize();
// 容器特有方法(叶子节点抛出异常或空实现)
public void add(FileSystemNode node) {
throw new UnsupportedOperationException("叶子节点不支持添加操作");
}
public void remove(FileSystemNode node) {
throw new UnsupportedOperationException("叶子节点不支持删除操作");
}
public List<FileSystemNode> getChildren() {
throw new UnsupportedOperationException("叶子节点没有子节点");
}
}
/**
* 叶子节点:文件
*/
public class File extends FileSystemNode {
private int size;
public File(String name, int size) {
super(name);
this.size = size;
}
@Override
public void display(int indent) {
System.out.println(" ".repeat(indent) + "📄 " + name + " (" + size + "KB)");
}
@Override
public int getSize() {
return size;
}
}
/**
* 容器节点:文件夹
*/
public class Directory extends FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
public Directory(String name) {
super(name);
}
@Override
public void add(FileSystemNode node) {
children.add(node);
}
@Override
public void remove(FileSystemNode node) {
children.remove(node);
}
@Override
public List<FileSystemNode> getChildren() {
return children;
}
@Override
public void display(int indent) {
System.out.println(" ".repeat(indent) + "📁 " + name);
for (FileSystemNode child : children) {
child.display(indent + 1);
}
}
@Override
public int getSize() {
int total = 0;
for (FileSystemNode child : children) {
total += child.getSize();
}
return total;
}
}
3.2 安全组合模式
将容器特有方法单独定义在容器节点中,不在抽象组件中声明。
java
arduino
/**
* 抽象组件:只定义公共方法
*/
public interface FileSystemComponent {
void display(int indent);
int getSize();
}
/**
* 叶子节点:文件
*/
public class SafeFile implements FileSystemComponent {
private String name;
private int size;
public SafeFile(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public void display(int indent) {
System.out.println(" ".repeat(indent) + "📄 " + name + " (" + size + "KB)");
}
@Override
public int getSize() {
return size;
}
}
/**
* 容器节点:文件夹(单独拥有添加/删除方法)
*/
public class SafeDirectory implements FileSystemComponent {
private String name;
private List<FileSystemComponent> children = new ArrayList<>();
public SafeDirectory(String name) {
this.name = name;
}
// 容器特有方法(不在接口中)
public void add(FileSystemComponent component) {
children.add(component);
}
public void remove(FileSystemComponent component) {
children.remove(component);
}
public List<FileSystemComponent> getChildren() {
return children;
}
@Override
public void display(int indent) {
System.out.println(" ".repeat(indent) + "📁 " + name);
for (FileSystemComponent child : children) {
child.display(indent + 1);
}
}
@Override
public int getSize() {
int total = 0;
for (FileSystemComponent child : children) {
total += child.getSize();
}
return total;
}
}
3.3 使用示例
java
ini
public class Client {
public static void main(String[] args) {
// 创建文件
FileSystemComponent file1 = new SafeFile("a.txt", 10);
FileSystemComponent file2 = new SafeFile("b.txt", 20);
FileSystemComponent file3 = new SafeFile("c.txt", 30);
FileSystemComponent file4 = new SafeFile("d.txt", 40);
// 创建文件夹
SafeDirectory folder1 = new SafeDirectory("文档");
folder1.add(file1);
folder1.add(file2);
SafeDirectory folder2 = new SafeDirectory("图片");
folder2.add(file3);
SafeDirectory root = new SafeDirectory("根目录");
root.add(file4);
root.add(folder1);
root.add(folder2);
// 统一显示
root.display(0);
System.out.println("\n总大小:" + root.getSize() + "KB");
}
}
输出:
text
scss
📁 根目录
📄 d.txt (40KB)
📁 文档
📄 a.txt (10KB)
📄 b.txt (20KB)
📁 图片
📄 c.txt (30KB)
总大小:100KB
四、透明组合 vs 安全组合
| 对比项 | 透明组合模式 | 安全组合模式 |
|---|---|---|
| 接口设计 | 叶子节点和容器节点接口相同 | 容器节点有额外方法 |
| 安全性 | 叶子节点可能被误调用add/remove | 编译期就能避免错误调用 |
| 透明性 | 客户端对叶子/容器完全透明 | 客户端需要区分叶子/容器 |
| 适用场景 | 叶子节点操作少,简单场景 | 需要类型安全,复杂场景 |
| 推荐程度 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
五、实战场景
5.1 公司组织架构
java
csharp
/**
* 组织节点接口
*/
public interface OrganizationComponent {
void display();
double getSalary();
}
/**
* 员工(叶子节点)
*/
public class Employee implements OrganizationComponent {
private String name;
private String position;
private double salary;
public Employee(String name, String position, double salary) {
this.name = name;
this.position = position;
this.salary = salary;
}
@Override
public void display() {
System.out.println(" 员工:" + name + "(" + position + ")");
}
@Override
public double getSalary() {
return salary;
}
}
/**
* 部门(容器节点)
*/
public class Department implements OrganizationComponent {
private String name;
private List<OrganizationComponent> members = new ArrayList<>();
public Department(String name) {
this.name = name;
}
public void add(OrganizationComponent component) {
members.add(component);
}
public void remove(OrganizationComponent component) {
members.remove(component);
}
@Override
public void display() {
System.out.println("部门:" + name);
for (OrganizationComponent member : members) {
member.display();
}
}
@Override
public double getSalary() {
double total = 0;
for (OrganizationComponent member : members) {
total += member.getSalary();
}
return total;
}
}
// 使用
public class Company {
public static void main(String[] args) {
// 创建员工
Employee emp1 = new Employee("张三", "开发工程师", 15000);
Employee emp2 = new Employee("李四", "测试工程师", 12000);
Employee emp3 = new Employee("王五", "产品经理", 18000);
Employee emp4 = new Employee("赵六", "HR", 10000);
// 创建部门
Department techDept = new Department("技术部");
techDept.add(emp1);
techDept.add(emp2);
techDept.add(emp3);
Department hrDept = new Department("人事部");
hrDept.add(emp4);
Department company = new Department("XX科技有限公司");
company.add(techDept);
company.add(hrDept);
// 显示组织架构
company.display();
System.out.println("\n公司总薪资:" + company.getSalary() + "元");
}
}
输出:
text
部门:XX科技有限公司
部门:技术部
员工:张三(开发工程师)
员工:李四(测试工程师)
员工:王五(产品经理)
部门:人事部
员工:赵六(HR)
公司总薪资:55000.0元
5.2 菜单系统
java
csharp
/**
* 菜单组件
*/
public interface MenuComponent {
String getName();
double getPrice();
void print();
}
/**
* 菜单项(叶子节点)
*/
public class MenuItem implements MenuComponent {
private String name;
private double price;
private String description;
public MenuItem(String name, double price, String description) {
this.name = name;
this.price = price;
this.description = description;
}
@Override
public String getName() { return name; }
@Override
public double getPrice() { return price; }
@Override
public void print() {
System.out.println(" " + name + " - ¥" + price + "(" + description + ")");
}
}
/**
* 子菜单(容器节点)
*/
public class SubMenu implements MenuComponent {
private String name;
private List<MenuComponent> items = new ArrayList<>();
public SubMenu(String name) {
this.name = name;
}
public void add(MenuComponent item) {
items.add(item);
}
public void remove(MenuComponent item) {
items.remove(item);
}
@Override
public String getName() { return name; }
@Override
public double getPrice() {
return items.stream().mapToDouble(MenuComponent::getPrice).sum();
}
@Override
public void print() {
System.out.println(name + ":");
for (MenuComponent item : items) {
item.print();
}
}
}
// 使用
SubMenu drinkMenu = new SubMenu("饮品");
drinkMenu.add(new MenuItem("咖啡", 28, "现磨咖啡"));
drinkMenu.add(new MenuItem("奶茶", 18, "珍珠奶茶"));
drinkMenu.add(new MenuItem("果汁", 22, "鲜榨果汁"));
SubMenu foodMenu = new SubMenu("主食");
foodMenu.add(new MenuItem("汉堡", 35, "牛肉汉堡"));
foodMenu.add(new MenuItem("意面", 42, "番茄肉酱意面"));
SubMenu dessertMenu = new SubMenu("甜品");
dessertMenu.add(new MenuItem("蛋糕", 28, "提拉米苏"));
dessertMenu.add(new MenuItem("冰淇淋", 15, "香草冰淇淋"));
SubMenu fullMenu = new SubMenu("=== 完整菜单 ===");
fullMenu.add(drinkMenu);
fullMenu.add(foodMenu);
fullMenu.add(dessertMenu);
fullMenu.print();
5.3 XML/JSON 解析
java
typescript
/**
* JSON 节点接口
*/
public interface JsonNode {
String toString();
Object getValue();
}
/**
* 基本类型节点(叶子)
*/
public class ValueNode implements JsonNode {
private Object value;
public ValueNode(Object value) {
this.value = value;
}
@Override
public Object getValue() {
return value;
}
@Override
public String toString() {
if (value instanceof String) {
return """ + value + """;
}
return String.valueOf(value);
}
}
/**
* 对象节点(容器)
*/
public class ObjectNode implements JsonNode {
private Map<String, JsonNode> properties = new LinkedHashMap<>();
public void put(String key, JsonNode value) {
properties.put(key, value);
}
@Override
public Object getValue() {
Map<String, Object> result = new LinkedHashMap<>();
properties.forEach((k, v) -> result.put(k, v.getValue()));
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
properties.forEach((k, v) -> {
sb.append(""").append(k).append("":").append(v).append(",");
});
if (sb.length() > 1) sb.deleteCharAt(sb.length() - 1);
sb.append("}");
return sb.toString();
}
}
/**
* 数组节点(容器)
*/
public class ArrayNode implements JsonNode {
private List<JsonNode> elements = new ArrayList<>();
public void add(JsonNode element) {
elements.add(element);
}
@Override
public Object getValue() {
return elements.stream().map(JsonNode::getValue).collect(Collectors.toList());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
for (JsonNode e : elements) {
sb.append(e).append(",");
}
if (sb.length() > 1) sb.deleteCharAt(sb.length() - 1);
sb.append("]");
return sb.toString();
}
}
六、常见面试题
Q1:组合模式的优缺点?
优点:
| 优点 | 说明 |
|---|---|
| 统一处理 | 叶子节点和容器节点使用方式一致 |
| 易于扩展 | 增加新类型的节点不影响现有代码 |
| 符合开闭原则 | 客户端不依赖具体组件类 |
| 简化客户端代码 | 不需要写条件判断区分叶子/容器 |
缺点:
| 缺点 | 说明 |
|---|---|
| 设计复杂 | 叶子节点和容器节点差异大时不好处理 |
| 类型安全 | 透明模式可能误调用叶子节点不支持的方法 |
| 遍历开销 | 深层树形结构遍历可能影响性能 |
Q2:什么时候用组合模式?
✅ 适合的场景:
- 需要表示树形结构(文件系统、组织架构、菜单)
- 需要统一处理部分和整体
- 可以对组合对象进行递归操作
❌ 不适合的场景:
- 树形结构很简单
- 叶子节点和容器节点差异太大
Q3:组合模式和其他模式的关系?
| 模式 | 关系 |
|---|---|
| 迭代器模式 | 可以用迭代器遍历组合结构的子节点 |
| 访问者模式 | 可以对组合结构的每个节点执行操作 |
| 责任链模式 | 可以组合成树形责任链 |
七、总结与速记卡
核心要点
text
组合模式 = 抽象组件 + 叶子节点 + 容器节点 + 树形结构
代码模板
java
csharp
// 1. 抽象组件
public abstract class Component {
public abstract void operation();
public void add(Component c) { /* 空实现或抛异常 */ }
public void remove(Component c) { /* 空实现或抛异常 */ }
}
// 2. 叶子节点
public class Leaf extends Component {
public void operation() { /* 具体实现 */ }
}
// 3. 容器节点
public class Composite extends Component {
private List<Component> children = new ArrayList<>();
public void add(Component c) { children.add(c); }
public void remove(Component c) { children.remove(c); }
public void operation() {
for (Component child : children) {
child.operation();
}
}
}
一句话记忆
| 场景 | 方案 |
|---|---|
| 树形结构,需要统一处理 | 组合模式 ✅ |
| 文件和文件夹 | 组合模式 ✅ |
| 组织架构 | 组合模式 ✅ |
| 菜单系统 | 组合模式 ✅ |