十、组合模式


1 基本介绍

组合模式(Composite Pattern),又称为 部分-整体模式,是一种用于 处理树形结构问题结构型 设计模式。它将对象组合成树形结构以表示“部分-整体”的层次结构,并使得用户对 单个对象(内容)组合对象(容器) 的使用具有 一致性,它模糊了 内容 和 容器 的概念,从而客户端可以像处理 内容 一样来处理 容器。

2 案例

提到 内容 与 容器 具有一致性,可以想到 套餐 与 套餐中的单品食物 也有一致性:买单品食物算一次购买,买套餐也算一次购买。除此之外,套餐中还可以嵌套套餐,从而形成 树形结构。例如本案例将会实现一个如下的树形结构:
alt text
其中,绿色的节点都是单品食物 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 类图

alt text
说明:

  • 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 总结

组合模式 是一种 结构型 设计模式,它经常被用来处理 树形结构 问题,模糊了 内容容器 的概念,使客户端可以像处理 内容 一样来处理 容器,进而构建出灵活且易于扩展的系统。然而,在使用组合模式时,也需要注意其可能带来的 复杂性性能问题

  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值