组合模式(Composite)
组合模式是一种结构型设计模式,它将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
模式定义
组合模式(Composite Pattern):将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
核心要点
- 树形结构:将对象组织成树形结构
- 统一接口:单个对象和组合对象使用相同的接口
- 递归处理:可以递归地处理整个树结构
- 部分-整体:表示对象的部分-整体层次结构
问题场景
假设我们需要实现一个文件系统,包含文件和文件夹。文件夹可以包含文件和其他文件夹。我们需要遍历整个文件系统,计算总大小:
// 问题代码:需要区分文件和文件夹
public class File {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
public long getSize() {
return size;
}
}
public class Directory {
private String name;
private List<File> files = new ArrayList<>();
private List<Directory> directories = new ArrayList<>();
public void addFile(File file) {
files.add(file);
}
public void addDirectory(Directory directory) {
directories.add(directory);
}
public long getSize() {
long total = 0;
// 计算文件大小
for (File file : files) {
total += file.getSize();
}
// 递归计算子文件夹大小
for (Directory dir : directories) {
total += dir.getSize();
}
return total;
}
}
// 使用
public class Client {
public static void main(String[] args) {
Directory root = new Directory("root");
root.addFile(new File("file1.txt", 100));
Directory subDir = new Directory("subdir");
subDir.addFile(new File("file2.txt", 200));
root.addDirectory(subDir);
System.out.println("总大小: " + root.getSize());
}
}
存在的问题:
- 客户端需要区分文件和文件夹
- 添加新类型的节点需要修改现有代码
- 代码重复,难以维护
- 无法统一处理文件和文件夹
解决方案
使用组合模式,将文件和文件夹抽象为统一的组件接口:
// 组件接口
public interface FileSystemComponent {
void display(String indent);
long getSize();
}
// 叶子节点:文件
public class File implements FileSystemComponent {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public void display(String indent) {
System.out.println(indent + "- 文件: " + name + " (" + size + " bytes)");
}
@Override
public long getSize() {
return size;
}
}
// 组合节点:文件夹
public class Directory implements FileSystemComponent {
private String name;
private List<FileSystemComponent> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
// 添加子组件
public void add(FileSystemComponent component) {
children.add(component);
}
// 移除子组件
public void remove(FileSystemComponent component) {
children.remove(component);
}
// 获取子组件
public FileSystemComponent getChild(int index) {
return children.get(index);
}
@Override
public void display(String indent) {
System.out.println(indent + "+ 文件夹: " + name);
for (FileSystemComponent component : children) {
component.display(indent + " ");
}
}
@Override
public long getSize() {
long total = 0;
for (FileSystemComponent component : children) {
total += component.getSize();
}
return total;
}
}
// 使用示例
public class Client {
public static void main(String[] args) {
// 创建文件系统
Directory root = new Directory("root");
root.add(new File("readme.txt", 100));
root.add(new File("config.json", 50));
Directory src = new Directory("src");
src.add(new File("Main.java", 200));
src.add(new File("Utils.java", 150));
root.add(src);
Directory docs = new Directory("docs");
docs.add(new File("api.md", 300));
docs.add(new File("guide.md", 400));
root.add(docs);
// 显示文件系统结构
root.display("");
// 计算总大小
System.out.println("\n总大小: " + root.getSize() + " bytes");
}
}
输出:
+ 文件夹: root
- 文件: readme.txt (100 bytes)
- 文件: config.json (50 bytes)
+ 文件夹: src
- 文件: Main.java (200 bytes)
- 文件: Utils.java (150 bytes)
+ 文件夹: docs
- 文件: api.md (300 bytes)
- 文件: guide.md (400 bytes)
总大小: 1200 bytes
模式结构
组成部分:
- Component(组件):声明叶子和组合节点的共同接口
- Leaf(叶子):表示叶子节点,没有子节点
- Composite(组合):表示组合节点,存储子组件
- Client(客户端):通过组件接口与所有对象交互
实现方式
1. 透明式组合
在组件接口中声明所有方法,包括管理子节点的方法:
// 组件接口(透明式)
public interface Component {
void operation();
// 管理子节点的方法
void add(Component component);
void remove(Component component);
Component getChild(int index);
}
// 叶子节点
public class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void operation() {
System.out.println("叶子节点: " + name);
}
@Override
public void add(Component component) {
throw new UnsupportedOperationException("叶子节点不能添加子节点");
}
@Override
public void remove(Component component) {
throw new UnsupportedOperationException("叶子节点不能移除子节点");
}
@Override
public Component getChild(int index) {
throw new UnsupportedOperationException("叶子节点没有子节点");
}
}
// 组合节点
public class Composite implements Component {
private String name;
private List<Component> children = new ArrayList<>();
public Composite(String name) {
this.name = name;
}
@Override
public void operation() {
System.out.println("组合节点: " + name);
for (Component child : children) {
child.operation();
}
}
@Override
public void add(Component component) {
children.add(component);
}
@Override
public void remove(Component component) {
children.remove(component);
}
@Override
public Component getChild(int index) {
return children.get(index);
}
}
优点:客户端可以统一处理所有组件 缺点:叶子节点需要实现不必要的方法
2. 安全式组合
只在组合节点中声明管理子节点的方法:
// 组件接口(安全式)
public interface Component {
void operation();
}
// 叶子节点
public class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void operation() {
System.out.println("叶子节点: " + name);
}
}
// 组合节点
public class Composite implements Component {
private String name;
private List<Component> children = new ArrayList<>();
public Composite(String name) {
this.name = name;
}
@Override
public void operation() {
System.out.println("组合节点: " + name);
for (Component child : children) {
child.operation();
}
}
// 管理子节点的方法只在组合节点中
public void add(Component component) {
children.add(component);
}
public void remove(Component component) {
children.remove(component);
}
public Component getChild(int index) {
return children.get(index);
}
}
// 使用
public class Client {
public static void main(String[] args) {
Composite root = new Composite("root");
root.add(new Leaf("leaf1"));
Composite sub = new Composite("sub");
sub.add(new Leaf("leaf2"));
root.add(sub);
root.operation();
}
}
优点:叶子节点不需要实现不必要的方法 缺点:客户端需要区分叶子和组合节点
实际应用案例
1. 图形编辑器
组合模式常用于图形编辑器,表示图形的层次结构:
// 图形接口
public interface Graphic {
void draw();
void move(int x, int y);
}
// 简单图形:圆形
public class Circle implements Graphic {
private int x, y, radius;
public Circle(int x, int y, int radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
System.out.println("绘制圆形: 位置(" + x + "," + y + "), 半径" + radius);
}
@Override
public void move(int dx, int dy) {
x += dx;
y += dy;
}
}
// 简单图形:矩形
public class Rectangle implements Graphic {
private int x, y, width, height;
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("绘制矩形: 位置(" + x + "," + y + "), 大小" + width + "x" + height);
}
@Override
public void move(int dx, int dy) {
x += dx;
y += dy;
}
}
// 复合图形
public class CompositeGraphic implements Graphic {
private List<Graphic> children = new ArrayList<>();
public void add(Graphic graphic) {
children.add(graphic);
}
public void remove(Graphic graphic) {
children.remove(graphic);
}
@Override
public void draw() {
for (Graphic graphic : children) {
graphic.draw();
}
}
@Override
public void move(int dx, int dy) {
for (Graphic graphic : children) {
graphic.move(dx, dy);
}
}
}
// 使用
public class Client {
public static void main(String[] args) {
CompositeGraphic drawing = new CompositeGraphic();
drawing.add(new Circle(10, 10, 5));
drawing.add(new Rectangle(20, 20, 30, 40));
CompositeGraphic group = new CompositeGraphic();
group.add(new Circle(50, 50, 10));
group.add(new Rectangle(60, 60, 20, 30));
drawing.add(group);
drawing.draw();
drawing.move(5, 5);
}
}
2. 菜单系统
GUI 菜单系统是组合模式的典型应用:
// 菜单组件
public abstract class MenuComponent {
protected String name;
public MenuComponent(String name) {
this.name = name;
}
public void add(MenuComponent component) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent component) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int index) {
throw new UnsupportedOperationException();
}
public abstract void display(int depth);
}
// 菜单项(叶子)
public class MenuItem extends MenuComponent {
private String action;
public MenuItem(String name, String action) {
super(name);
this.action = action;
}
@Override
public void display(int depth) {
System.out.println(" ".repeat(depth) + "- " + name + " [" + action + "]");
}
}
// 菜单(组合)
public class Menu extends MenuComponent {
private List<MenuComponent> children = new ArrayList<>();
public Menu(String name) {
super(name);
}
@Override
public void add(MenuComponent component) {
children.add(component);
}
@Override
public void remove(MenuComponent component) {
children.remove(component);
}
@Override
public MenuComponent getChild(int index) {
return children.get(index);
}
@Override
public void display(int depth) {
System.out.println(" ".repeat(depth) + "+ " + name);
for (MenuComponent child : children) {
child.display(depth + 1);
}
}
}
// 使用
public class Client {
public static void main(String[] args) {
Menu fileMenu = new Menu("文件");
fileMenu.add(new MenuItem("新建", "newFile"));
fileMenu.add(new MenuItem("打开", "openFile"));
fileMenu.add(new MenuItem("保存", "saveFile"));
Menu editMenu = new Menu("编辑");
editMenu.add(new MenuItem("撤销", "undo"));
editMenu.add(new MenuItem("重做", "redo"));
Menu recentMenu = new Menu("最近文件");
recentMenu.add(new MenuItem("file1.txt", "openRecent"));
recentMenu.add(new MenuItem("file2.txt", "openRecent"));
fileMenu.add(recentMenu);
Menu mainMenu = new Menu("主菜单");
mainMenu.add(fileMenu);
mainMenu.add(editMenu);
mainMenu.display(0);
}
}
3. 组织架构
表示公司的组织架构:
// 组织单元接口
public interface OrganizationComponent {
String getName();
int getEmployeeCount();
double getTotalSalary();
void print(String indent);
}
// 员工(叶子)
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 String getName() {
return name;
}
@Override
public int getEmployeeCount() {
return 1;
}
@Override
public double getTotalSalary() {
return salary;
}
@Override
public void print(String indent) {
System.out.println(indent + "- " + name + " (" + position + ", ¥" + 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 String getName() {
return name;
}
@Override
public int getEmployeeCount() {
int count = 0;
for (OrganizationComponent member : members) {
count += member.getEmployeeCount();
}
return count;
}
@Override
public double getTotalSalary() {
double total = 0;
for (OrganizationComponent member : members) {
total += member.getTotalSalary();
}
return total;
}
@Override
public void print(String indent) {
System.out.println(indent + "+ " + name + " (员工: " + getEmployeeCount() + ")");
for (OrganizationComponent member : members) {
member.print(indent + " ");
}
}
}
// 使用
public class Client {
public static void main(String[] args) {
Department company = new Department("公司");
Department techDept = new Department("技术部");
techDept.add(new Employee("张三", "工程师", 15000));
techDept.add(new Employee("李四", "工程师", 14000));
techDept.add(new Employee("王五", "经理", 20000));
Department hrDept = new Department("人力资源部");
hrDept.add(new Employee("赵六", "HR", 10000));
hrDept.add(new Employee("钱七", "HR", 10000));
company.add(techDept);
company.add(hrDept);
company.print("");
System.out.println("\n总员工数: " + company.getEmployeeCount());
System.out.println("总薪资: ¥" + company.getTotalSalary());
}
}
4. Java AWT/Swing
Java AWT 和 Swing 中的组件体系使用了组合模式:
// Component 是所有组件的基类
// Container 是可以包含其他组件的容器
public abstract class Component {
public void paint(Graphics g) { }
}
public class Container extends Component {
private List<Component> components = new ArrayList<>();
public void add(Component comp) {
components.add(comp);
}
public void remove(Component comp) {
components.remove(comp);
}
@Override
public void paint(Graphics g) {
for (Component comp : components) {
comp.paint(g);
}
}
}
// 具体组件
public class Button extends Component { }
public class Label extends Component { }
public class Panel extends Container { }
public class Frame extends Container { }
透明式 vs 安全式
| 特性 | 透明式 | 安全式 |
|---|---|---|
| 接口位置 | 所有方法在接口中 | 管理方法在组合类中 |
| 客户端 | 统一处理 | 需要区分类型 |
| 叶子节点 | 需要实现不必要方法 | 不需要实现不必要方法 |
| 类型安全 | 运行时异常 | 编译时检查 |
优缺点分析
优点
- 统一接口:客户端可以统一处理单个对象和组合对象
- 简化客户端:客户端不需要知道是叶子还是组合
- 易于扩展:新增组件类型不需要修改现有代码
- 递归结构:天然支持树形结构的递归处理
缺点
- 设计复杂:需要设计良好的组件接口
- 类型限制:难以限制组合中的组件类型
- 透明性问题:透明式可能导致运行时错误
使用建议
何时使用组合模式
- 需要表示对象的部分-整体层次结构
- 希望用户统一处理单个对象和组合对象
- 需要递归处理树形结构
何时避免使用组合模式
- 层次结构简单,不需要递归处理
- 叶子和组合的区别对客户端很重要
- 组合模式会导致设计过于复杂
最佳实践
- 选择合适的实现方式:透明式或安全式
- 保持接口简单:只声明必要的方法
- 考虑使用抽象类:提供默认实现
- 注意内存管理:大型树结构可能占用大量内存
小结
组合模式是处理树形结构的利器:
| 应用场景 | 示例 |
|---|---|
| 文件系统 | 文件和文件夹 |
| 图形编辑器 | 简单图形和复合图形 |
| 菜单系统 | 菜单项和菜单 |
| 组织架构 | 员工和部门 |
练习
- 实现一个算术表达式求值器,支持数字和加减乘除表达式
- 实现一个 XML 解析器,将 XML 文档解析为树形结构
- 实现一个购物车系统,支持单个商品和商品组合
- 分析 Java Swing 中 Container 的设计