这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
组合模式
前言
今天我们来学习结构型模式的第六种----组合模式。
在探讨组合模式之前,先回顾几个概念,就是组合、继承、聚合。
- 继承:它是 is a 的关系,对于接口来说是实现。例如 猫是动物。
- 聚合:它体现整体和部分的关系,has a 的关系,例如:家里有只猫。这个整体和部分的关系不是强依赖的,家里也可以没有猫。
- 组合:它同样体现整体和部分的关系,但这关系比聚合更强,它强调整体和部分的生命周期是一样的,整体生命周期结束也意味着部分的生命周期结束。
好了,下面正式学习组合模式。
一、组合模式入门
1.1 概述
组合模式(Composite Pattern)常常用来表达整体和部分的关系。而它也被常常用来表示树形菜单结构,我们可以通过这种模式将对象组合成树状结构,并且能够使用独立对象一样使用它们。
那么具体如何理解呢,
首先,组合模式它通常使用一个通用接口来与产品
和盒子
进行交互。比如一个产品内部是有很多黑盒组成,例如一个电脑由主机、键盘、显示器、鼠标构成。
电脑是个产品,这几个部件对应我们来说就是盒子。对于电脑,我们在里面声明一个计算总价的方法;对于盒子,我们就遍历所有盒子计算出鼠标、显示器、键盘的价格,如果主机是组装的,那它里面可能又有很多盒子,包括 内存条、硬盘等,由需要访问这些小黑盒
的价格, 以这种递归的方式处理了电脑内部组成部分的价格,得到最终电脑的价格,而我们无需关注黑盒里面的复杂内容,只需调用相同的接口进行处理即可。类似下图的逻辑。
明白了组合模式的定义,我们来看看 维基百科的定义:
在软件工程中,组合模式是一种分区设计模式。组合模式描述了一组对象,它们的处理方式与相同类型对象的单个实例相同。复合的目的是将对象“组合”成树结构以表示部分-整体层次结构。实现复合模式可以让客户端统一处理单个对象和组合。
组合模式可以让客户统一处理单个对象和组合。就是与我们的常见的树形目录的组装处理是类似的。
由于最近的疫情反扑,每天都有着本土病例的新增。我现在看到的疫情数据如下,这里面我们以市级为单位做了个初步统计,如果要计算昨日中国本土病例的新增值。这里就可以用到组合模式。后面,我们用代码来演示下。
1.2 结构
1.2.1 角色
组合模式中主要包含三种角色:
- 抽象根节点:定义系统各层次对象的共同方法和属性,可以预先定义一些默认行为和属性
- 树枝节点:定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构
- 叶子节点:叶子节点对象,其下再无分支,是系统层次遍历的最小单位
1.2.2 模式分类
组合模式根据抽象构件类的定义形式,可以分为透明组合模式和安全组合模式两种形式:
-
透明组合模式
- 透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,这样所有的树干对象和树叶对象都有相同的接口。
- 但是缺点就是不够安全,比如添加子类,如果是树叶元素就不需要添加树叶的这个功能,但是它却有这个接口存在,如果运行时不小心调用可能会报错
-
安全组合模式:
- 在安全模式中,抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点类中进行管理并实现这些方法,它的缺点就是不够透明,必须有区别的对待叶子构件和容器构件。
1.3 应用场景
上面分析了组合模式的角色与分类,下面分析下它的适用场景。
组合模式它是应对树形结构而生,所以组合模式的适用场景就是出现树形结构的地方,比如:文件目录显示,多级目录呈现等树形结构数据的操作。概括起来就是下面两种:
- 在需要表示一个对象整体与部分的层次结构的场合
- 在要求对用户用统一接口适用组合结构中的所有对象的场合。
1.4 优缺点
优点:
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
缺点:
- 客户端需要花更多的时间理清类之间的层次关系。
- 不容易限制容器中的构建。
- 不容易用继承的方法来增加构建的新功能。
- 比较难限制组合中的组件类型。
二、案例讲解
接下来,我们把这个疫情统计的案例用组合模式实现一下吧。
这边先挑选两个省份和一个城市来做个演示。我们要计算出两个省份和一个城市的总感染人数。
测试类:
public class Test {
public static void main(String[] args) {
/**
* 筛选几个城市疫情数目,
* 中国昨日本土疫情新增:
* |-- 江苏:38
* -- 扬州:37
* -- 南京: 1
* |-- 台湾:22
* |-- 湖北:10
* -- 武汉:4
* -- 荆门:6
* |-- ......
*/
int sum =0;
Component china = new Component("中国");
Component js = new Component("江苏");
City nj = new City(1, "南京");
City yz = new City(37, "扬州");
js.add(nj);
js.add(yz);
City tw = new City(22, "台湾");
Component hb = new Component("湖北");
City wh = new City(4, "武汉");
City jm = new City(6, "荆门");
hb.add(wh);
hb.add(jm);
china.add(js);
china.add(tw);
china.add(hb);
System.out.println("昨日感染人数:"+china.count());
}
}
复制代码
树叶构件:城市
/**
* @Author xiaolei
* 树叶构件:城市
**/
public class City implements Counter{
// 数量
private int sum =0;
// 名称
private String name;
public City(int sum,String name){
this.sum=sum;
this.name=name;
}
public int count() {
return sum;
}
}
复制代码
树干构件:容器
public class Component implements Counter{
private String name;
private ArrayList<Counter> arrayList =new ArrayList<Counter>();
public Component(String name){
this.name=name;
}
// 添加城市人数
public void add(Counter counter){
arrayList.add(counter);
}
// 计算
public int count() {
int sum=0;
for (Counter counter : arrayList) {
sum+=counter.count();
}
return sum;
}
}
复制代码
抽象构件:
/**
* 统计接口
**/
public interface Counter {
int count();
}
复制代码
输出结果:
昨日感染人数:70
复制代码
三、总结
组合模式处理的正是整体和部分的组合。如果树叶构件和树干构件有很多差异的话,比如很多属性和方法都不一样的时候,就不适用它了,而适用它可以非常方便的处理树形结构,本文中提到三个例子,一个是文件夹的目录层级结构,一个是电脑的价格计算,以及一个疫情感染统计计算。通过这些简单易懂的例子,可以帮助我们快速理解组合模式。
设计模式本身在应用中是有一定难度的,需要一定的业务经验积累,如果哪一天你在哪个底层源码中突然领略到哪个你看过的模式,就值了,如果哪一天,你在面对业务捉急的时候,突然把脑门一拍,哎,要不试试组合模式,那你可能就顿悟了,希望那一天离你更近一点。
最后,祝愿疫情早日结束!!!
互动环节
- 如果我的文章对你有用,请给我点个👍,感谢你😊!
- 有问题,欢迎在评论区指出!💪