设计模式之组合模式详解

深入理解设计模式之组合模式(Composite Pattern)

一、引言

在软件开发中,我们经常需要处理树形结构的数据。比如:文件系统中的文件和文件夹、公司的组织架构、菜单系统、XML文档结构等。这些场景有一个共同的特点:它们都是树形结构,并且叶子节点和容器节点在某些操作上需要保持一致性。

传统的处理方式是为叶子节点和容器节点定义不同的类,客户端需要区分它们并使用不同的方式处理。这会导致客户端代码复杂,耦合度高,难以维护。

复制代码
传统方式的问题:

if (node instanceof File) {
    File file = (File) node;
    file.display();
} else if (node instanceof Folder) {
    Folder folder = (Folder) node;
    folder.display();
    for (Component child : folder.getChildren()) {
        // 递归处理...需要再次判断类型
    }
}

代码充满了类型判断,难以扩展

组合模式为这类问题提供了优雅的解决方案。它就像现实中的文件系统:无论是文件还是文件夹,你都可以用统一的方式打开、删除、移动它们;文件夹可以包含文件,也可以包含其他文件夹,形成树形结构。

本文将深入探讨组合模式的原理、实现方式,并结合文件系统、组织架构、Swing组件树等实际应用,帮助你全面掌握这一重要的设计模式。

二、什么是组合模式

2.1 定义

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

2.2 核心思想

  • 统一对待:叶子节点和容器节点实现相同的接口,客户端统一对待
  • 树形结构:将对象组合成树形结构表示层次关系
  • 递归组合:容器对象可以包含叶子对象,也可以包含其他容器对象
  • 透明性:客户端无需区分叶子节点和容器节点

2.3 模式结构

复制代码
组合模式结构:

┌───────────────────────┐
│   <<interface>>       │
│     Component         │  抽象组件
├───────────────────────┤
│ + operation()         │  通用操作
│ + add(Component)      │  添加子节点
│ + remove(Component)   │  移除子节点
│ + getChild(int)       │  获取子节点
└──────────┬────────────┘
           △
           │ 实现
    ┌──────┴──────┐
    │             │
┌───┴──────┐  ┌──┴─────────────┐
│  Leaf    │  │   Composite    │  容器节点
├──────────┤  ├────────────────┤
│operation │  │- children: List│  包含子节点
└──────────┘  │+ operation()   │
              │+ add()         │
              │+ remove()      │
              │+ getChild()    │
              └────────────────┘

树形结构示例:

         Composite(root)
         /     |      \
        /      |       \
    Leaf1  Composite2  Leaf2
              /   \
             /     \
         Leaf3    Leaf4

客户端代码:
Component component = getComponent();  // 可能是Leaf或Composite
component.operation();  // 统一调用,无需判断类型

2.4 组合模式的两种实现

复制代码
1. 透明方式(Transparent):
   - 在Component中声明所有方法(包括管理子节点的方法)
   - 优点:客户端完全透明,无需区分类型
   - 缺点:Leaf需要实现管理方法(但实际不支持)

   Component
   ├─ operation()
   ├─ add()       ← Leaf也有这些方法(但会抛异常)
   ├─ remove()
   └─ getChild()

2. 安全方式(Safe):
   - 在Component中只声明共同方法
   - 管理方法只在Composite中定义
   - 优点:类型安全,Leaf不会有无效方法
   - 缺点:客户端需要区分Leaf和Composite

   Component
   └─ operation()  ← 只有通用方法

   Composite extends Component
   ├─ add()        ← 管理方法只在这里
   ├─ remove()
   └─ getChild()

三、基础示例

3.1 场景:文件系统

文件系统是组合模式最经典的应用场景。

抽象组件(透明方式):

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 抽象组件:文件系统组件
 */
public abstract class FileSystemComponent {
    protected String name;

    public FileSystemComponent(String name) {
        this.name = name;
    }

    /**
     * 显示组件信息(通用方法)
     */
    public abstract void display(int depth);

    /**
     * 添加子组件(容器方法)
     * 叶子节点不支持,抛出异常
     */
    public void add(FileSystemComponent component) {
        throw new UnsupportedOperationException("不支持此操作");
    }

    /**
     * 移除子组件(容器方法)
     */
    public void remove(FileSystemComponent component) {
        throw new UnsupportedOperationException("不支持此操作");
    }

    /**
     * 获取子组件(容器方法)
     */
    public FileSystemComponent getChild(int index) {
        throw new UnsupportedOperationException("不支持此操作");
    }

    /**
     * 获取大小
     */
    public abstract long getSize();

    public String getName() {
        return name;
    }
}

叶子节点:

java 复制代码
/**
 * 叶子节点:文件
 */
public class File extends FileSystemComponent {
    private long size;

    public File(String name, long size) {
        super(name);
        this.size = size;
    }

    @Override
    public void display(int depth) {
        // 缩进显示
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            indent.append("  ");
        }
        System.out.println(indent + "📄 文件: " + name + " (" + size + " KB)");
    }

    @Override
    public long getSize() {
        return size;
    }
}

容器节点:

java 复制代码
/**
 * 容器节点:文件夹
 */
public class Folder extends FileSystemComponent {
    private List<FileSystemComponent> children = new ArrayList<>();

    public Folder(String name) {
        super(name);
    }

    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }

    @Override
    public FileSystemComponent getChild(int index) {
        return children.get(index);
    }

    @Override
    public void display(int depth) {
        // 显示文件夹
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            indent.append("  ");
        }
        System.out.println(indent + "📁 文件夹: " + name);

        // 递归显示所有子组件
        for (FileSystemComponent child : children) {
            child.display(depth + 1);
        }
    }

    @Override
    public long getSize() {
        // 文件夹大小 = 所有子组件大小之和
        long total = 0;
        for (FileSystemComponent child : children) {
            total += child.getSize();
        }
        return total;
    }

    public List<FileSystemComponent> getChildren() {
        return children;
    }
}

客户端使用:

java 复制代码
public class FileSystemDemo {
    public static void main(String[] args) {
        // 创建根文件夹
        Folder root = new Folder("根目录");

        // 创建documents文件夹
        Folder documents = new Folder("Documents");
        documents.add(new File("简历.docx", 50));
        documents.add(new File("项目报告.pdf", 120));

        // 创建photos文件夹
        Folder photos = new Folder("Photos");
        photos.add(new File("照片1.jpg", 300));
        photos.add(new File("照片2.jpg", 280));

        // 创建photos的子文件夹
        Folder vacation = new Folder("Vacation");
        vacation.add(new File("海滩.jpg", 400));
        vacation.add(new File("山景.jpg", 350));
        photos.add(vacation);

        // 创建music文件夹
        Folder music = new Folder("Music");
        music.add(new File("歌曲1.mp3", 5000));
        music.add(new File("歌曲2.mp3", 4800));

        // 组装树形结构
        root.add(documents);
        root.add(photos);
        root.add(music);
        root.add(new File("README.txt", 5));

        // 显示整个文件系统(统一操作)
        System.out.println("========== 文件系统结构 ==========\n");
        root.display(0);

        // 计算总大小(统一操作)
        System.out.println("\n========== 大小统计 ==========");
        System.out.println("根目录总大小: " + root.getSize() + " KB");
        System.out.println("Documents大小: " + documents.getSize() + " KB");
        System.out.println("Photos大小: " + photos.getSize() + " KB");

        // 演示透明性:客户端无需区分File和Folder
        System.out.println("\n========== 演示透明性 ==========");
        processComponent(root);
        processComponent(documents);
        processComponent(new File("test.txt", 10));
    }

    /**
     * 统一处理组件(无需判断类型)
     */
    private static void processComponent(FileSystemComponent component) {
        System.out.println("处理组件: " + component.getName() +
                         ", 大小: " + component.getSize() + " KB");
    }
}

输出:

复制代码
========== 文件系统结构 ==========

📁 文件夹: 根目录
  📁 文件夹: Documents
    📄 文件: 简历.docx (50 KB)
    📄 文件: 项目报告.pdf (120 KB)
  📁 文件夹: Photos
    📄 文件: 照片1.jpg (300 KB)
    📄 文件: 照片2.jpg (280 KB)
    📁 文件夹: Vacation
      📄 文件: 海滩.jpg (400 KB)
      📄 文件: 山景.jpg (350 KB)
  📁 文件夹: Music
    📄 文件: 歌曲1.mp3 (5000 KB)
    📄 文件: 歌曲2.mp3 (4800 KB)
  📄 文件: README.txt (5 KB)

========== 大小统计 ==========
根目录总大小: 11305 KB
Documents大小: 170 KB
Photos大小: 1330 KB

========== 演示透明性 ==========
处理组件: 根目录, 大小: 11305 KB
处理组件: Documents, 大小: 170 KB
处理组件: test.txt, 大小: 10 KB

四、实际生产场景应用

4.1 场景:企业组织架构

企业的组织架构天然就是树形结构。

java 复制代码
/**
 * 抽象组件:组织单元
 */
abstract class OrganizationComponent {
    protected String name;
    protected String position;

    public OrganizationComponent(String name, String position) {
        this.name = name;
        this.position = position;
    }

    /**
     * 显示组织结构
     */
    public abstract void display(int depth);

    /**
     * 添加下属
     */
    public void add(OrganizationComponent component) {
        throw new UnsupportedOperationException();
    }

    /**
     * 移除下属
     */
    public void remove(OrganizationComponent component) {
        throw new UnsupportedOperationException();
    }

    /**
     * 计算总人数
     */
    public abstract int getEmployeeCount();

    /**
     * 计算总工资
     */
    public abstract double getTotalSalary();

    public String getName() { return name; }
    public String getPosition() { return position; }
}

/**
 * 叶子节点:员工
 */
class Employee extends OrganizationComponent {
    private double salary;

    public Employee(String name, String position, double salary) {
        super(name, position);
        this.salary = salary;
    }

    @Override
    public void display(int depth) {
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            indent.append("  ");
        }
        System.out.println(indent + "👤 " + position + ": " + name +
                         " (工资: ¥" + salary + ")");
    }

    @Override
    public int getEmployeeCount() {
        return 1;
    }

    @Override
    public double getTotalSalary() {
        return salary;
    }

    public double getSalary() {
        return salary;
    }
}

/**
 * 容器节点:部门
 */
class Department extends OrganizationComponent {
    private List<OrganizationComponent> members = new ArrayList<>();

    public Department(String name) {
        super(name, "部门");
    }

    @Override
    public void add(OrganizationComponent component) {
        members.add(component);
    }

    @Override
    public void remove(OrganizationComponent component) {
        members.remove(component);
    }

    @Override
    public void display(int depth) {
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            indent.append("  ");
        }
        System.out.println(indent + "🏢 " + name);

        // 递归显示所有成员
        for (OrganizationComponent member : members) {
            member.display(depth + 1);
        }
    }

    @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;
    }
}

/**
 * 测试组织架构
 */
class OrganizationDemo {
    public static void main(String[] args) {
        // 创建公司
        Department company = new Department("科技有限公司");

        // 创建技术部
        Department techDept = new Department("技术部");
        techDept.add(new Employee("张三", "技术总监", 30000));
        techDept.add(new Employee("李四", "高级工程师", 20000));
        techDept.add(new Employee("王五", "工程师", 15000));

        // 创建销售部
        Department salesDept = new Department("销售部");
        salesDept.add(new Employee("赵六", "销售总监", 25000));
        salesDept.add(new Employee("钱七", "销售经理", 18000));

        // 创建销售部的子部门
        Department regionalSales = new Department("华东区销售");
        regionalSales.add(new Employee("孙八", "区域经理", 16000));
        regionalSales.add(new Employee("周九", "销售代表", 12000));
        salesDept.add(regionalSales);

        // 创建人事部
        Department hrDept = new Department("人事部");
        hrDept.add(new Employee("吴十", "人事经理", 18000));
        hrDept.add(new Employee("郑十一", "招聘专员", 12000));

        // 组装公司结构
        company.add(new Employee("CEO", "首席执行官", 50000));
        company.add(techDept);
        company.add(salesDept);
        company.add(hrDept);

        // 显示组织结构
        System.out.println("========== 组织架构 ==========\n");
        company.display(0);

        // 统计信息
        System.out.println("\n========== 统计信息 ==========");
        System.out.println("总人数: " + company.getEmployeeCount());
        System.out.println("总工资: ¥" + company.getTotalSalary());
        System.out.println("技术部人数: " + techDept.getEmployeeCount());
        System.out.println("技术部工资: ¥" + techDept.getTotalSalary());
    }
}

4.2 场景:菜单系统

Web应用中的多级菜单系统。

java 复制代码
/**
 * 抽象组件:菜单组件
 */
abstract class MenuComponent {
    protected String name;
    protected String url;
    protected String icon;

    public MenuComponent(String name, String url, String icon) {
        this.name = name;
        this.url = url;
        this.icon = icon;
    }

    /**
     * 渲染菜单
     */
    public abstract void render(int level);

    /**
     * 添加子菜单
     */
    public void add(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    /**
     * 移除子菜单
     */
    public void remove(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    /**
     * 检查权限
     */
    public abstract boolean hasPermission(String userId);

    public String getName() { return name; }
    public String getUrl() { return url; }
}

/**
 * 叶子节点:菜单项
 */
class MenuItem extends MenuComponent {
    private String permission;  // 权限标识

    public MenuItem(String name, String url, String icon, String permission) {
        super(name, url, icon);
        this.permission = permission;
    }

    @Override
    public void render(int level) {
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < level; i++) {
            indent.append("  ");
        }
        System.out.println(indent + icon + " " + name + " -> " + url);
    }

    @Override
    public boolean hasPermission(String userId) {
        // 简化:实际应该查询数据库
        return true;
    }
}

/**
 * 容器节点:菜单组
 */
class Menu extends MenuComponent {
    private List<MenuComponent> children = new ArrayList<>();

    public Menu(String name, String icon) {
        super(name, null, icon);
    }

    @Override
    public void add(MenuComponent component) {
        children.add(component);
    }

    @Override
    public void remove(MenuComponent component) {
        children.remove(component);
    }

    @Override
    public void render(int level) {
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < level; i++) {
            indent.append("  ");
        }
        System.out.println(indent + icon + " " + name);

        // 递归渲染子菜单
        for (MenuComponent child : children) {
            child.render(level + 1);
        }
    }

    @Override
    public boolean hasPermission(String userId) {
        // 菜单组:只要有一个子菜单有权限就显示
        for (MenuComponent child : children) {
            if (child.hasPermission(userId)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取有权限的子菜单
     */
    public List<MenuComponent> getAuthorizedChildren(String userId) {
        List<MenuComponent> result = new ArrayList<>();
        for (MenuComponent child : children) {
            if (child.hasPermission(userId)) {
                result.add(child);
            }
        }
        return result;
    }
}

/**
 * 测试菜单系统
 */
class MenuSystemDemo {
    public static void main(String[] args) {
        // 创建菜单结构
        Menu mainMenu = new Menu("主菜单", "🏠");

        // 系统管理菜单
        Menu systemMenu = new Menu("系统管理", "⚙️");
        systemMenu.add(new MenuItem("用户管理", "/system/users", "👥", "system:user"));
        systemMenu.add(new MenuItem("角色管理", "/system/roles", "🎭", "system:role"));
        systemMenu.add(new MenuItem("权限管理", "/system/permissions", "🔐", "system:permission"));

        // 业务管理菜单
        Menu businessMenu = new Menu("业务管理", "💼");

        Menu orderMenu = new Menu("订单管理", "📋");
        orderMenu.add(new MenuItem("订单列表", "/business/orders", "📝", "business:order:list"));
        orderMenu.add(new MenuItem("订单统计", "/business/orders/stats", "📊", "business:order:stats"));

        Menu productMenu = new Menu("商品管理", "📦");
        productMenu.add(new MenuItem("商品列表", "/business/products", "📄", "business:product:list"));
        productMenu.add(new MenuItem("分类管理", "/business/categories", "🏷️", "business:category"));

        businessMenu.add(orderMenu);
        businessMenu.add(productMenu);

        // 报表中心
        Menu reportMenu = new Menu("报表中心", "📈");
        reportMenu.add(new MenuItem("销售报表", "/reports/sales", "💰", "report:sales"));
        reportMenu.add(new MenuItem("库存报表", "/reports/inventory", "📦", "report:inventory"));

        // 组装主菜单
        mainMenu.add(systemMenu);
        mainMenu.add(businessMenu);
        mainMenu.add(reportMenu);
        mainMenu.add(new MenuItem("个人中心", "/profile", "👤", "profile"));

        // 渲染完整菜单
        System.out.println("========== 完整菜单结构 ==========\n");
        mainMenu.render(0);

        // 模拟根据权限渲染菜单
        System.out.println("\n========== 根据权限过滤后的菜单 ==========\n");
        renderAuthorizedMenu(mainMenu, "user123", 0);
    }

    /**
     * 渲染有权限的菜单
     */
    private static void renderAuthorizedMenu(MenuComponent menu, String userId, int level) {
        if (menu.hasPermission(userId)) {
            menu.render(level);
        }
    }
}

五、开源框架中的应用

5.1 AWT/Swing组件树

Java GUI框架使用组合模式构建组件树。

java 复制代码
/**
 * Swing组件树示例
 *
 * Component (抽象组件)
 *    ├─ Container (容器组件)
 *    │     ├─ JPanel
 *    │     ├─ JFrame
 *    │     └─ JDialog
 *    └─ JComponent (叶子组件)
 *          ├─ JButton
 *          ├─ JLabel
 *          └─ JTextField
 */

import javax.swing.*;
import java.awt.*;

class SwingCompositeDemo {
    public static void main(String[] args) {
        // 创建窗口(容器)
        JFrame frame = new JFrame("组合模式示例");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 300);

        // 创建面板(容器)
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());

        // 添加组件(叶子)
        panel.add(new JLabel("用户名:"));
        panel.add(new JTextField(15));
        panel.add(new JLabel("密码:"));
        panel.add(new JPasswordField(15));
        panel.add(new JButton("登录"));

        // 容器包含容器
        frame.add(panel);

        frame.setVisible(true);

        // 组合模式的体现:
        // 1. Container和Component都是组件
        // 2. Container可以包含Component
        // 3. Container也可以包含其他Container
        // 4. 统一的操作:setVisible(), setEnabled()等
    }
}

/**
 * 简化的Swing组件结构
 */
abstract class Component {
    protected String name;

    public abstract void paint();
    public abstract void setVisible(boolean visible);

    // 容器方法(透明方式)
    public void add(Component component) {
        throw new UnsupportedOperationException();
    }

    public void remove(Component component) {
        throw new UnsupportedOperationException();
    }
}

class Container extends Component {
    private List<Component> children = new ArrayList<>();

    @Override
    public void paint() {
        System.out.println("绘制容器: " + name);
        for (Component child : children) {
            child.paint();
        }
    }

    @Override
    public void setVisible(boolean visible) {
        System.out.println((visible ? "显示" : "隐藏") + "容器: " + name);
        // 递归设置所有子组件
        for (Component child : children) {
            child.setVisible(visible);
        }
    }

    @Override
    public void add(Component component) {
        children.add(component);
    }

    @Override
    public void remove(Component component) {
        children.remove(component);
    }
}

class Button extends Component {
    @Override
    public void paint() {
        System.out.println("绘制按钮: " + name);
    }

    @Override
    public void setVisible(boolean visible) {
        System.out.println((visible ? "显示" : "隐藏") + "按钮: " + name);
    }
}

5.2 MyBatis动态SQL

MyBatis的SqlNode使用组合模式构建动态SQL。

java 复制代码
/**
 * MyBatis的SqlNode接口(简化版)
 *
 * 用于构建动态SQL的树形结构
 */
interface SqlNode {
    /**
     * 应用节点,生成SQL片段
     */
    boolean apply(DynamicContext context);
}

/**
 * 动态上下文
 */
class DynamicContext {
    private StringBuilder sqlBuilder = new StringBuilder();
    private Map<String, Object> bindings = new HashMap<>();

    public void appendSql(String sql) {
        sqlBuilder.append(sql).append(" ");
    }

    public String getSql() {
        return sqlBuilder.toString().trim();
    }

    public Map<String, Object> getBindings() {
        return bindings;
    }
}

/**
 * 叶子节点:静态文本节点
 */
class StaticTextSqlNode implements SqlNode {
    private String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }

    @Override
    public boolean apply(DynamicContext context) {
        context.appendSql(text);
        return true;
    }
}

/**
 * 容器节点:混合SQL节点
 */
class MixedSqlNode implements SqlNode {
    private List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 递归应用所有子节点
        for (SqlNode node : contents) {
            node.apply(context);
        }
        return true;
    }
}

/**
 * 条件节点:IF节点
 */
class IfSqlNode implements SqlNode {
    private String test;
    private SqlNode contents;

    public IfSqlNode(SqlNode contents, String test) {
        this.test = test;
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 简化:实际会解析test表达式
        if (evaluateTest(test, context)) {
            contents.apply(context);
            return true;
        }
        return false;
    }

    private boolean evaluateTest(String test, DynamicContext context) {
        // 简化实现
        return true;
    }
}

/**
 * 测试MyBatis SqlNode
 */
class MyBatisSqlNodeDemo {
    public static void main(String[] args) {
        // 构建SQL树:SELECT * FROM users WHERE 1=1 <if test="name != null">AND name = #{name}</if>
        List<SqlNode> nodes = new ArrayList<>();
        nodes.add(new StaticTextSqlNode("SELECT * FROM users WHERE 1=1"));

        // IF节点
        SqlNode ifContents = new StaticTextSqlNode("AND name = #{name}");
        nodes.add(new IfSqlNode(ifContents, "name != null"));

        // 创建根节点
        MixedSqlNode rootNode = new MixedSqlNode(nodes);

        // 应用节点,生成SQL
        DynamicContext context = new DynamicContext();
        rootNode.apply(context);

        System.out.println("生成的SQL: " + context.getSql());

        System.out.println("\n组合模式的体现:");
        System.out.println("1. SqlNode是抽象组件");
        System.out.println("2. StaticTextSqlNode是叶子节点");
        System.out.println("3. MixedSqlNode、IfSqlNode是容器节点");
        System.out.println("4. 可以组合成复杂的动态SQL树");
    }
}

六、组合模式的优缺点

6.1 优点

1. 统一对待单个对象和组合对象

java 复制代码
// 无需类型判断
void process(Component component) {
    component.operation();  // 统一调用
}

// 而不是:
void process(Object obj) {
    if (obj instanceof Leaf) {
        ((Leaf) obj).operation();
    } else if (obj instanceof Composite) {
        ((Composite) obj).operation();
    }
}

2. 简化客户端代码

复制代码
客户端不需要关心:
- 是叶子节点还是容器节点
- 树的层次结构
- 递归遍历逻辑

只需统一调用接口方法即可

3. 易于扩展

复制代码
添加新的组件类型:
只需实现Component接口
不影响现有代码

4. 符合开闭原则

复制代码
对扩展开放:可以添加新的Leaf或Composite
对修改封闭:无需修改现有代码

6.2 缺点

1. 设计较为抽象

复制代码
需要合理设计组件接口
需要理解递归结构
新手可能难以理解

2. 限制组件类型困难

复制代码
Composite可以包含任何Component
如果想限制只能包含特定类型,实现起来困难

3. 透明方式的问题

复制代码
Leaf需要实现管理方法(如add、remove)
但实际不支持,只能抛异常
违反了类型安全

七、最佳实践

7.1 选择合适的实现方式

java 复制代码
/**
 * 透明方式 vs 安全方式
 */

// 1. 透明方式:适用于希望客户端完全透明的场景
abstract class TransparentComponent {
    public abstract void operation();
    public void add(TransparentComponent c) {
        throw new UnsupportedOperationException();
    }
    public void remove(TransparentComponent c) {
        throw new UnsupportedOperationException();
    }
}

// 2. 安全方式:适用于类型安全要求高的场景
interface SafeComponent {
    void operation();
}

class SafeComposite implements SafeComponent {
    private List<SafeComponent> children = new ArrayList<>();

    @Override
    public void operation() {
        for (SafeComponent child : children) {
            child.operation();
        }
    }

    // 只在Composite中定义管理方法
    public void add(SafeComponent component) {
        children.add(component);
    }

    public void remove(SafeComponent component) {
        children.remove(component);
    }
}

7.2 提供遍历器

java 复制代码
/**
 * 提供迭代器遍历组合结构
 */
class CompositeWithIterator extends FileSystemComponent {
    private List<FileSystemComponent> children = new ArrayList<>();

    public CompositeWithIterator(String name) {
        super(name);
    }

    /**
     * 提供迭代器
     */
    public Iterator<FileSystemComponent> iterator() {
        return children.iterator();
    }

    /**
     * 深度优先遍历
     */
    public Iterator<FileSystemComponent> deepIterator() {
        return new CompositeIterator(children.iterator());
    }

    // ... 其他方法
}

/**
 * 组合迭代器(深度优先)
 */
class CompositeIterator implements Iterator<FileSystemComponent> {
    private Stack<Iterator<FileSystemComponent>> stack = new Stack<>();

    public CompositeIterator(Iterator<FileSystemComponent> iterator) {
        stack.push(iterator);
    }

    @Override
    public boolean hasNext() {
        if (stack.isEmpty()) {
            return false;
        }

        Iterator<FileSystemComponent> iterator = stack.peek();
        if (!iterator.hasNext()) {
            stack.pop();
            return hasNext();
        }

        return true;
    }

    @Override
    public FileSystemComponent next() {
        if (hasNext()) {
            Iterator<FileSystemComponent> iterator = stack.peek();
            FileSystemComponent component = iterator.next();

            // 如果是容器,将其子元素压入栈
            if (component instanceof CompositeWithIterator) {
                stack.push(((CompositeWithIterator) component).iterator());
            }

            return component;
        }
        return null;
    }
}

7.3 缓存计算结果

java 复制代码
/**
 * 缓存昂贵的计算结果
 */
class CachedComposite extends FileSystemComponent {
    private List<FileSystemComponent> children = new ArrayList<>();
    private Long cachedSize = null;  // 缓存大小

    public CachedComposite(String name) {
        super(name);
    }

    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
        // 添加子组件时清除缓存
        cachedSize = null;
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
        // 移除子组件时清除缓存
        cachedSize = null;
    }

    @Override
    public long getSize() {
        if (cachedSize == null) {
            // 计算并缓存
            long total = 0;
            for (FileSystemComponent child : children) {
                total += child.getSize();
            }
            cachedSize = total;
        }
        return cachedSize;
    }

    @Override
    public void display(int depth) {
        // 实现显示逻辑
    }
}

7.4 使用访问者模式增强

java 复制代码
/**
 * 结合访问者模式处理不同操作
 */
interface ComponentVisitor {
    void visitFile(File file);
    void visitFolder(Folder folder);
}

abstract class VisitableComponent extends FileSystemComponent {
    public VisitableComponent(String name) {
        super(name);
    }

    public abstract void accept(ComponentVisitor visitor);
}

/**
 * 计算文件数量的访问者
 */
class FileCountVisitor implements ComponentVisitor {
    private int fileCount = 0;

    @Override
    public void visitFile(File file) {
        fileCount++;
    }

    @Override
    public void visitFolder(Folder folder) {
        // 文件夹本身不计数
    }

    public int getFileCount() {
        return fileCount;
    }
}

八、总结

8.1 核心要点

  1. 组合模式的本质:将对象组合成树形结构,统一对待单个对象和组合对象
  2. 关键角色
    • Component:抽象组件,定义统一接口
    • Leaf:叶子节点,不能包含子节点
    • Composite:容器节点,可以包含子节点
  3. 适用场景
    • 需要表示对象的部分-整体层次结构
    • 希望统一对待单个对象和组合对象
    • 树形结构的数据
  4. 核心优势
    • 简化客户端代码
    • 易于扩展
    • 符合开闭原则

8.2 使用建议

复制代码
选择组合模式的检查清单:

✓ 数据结构是否是树形的?
✓ 是否需要统一对待叶子和容器?
✓ 是否需要递归处理?
✓ 是否希望客户端代码简单?

如果有3个以上"是",建议使用组合模式!

8.3 与其他模式的对比

复制代码
组合 vs 装饰者:
- 组合:关注部分-整体关系,一对多
- 装饰者:关注动态添加职责,一对一

组合 vs 迭代器:
- 组合:定义树形结构
- 迭代器:遍历组合结构

组合 vs 访问者:
- 组合:定义结构
- 访问者:对结构执行操作

8.4 实践经验

  1. 合理选择实现方式:透明方式 vs 安全方式
  2. 提供便利方法:如迭代器、深度优先遍历等
  3. 考虑性能优化:缓存计算结果,避免重复计算
  4. 注意内存管理:深层嵌套可能导致栈溢出
  5. 结合其他模式:可以与访问者、迭代器等模式结合

相关推荐
杨充2 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充2 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充2 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充2 小时前
2.面向对象的特性
设计模式
杨充2 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充2 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充2 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯5 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯14 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式