文章目录
1 基本介绍
组合模式(Composite Pattern),又称为 部分-整体模式,是一种用于 处理树形结构问题 的 结构型 设计模式。它将对象组合成树形结构以表示“部分-整体”的层次结构,并使得用户对 单个对象(内容) 和 组合对象(容器) 的使用具有 一致性,它模糊了 内容 和 容器 的概念,从而客户端可以像处理 内容 一样来处理 容器。
2 案例
提到 内容 与 容器 具有一致性,可以想到 套餐 与 套餐中的单品食物 也有一致性:买单品食物算一次购买,买套餐也算一次购买。除此之外,套餐中还可以嵌套套餐,从而形成 树形结构。例如本案例将会实现一个如下的树形结构:
其中,绿色的节点都是单品食物 SingleFood
,蓝色的节点都是套餐 SetMeal
,由于需要体现它们的一致性,所以让它们实现共同的抽象父类——食物 Food
。
2.1 Food 类
public abstract class Food { // 食物
/**
* 添加一个新的食物
* @param food 待添加食物
* @throws Exception 如果 food 是 SingleFood 对象,则会报错
*/
public void add(Food food) throws Exception {
throw new UnsupportedOperationException("如果是套餐,则没有实现该方法;" +
"如果是单品食物,则不应该调用该方法");
}
public abstract void printInfo(); // 打印食物的信息,供外部使用
protected abstract void printInfo(String prefix); // 带前缀打印食物的信息,供子类重写
}
2.2 SingleFood 类
public class SingleFood extends Food { // 单品食物
private final String foodName; // 单品食物的名称
public SingleFood(String foodName) {
this.foodName = foodName;
}
@Override
public void printInfo() {
System.out.println(foodName);
}
@Override
protected void printInfo(String prefix) {
System.out.println(prefix + foodName);
}
}
2.3 SetMeal 类
public class SetMeal extends Food { // 套餐
private final String setMealName; // 套餐的名称
private final List<Food> foodList = new ArrayList<>(); // 存储食物的集合
public SetMeal(String setMealName) {
this.setMealName = setMealName;
}
@Override
public void add(Food food) {
foodList.add(food);
}
@Override
public void printInfo() {
printInfo("");
}
@Override
protected void printInfo(String prefix) { // 递归输出
System.out.println(prefix + setMealName + ":");
for (Food food : foodList) {
food.printInfo(prefix + "- ");
}
}
}
2.4 Client 类
public class Client { // 创建 多种单品食物、经典套餐、豪华套餐,并打印信息
public static void main(String[] args) throws Exception {
// 创建所需的单品食物,并选取其中之一打印信息
Food hamburger = new SingleFood("汉堡");
Food cola = new SingleFood("可乐");
Food frenchFries = new SingleFood("炸薯条");
Food friedChicken = new SingleFood("炸鸡");
hamburger.printInfo();
System.out.println("======================"); // 分隔
// 创建经典套餐,并打印其信息
Food classicSetMeal = new SetMeal("经典套餐");
classicSetMeal.add(hamburger);
classicSetMeal.add(cola);
classicSetMeal.add(frenchFries);
classicSetMeal.printInfo();
System.out.println("======================"); // 分隔
// 创建豪华套餐,并打印其信息
Food luxurySetMeal = new SetMeal("豪华套餐");
luxurySetMeal.add(classicSetMeal);
luxurySetMeal.add(hamburger);
luxurySetMeal.add(friedChicken);
luxurySetMeal.printInfo();
}
}
2.5 Client 类运行的结果
汉堡
======================
经典套餐:
- 汉堡
- 可乐
- 炸薯条
======================
豪华套餐:
- 经典套餐:
- - 汉堡
- - 可乐
- - 炸薯条
- 汉堡
- 炸鸡
2.6 总结
按理来说,有 add()
添加方法 就有 remove()
移除方法、getContent()
获取 容器 内所有 内容 的方法。不过,为了避免案例太过复杂,所以没有实现 remove(), getContent()
,remove(), getContent()
和 add()
类似,都是在抽象父类中先抛出异常,然后在 容器 中实现,无需在 内容 中实现。
一旦在 内容 中调用 容器 特有的方法 add(), remove(), getContent()
时,会抛出异常,因为 内容 是 最小的 不可分割的 单位,就和 量子 一样,无法再往其中放置内容。
在本案例中需要仔细体会这两点:
- 套餐
SetMeal
与 单品食物SingleFood
之间形成的树形结构。 - 套餐
SetMeal
与 单品食物SingleFood
都继承了 食物Food
,从而拥有了一致性。
3 各角色之间的关系
3.1 角色
3.1.1 Component ( 组件 )
该角色负责 使 Leaf 角色 和 Composite 角色 具有 一致性,是他们两个的 父类。在本案例中,Food
抽象类扮演该角色。
3.1.2 Leaf ( 树叶 )
该角色表示 内容,是 无法放入其他内容 的最小单位。在本案例中,SingleFood
类扮演该角色。
3.1.3 Composite ( 复合物 )
该角色表示 容器,可以放入 Leaf 角色 和 Composite 角色。在本案例中,SetMeal
类扮演该角色。
3.1.4 Client ( 客户端 )
该角色负责 使用 本模式的各个角色。在本案例中,Client
类扮演该角色。
3.2 类图
说明:
- Component 中的
add(), remove(), getContent()
方法都做了抛出异常的实现,一旦 Leaf 调用了这几个方法之一,则会抛出异常;如果 Composite 没有重写这几个方法,则在调用时也会抛出异常。 - Component 中的
method1(), method2()
方法都是abstract
的,强制 Leaf 和 Composite 重写。
4 注意事项
- 明确整体与部分的关系:在设计初期,需要清晰地定义整体与部分之间的关系。容器(Composite)应该能够包含部分内容(Leaf),并且部分内容可以是 容器 或 内容 的实例。
- 设计合理的接口:设计一个抽象的组件接口,为所有的 Component(包括 Leaf 和 Composite)提供统一的接口。这个接口应该包含所有对象共有的方法,如添加、删除子对象,以及执行某些操作等。对于 Leaf 不支持的方法(如 添加 或 删除 子对象),可以在接口中提供默认实现(如 抛出异常 或 空操作),或者采用 安全式组合模式,将这部分方法放在 Composite 中,但是每次调用方法前先要从 Component 强制转换到 Composite。
- 平衡 透明性 和 安全性:
- 透明式组合模式 允许客户端无需区分是 Leaf 还是 Composite,直接对它们进行操作。但这可能导致 Leaf 执行一些无意义的方法 或 报错。
- 安全式组合模式 将管理子对象的方法放在 Composite 中,Leaf 不包含这些方法。这增加了安全性,但牺牲了透明性,客户端需要区分对象类型。
- 在实际应用中,需要根据具体需求平衡 透明性 和 安全性。
- 递归操作的处理:组合模式中的 递归操作 是很常见的,如 遍历整个树形结构。在处理递归时,需要注意递归的 终止条件 和 递归深度,避免 栈溢出 等错误。
- 性能考虑:
- 当树形结构较大时,递归操作可能会导致性能问题。在这种情况下,可以考虑使用 迭代(广度优先搜索) 替换 递归(深度优先搜索) 来优化性能。
- 同时,需要关注对象的创建和销毁成本,避免在频繁的操作中造成大量的内存分配和释放。
- 扩展性和灵活性:组合模式应具有良好的扩展性和灵活性,以便在需要时能够轻松地添加新的组件类型或修改现有组件的行为,这要求在设计时充分考虑未来可能的变化,并采用合理的抽象和封装来隔离变化点。
5 在源码中的使用
在 Spring 框架中,CacheManager
接口 和 CompositeCacheManager
实现类 通过组合模式来管理多个缓存管理器,从而实现缓存的灵活配置和使用。以下是 Spring 的 CacheManager
如何使用组合模式的详细解析:
5.1 Component——CacheManager 接口
Spring 的 CacheManager
接口是缓存管理器的核心接口,它定义了获取、管理缓存的基本方法。不同的缓存实现(如 Redis)会提供 CacheManager
接口的具体实现。
public interface CacheManager {
@Nullable
Cache getCache(String name);
Collection<String> getCacheNames();
}
5.2 Composite——CompositeCacheManager 类
CompositeCacheManager
是 Spring 提供的一个特殊的 CacheManager
实现,它本身 不直接管理缓存,而是 将多个 CacheManager
实例组合在一起,作为一个整体对外提供缓存服务。
给 CompositeCacheManager
对象添加 CacheManager
实例有两种方式:
- 构造器:允许在创建
CompositeCacheManager
实例时指定被组合的CacheManager
实例。 setCacheManagers()
:动态地添加多个CacheManager
实例。
CompositeCacheManager
类实现的方法(都使用了 递归遍历 的思想):
getCache()
:根据缓存名称获取缓存。该方法会 递归遍历 所有被组合的CacheManager
实例,直到找到包含指定名称缓存的CacheManager
,然后返回对应的缓存实例。如果所有CacheManager
都不包含该名称的缓存,则返回null
。getCacheNames()
:返回所有被组合的CacheManager
实例中所有缓存的名称集合。这通常涉及到 递归遍历 所有CacheManager
实例并收集它们的缓存名称。
public class CompositeCacheManager implements CacheManager, InitializingBean {
// 存储被组合的缓存管理器
private final List<CacheManager> cacheManagers = new ArrayList<>();
// 省略了一个成员变量
public CompositeCacheManager() {}
// 通过构造方法添加 CacheManager 实例
public CompositeCacheManager(CacheManager... cacheManagers) {
setCacheManagers(Arrays.asList(cacheManagers));
}
// 添加 CacheManager 实例
public void setCacheManagers(Collection<CacheManager> cacheManagers) {
this.cacheManagers.addAll(cacheManagers);
}
// 省略了两个成员方法
@Override
@Nullable
public Cache getCache(String name) {
for (CacheManager cacheManager : this.cacheManagers) {
Cache cache = cacheManager.getCache(name); // 递归遍历
if (cache != null) {
return cache;
}
}
return null;
}
@Override
public Collection<String> getCacheNames() {
Set<String> names = new LinkedHashSet<>();
for (CacheManager manager : this.cacheManagers) {
names.addAll(manager.getCacheNames()); // 递归遍历
}
return Collections.unmodifiableSet(names);
}
}
5.3 概括
Spring 的 CacheManager
接口和 CompositeCacheManager
实现类通过组合模式提供了一种 灵活 且 强大 的缓存管理机制。它允许开发人员 将多个缓存管理器组合在一起,通过统一的接口进行管理,从而 简化了缓存的配置和使用。
6 优缺点
优点:
- 清晰的层次结构:组合模式可以清晰地表达对象之间的 部分-整体 层次结构,使得代码更加易于理解和维护。
- 客户端代码简化:客户端可以 一致地 处理 单个对象 和 组合对象,无需区分当前处理的是单个对象还是组合对象,简化了客户端代码。
- 高内聚低耦合:组合模式将“部分”和“整体”的概念 分离,提高了系统的 内聚性,同时降低了系统的 耦合性。
- 易于扩展:在组合模式中添加新的组件类变得容易,因为只需要新的类实现抽象组件接口即可,无需修改现有类的代码。
- 灵活性:可以 灵活地 增加
add
和 删除remove
树中的节点,支持 动态地 组合 和 拆分 对象。
缺点:
- 设计相对复杂:组合模式的设计和实现相对复杂,特别是涉及到 递归操作 时,可能会增加代码的复杂度和理解难度。
- 可能使系统变得庞大:如果 树形结构 过于庞大,可能会增加系统的复杂性和管理难度,特别是在大型项目中。
- 增加对象数量:使用组合模式可能会增加系统中对象的数量,这可能会对内存和性能产生影响,尤其是在对象数量非常多的情况下
- 递归操作可能导致性能问题:组合模式中的递归操作可能会导致 性能问题,特别是在处理大型树形结构时。递归深度过大 可能会导致 栈溢出 等错误。
7 适用场景
- 表示具有 层次结构 的数据:当需要表示的对象集合呈现为 树形结构 时,可以使用组合模式。例如,文件系统的 目录 和 文件、组织结构中的 部门 与 员工、UI 界面中的 窗口 与 控件 等。
- 整体 与 部分 具有相同的接口:如果希望客户端能够忽略 整体对象 与 部分对象 之间的差异,对它们进行 统一处理,那么组合模式是一个很好的选择。它使得客户端可以通过相同的接口与 单个对象 或 对象组合 进行交互,从而简化了客户端的代码,提高了代码的 模块性 和 可重用性。
- 动态增加或删除对象:如果对象集合的结构是动态变化的,比如可以动态地添加或删除对象,组合模式能够很好地支持这种需求。
8 总结
组合模式 是一种 结构型 设计模式,它经常被用来处理 树形结构 问题,模糊了 内容 和 容器 的概念,使客户端可以像处理 内容 一样来处理 容器,进而构建出灵活且易于扩展的系统。然而,在使用组合模式时,也需要注意其可能带来的 复杂性 和 性能问题。