跳到主要内容

组合模式(Composite)

组合模式是一种结构型设计模式,它将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

模式定义

组合模式(Composite Pattern):将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

核心要点

  1. 树形结构:将对象组织成树形结构
  2. 统一接口:单个对象和组合对象使用相同的接口
  3. 递归处理:可以递归地处理整个树结构
  4. 部分-整体:表示对象的部分-整体层次结构

问题场景

假设我们需要实现一个文件系统,包含文件和文件夹。文件夹可以包含文件和其他文件夹。我们需要遍历整个文件系统,计算总大小:

// 问题代码:需要区分文件和文件夹
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 安全式

特性透明式安全式
接口位置所有方法在接口中管理方法在组合类中
客户端统一处理需要区分类型
叶子节点需要实现不必要方法不需要实现不必要方法
类型安全运行时异常编译时检查

优缺点分析

优点

  1. 统一接口:客户端可以统一处理单个对象和组合对象
  2. 简化客户端:客户端不需要知道是叶子还是组合
  3. 易于扩展:新增组件类型不需要修改现有代码
  4. 递归结构:天然支持树形结构的递归处理

缺点

  1. 设计复杂:需要设计良好的组件接口
  2. 类型限制:难以限制组合中的组件类型
  3. 透明性问题:透明式可能导致运行时错误

使用建议

何时使用组合模式

  • 需要表示对象的部分-整体层次结构
  • 希望用户统一处理单个对象和组合对象
  • 需要递归处理树形结构

何时避免使用组合模式

  • 层次结构简单,不需要递归处理
  • 叶子和组合的区别对客户端很重要
  • 组合模式会导致设计过于复杂

最佳实践

  1. 选择合适的实现方式:透明式或安全式
  2. 保持接口简单:只声明必要的方法
  3. 考虑使用抽象类:提供默认实现
  4. 注意内存管理:大型树结构可能占用大量内存

小结

组合模式是处理树形结构的利器:

应用场景示例
文件系统文件和文件夹
图形编辑器简单图形和复合图形
菜单系统菜单项和菜单
组织架构员工和部门

练习

  1. 实现一个算术表达式求值器,支持数字和加减乘除表达式
  2. 实现一个 XML 解析器,将 XML 文档解析为树形结构
  3. 实现一个购物车系统,支持单个商品和商品组合
  4. 分析 Java Swing 中 Container 的设计

参考资源