设计模式 - 组合模式:用树形结构处理对象之间的复杂关系
文章目录
- 一、引言
- 二、模式原理分析
- 三、代码示例
- 四、核心要点
- 五、使用场景分析
- 六、案例
- 七、为何使用组合模式?
- 八、优缺点剖析
- 九、最佳实践建议
- 十、总结
一、引言
“组合模式”(Composite Pattern)常被误解为“组合关系”。前者专注于将对象组合成树形结构来表示“整体—部分”的层次,并允许客户端以一致方式处理单个对象和组合对象;后者仅指通过封装多种对象完成某个功能。尽管组合模式在日常项目中并不如装饰、代理那样频繁,但它对于处理复杂结构、理解框架源码、数据库索引等场景至关重要。
二、模式原理分析
定义:将对象组合成树形结构以表示整体—部分的层次结构,客户端可统一对待单个对象和组合对象。
-
关键:
- 树形分层:组织节点形成多级结构。
- 统一操作:对叶子与组合节点调用同一接口。
下图为组合模式 UML:
角色说明:
- 抽象组件(Component):定义公共接口(如
operation()
)。 - 叶子节点(Leaf):树的最底层,不再有子节点,实现抽象组件接口。
- 组合节点(Composite / Node):可包含子组件,维护子列表并在其操作中遍历调用子组件。
三、代码示例
// 抽象组件
public abstract class Component {public abstract void operation();
}// 叶子节点
public class Leaf extends Component {@Overridepublic void operation() {// 叶子节点具体操作逻辑}
}// 组合节点
public class Node extends Component {private List<Component> children = new ArrayList<>();@Overridepublic void operation() {// 遍历所有子节点,统一调用for (Component child : children) {child.operation();}}public void add(Component c) {children.add(c);}public void remove(Component c) {children.remove(c);}public Component getChild(int index) {return children.get(index);}
}
注意:遍历可能用递归、深度优先或广度优先,不必局限于简单for循环,具体算法可根据场景灵活选用。
四、核心要点
组合模式封装了:
- 叶子与组合节点的区别;
- 真实结构的多种形态(树、环、双向链);
- 遍历算法的策略(深/广度、排序、过滤等);
- 操作策略(聚合、搜索、修改等)。
客户端只需面向统一接口,无需关心内部结构细节。
五、使用场景分析
- 树形结构管理:组织架构、文件系统、商品订单层次。
- 跨层级聚合统计:统计某部门或文件夹下所有子项的数据。
- 统一操作集合:对结构中所有节点执行新增、删除、查找等相同行为。
示例:订单信息计算
订单包含商品、发票、包装盒,盒子中可嵌套更小盒子,结构呈“倒置树”。组合模式可递归累加每层节点的金额、促销与抵扣,使计算逻辑简洁一致。
六、案例
- 公司组织架构:总经理→部门经理→组长→员工,各层均可“统计人数”“发放奖金”。
- 定义抽象组件
OrgComponent
,声明统一操作:统计人数和发放奖金。 - 实现叶子节点
Employee
,表示员工。 - 实现组合节点
Department
,表示部门,可添加/移除子组件,并递归实现操作。 - 提供示例构建组织架构并执行操作。
// 抽象组件
public abstract class OrgComponent {protected String name;public OrgComponent(String name) {this.name = name;}// 统计该节点及子节点的总人数public abstract int countEmployees();// 按指定金额发放奖金public abstract void distributeBonus(double amount);
}// 叶子节点:员工
public class Employee extends OrgComponent {public Employee(String name) {super(name);}@Overridepublic int countEmployees() {return 1; // 单个员工}@Overridepublic void distributeBonus(double amount) {System.out.printf("给员工 %s 发放奖金:%.2f 元%n", name, amount);}
}// 组合节点:部门/职位
public class Department extends OrgComponent {private List<OrgComponent> children = new ArrayList<>();public Department(String name) {super(name);}public void add(OrgComponent comp) {children.add(comp);}public void remove(OrgComponent comp) {children.remove(comp);}@Overridepublic int countEmployees() {int total = 0;for (OrgComponent comp : children) {total += comp.countEmployees();}return total;}@Overridepublic void distributeBonus(double amount) {System.out.printf("给部门 %s 总计 %.2f 元奖金,按人数平均分配:%d 人%n",name, amount, countEmployees());double perCapita = amount / countEmployees();for (OrgComponent comp : children) {comp.distributeBonus(perCapita);}}
}// 示例使用
public class CompanyDemo {public static void main(String[] args) {// 构建组织架构Department root = new Department("总经理");Department deptA = new Department("部门经理A");Department teamA1 = new Department("组长A1");teamA1.add(new Employee("张三"));teamA1.add(new Employee("李四"));Department teamA2 = new Department("组长A2");teamA2.add(new Employee("王五"));deptA.add(teamA1);deptA.add(teamA2);Department deptB = new Department("部门经理B");deptB.add(new Employee("赵六"));deptB.add(new Employee("钱七"));root.add(deptA);root.add(deptB);// 统计总人数System.out.println("公司总人数: " + root.countEmployees() + " 人");// 发放奖金 10000 元root.distributeBonus(10000.0);}
}
运行结果示例
公司总人数: 6 人
给部门 总经理 总计 10000.00 元奖金,按人数平均分配:6 人
给部门 部门经理A 总计 3333.33 元奖金,按人数平均分配:4 人
给部门 组长A1 总计 833.33 元奖金,按人数平均分配:2 人
给员工 张三 发放奖金:416.67 元
给员工 李四 发放奖金:416.67 元
给部门 组长A2 总计 833.33 元奖金,按人数平均分配:1 人
给员工 王五 发放奖金:833.33 元
给部门 部门经理B 总计 3333.33 元奖金,按人数平均分配:2 人
给员工 赵六 发放奖金:1666.67 元
给员工 钱七 发放奖金:1666.67 元
- MySQL B+ 树索引:叶子节点存储数据指针,内部节点存储键与子节点指针,整个树结构即组合模式的典型应用,提升查询性能。
七、为何使用组合模式?
- 层次管理:按层级自然分类对象,如订单与子订单、文件夹与文件。
- 统一行为:客户端代码无需区分处理单个对象或组合,简化调用逻辑。
- 快速扩展:动态添加新节点或新类别,符合开闭原则,无需修改现有代码。
八、优缺点剖析
收益
- 清晰分层,层次关系一目了然。
- 客户端使用统一接口,无差异化处理代码。
- 易于扩展,动态挂载节点类型。
损失
- 类型约束难:无编译期限制,需运行时检查。
- 额外检查成本:叶子与组合节点行为需抛出或忽略操作,增加实现复杂度。
- 遍历性能风险:错误算法(多层嵌套循环)在大数据量下可能导致指数级开销。
九、最佳实践建议
- 逻辑约束:在设计时明确节点类型与层级,避免任意组合。
- 遍历算法:选择合适的深度或广度优先、迭代器模式或并行遍历,控制时间复杂度。
- 异常处理:叶子节点对不支持的方法抛出
UnsupportedOperationException
,并在客户端捕获或规避。
十、总结
组合模式通过树形结构与统一接口,将复杂对象层次简化为可递归处理的组件,大大提升代码灵活性与可扩展性。但需谨慎约束与优化遍历,否则会带来维护和性能成本。