设计模式(结构型)-组合模式
定义
组合模式的定义为:将对象组合成树形结构以表示 “部分 - 整体” 的层次结构,并且使得用户对单个对象和组合对象的使用具有一致性。其最关键的实现要点在于,简单对象和复合对象必须实现相同的接口,这一特性正是组合模式能够对组合对象和简单对象进行统一处理的基石。
想象一下,在一个文件系统中,文件是简单对象,文件夹是复合对象,文件夹可以包含文件和其他子文件夹。通过组合模式,我们可以将文件和文件夹看作是具有相同接口的对象,无论是对单个文件进行操作,还是遍历整个文件夹及其子文件夹,操作方式都能保持一致,极大地简化了代码逻辑。
类图
角色
-
抽象构件(Component)角色:抽象构件是一个抽象角色,它为参加组合的对象定义了公共接口以及默认行为。在透明式的组合模式中,它还负责管理所有的子对象;而在安全式的组合模式里,管理子对象的方法由树枝结构对象定义。抽象构件就像是整个层次结构的 “标准制定者”,确保所有对象都遵循统一的规则。
-
树叶构件(Leaf)角色:树叶对象是没有下级子对象的对象,它们定义了参加组合的原始对象的行为。比如在上述文件系统例子中,具体的文件就是树叶构件,它们实现了抽象构件定义的接口,执行如读取、写入等具体操作。
-
树枝构件(Composite)角色:树枝对象代表有下级子对象的对象,它们给出所有管理子对象的方法实现,如添加(Add)、删除(Remove)等。文件夹在文件系统中就扮演着树枝构件的角色,通过这些方法,文件夹可以方便地管理其包含的文件和子文件夹。
优缺点
优点
-
统一处理对象与对象容器:组合模式让客户端代码能够以相同的方式处理单个对象和对象容器,无需关心具体操作的是简单对象还是复杂的组合对象,极大地提高了代码的简洁性和可读性。
-
解耦客户代码与对象容器结构:将客户端代码与复杂的对象容器结构分离,降低了系统的耦合度,使得代码更易于维护和扩展。当对象容器结构发生变化时,客户端代码无需进行大量修改。
-
便于添加新构件:由于组合模式的统一接口设计,往组合对象中添加新的构件变得更加容易,不会对现有代码造成较大影响,增强了系统的可扩展性。
缺点
-
设计复杂度增加:组合模式的引入使得设计变得更加复杂,客户端需要花费更多时间去理清类之间的层次关系,理解各个角色的职责和交互方式。
-
接口设计挑战:在透明组合模式中,可能会导致叶子节点获得一些不属于它的方法,违背接口隔离性原则;而安全组合模式虽然避免了这一问题,但也可能增加客户端代码的判断逻辑。
注意事项
在使用组合模式时,有一些关键要点需要注意。当系统需要频繁遍历树枝结构的子构件时,可以考虑在父构件中存储遍历子构件的结构作为缓存,以提高系统性能。同时,客户端应尽量避免直接调用树叶类中的方法,而是借助父类的多态性完成调用,这样可以增强代码的复用性,减少代码冗余。
组合模式的类型
透明组合模式
透明组合模式将组合对象所有的公共方法都定义在抽象组件内。这种方式的优势在于客户端无需区分当前对象是树枝节点还是叶子节点,因为它们拥有完全一致的接口。然而,其缺点也很明显,叶子节点会得到一些不属于它的方法,破坏了接口隔离性原则。
安全组合模式
安全组合模式仅规定了系统各个层次最基础的一致性行为,把组合(树节点)自身的方法,如树枝节点管理子类的方法,放到自身当中。这种模式避免了透明组合模式中叶子节点获得多余方法的问题,但客户端在使用时可能需要更多地关注对象的类型,以调用合适的方法。
使用场景
-
当需要表示一个对象整体或部分的层次结构时,组合模式是绝佳选择,如组织结构图、文件目录结构等。
-
若希望用户忽略组合对象与单个对象的差异,统一使用组合结构中的所有对象,组合模式能够满足这一需求,简化用户操作。
使用案例
-
HashMap:HashMap 中的 putAll 方法,其参数是一个 Map。这里将一个 Map(可能是简单的键值对集合,也可能是包含多个子 Map 的复杂结构)作为整体添加到另一个 Map 中,体现了组合模式的思想,将单个对象和组合对象进行统一处理。
-
MyBatis 中的 SqlNode 接口:在 MyBatis 中,SqlNode 接口是一个抽象构件,具体的 SqlNode 实现类如 TextSqlNode(树叶构件)和 DynamicSqlNode(树枝构件),通过组合模式构建出复杂的 SQL 解析结构,实现对 SQL 语句的灵活处理和解析。
代码实现
-
抽象构件(
OrganizationComponent
):定义了组织组件的公共接口,包含一个抽象方法showInfo
用于显示信息,同时提供了add
和remove
方法的默认实现,在树叶构件中调用这些方法会抛出不支持操作的异常。
// 抽象构件角色
abstract class OrganizationComponent {protected String name;public OrganizationComponent(String name) {this.name = name;}// 抽象方法,用于显示信息public abstract void showInfo();// 以下两个方法在安全组合模式中,只在树枝构件中实现public void add(OrganizationComponent component) {throw new UnsupportedOperationException("不支持此操作");}public void remove(OrganizationComponent component) {throw new UnsupportedOperationException("不支持此操作");}
}
-
树枝构件(
Department
):继承自OrganizationComponent
,表示部门,包含一个List
用于存储子组件(员工或子部门),实现了add
、remove
和showInfo
方法,showInfo
方法会递归调用子组件的showInfo
方法。
// 树枝构件角色:部门
import java.util.ArrayList;
import java.util.List;class Department extends OrganizationComponent {private List<OrganizationComponent> children = new ArrayList<>();public Department(String name) {super(name);}@Overridepublic void add(OrganizationComponent component) {children.add(component);}@Overridepublic void remove(OrganizationComponent component) {children.remove(component);}@Overridepublic void showInfo() {System.out.println("部门: " + name);for (OrganizationComponent component : children) {component.showInfo();}}
}
-
树叶构件(
Employee
):继承自OrganizationComponent
,表示员工,实现了showInfo
方法,用于显示员工信息。
// 树叶构件角色:员工
class Employee extends OrganizationComponent {public Employee(String name) {super(name);}@Overridepublic void showInfo() {System.out.println("员工: " + name);}
}
-
测试(
Main
):创建了部门和员工对象,组装了组织架构,并调用根部门的showInfo
方法显示整个组织架构的信息。
// 主类,用于测试
public class Main {public static void main(String[] args) {// 创建部门Department rootDepartment = new Department("总公司");Department subDepartment1 = new Department("研发部");Department subDepartment2 = new Department("市场部");// 创建员工Employee employee1 = new Employee("张三");Employee employee2 = new Employee("李四");Employee employee3 = new Employee("王五");// 组装组织架构subDepartment1.add(employee1);subDepartment2.add(employee2);subDepartment2.add(employee3);rootDepartment.add(subDepartment1);rootDepartment.add(subDepartment2);// 显示组织架构信息rootDepartment.showInfo();}
}
总结
组合模式以其独特的树形结构设计和统一接口的处理方式,为处理对象的 “部分 - 整体” 关系提供了高效的解决方案。虽然它存在一定的设计复杂性,但在合适的场景下,能够显著提升代码的可维护性、扩展性和复用性,是软件开发中不可或缺的重要设计模式之一。