今天我们一起学习组合模式,当我们需要强调整体与部分之间存在层次关系,且整体与部分都具有一样的行为时就可以考虑使用组合模式。比如:在学校老师查人数时,他不需要亲自去数每个同学是否到齐,他可以让小组长去数自己组里有多少同学,然后每个小组长再将结果报给老师,老师做一个汇总即可。组合模式在实际开发中用的可能不是很多,并且使用起来也很简单,我们掌握它的思想即可。
1 组合模式定义
定义: 组合模式又叫部分-整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
我们用下图来解释一下该定义:
很明显这是一个普通的树结构,其中 G、D、E、F 是叶子节点,A、B、C为非叶子节点。假如我们现在要计算以 A 为根节点的树一共有多少个叶子节点,我们就可以来分别计算 A 的子节点 B 、C 分别有多少个叶子节点,在计算 B 时,需要计算 B 的子节点 G 有多少个叶子节点;在计算 C 时,又需要计算 C 的子节点 D、E、F 分别有所少个叶子节点。当B 、C 都计算出自己有多少叶子节点并相加后返回给 A ,从而我们就可以得出以 A 为根节点的树一共有多少个叶子节点了。
在计算叶子节点的过程中,我们就使用到了组合模式的思想,我们将 以 A 为根节点的树划分成两个子树:以 B 和 C 为根节点的树。B 树和 C 树与 A 树的关系就是部分与整体的关系,C 树组合了三个叶子节点,我们可以把 C、D、E、F当做一个整体,同样 B、G 也当做一个整体 。A 树有计算所有叶子节点的方法,那么我们也给它的子节点 B 树 和 C 树也赋予计算当前树中叶子节点的方法,以此类推也给 B 树和 C 树的子节点赋予计算叶子节点的方法,直到叶子节点,根据组合模式的思想,叶子节点也应该被赋予计算叶子节点的方法,对于叶子节点该方法返回 1 即可。
理解组合模式的精髓在于:把一组对象看成是一个对象前提是这些对象必须是相似的,即实现了同一个接口等,都具有相同的行为,对于客户端来讲,它并不知道自己使用的是一个单独的对象还是一组组合在一起的对象。就比如解放军在走正步训练时,如果我们从侧面看的话,我们很难看出是一个人在走,还是一个班在走,因为这个班的士兵所有动作都整齐划一。
2 UML 图
组合模式一般有如下几个角色:
Component:抽象构件类,定义所有子类共有的方法,叶子节点和组合构件类继承自该类。
Leaf:叶子节点,继承自 Component ,叶子节点不能再有子节点。
CompositeComponent:组合构建类,组合一些其他的 Component 实例 (叶子节点对象和组合构建类对象)。所以需要有管理这些对象的方法( add() 和 remove() 等)。
其中 doSomething() 方法就是这些对象共同的行为,当一个客户端调用 CompositeComponent 的 doSomething() 方法时,就需要循环调用 componentList 集合中所有的 Component 的 doSomething() 方法,这对于客户端来说是不透明的。
3 实际案例
接下来,我们已一个实际案例来展示一下如何使用组合模式。
需求:列出一个文件夹的文件树,并计算该文件夹所占用的空间。
在 windows 系统中,我们可以使用 tree 命令来查看文件树:
根据组合模式的思想,我们可以将文件夹看做是一个组合对象,文件夹中有文件夹,也有文件。
我们要展示这个文件树,我们就要递归的展示所有文件夹中的所有文件。计算所占空间时,就计算当前文件夹下所有子文件夹与文件的大小之和,以此类推。
接下来我们展示一下代码:
File.java:Component:抽象构件类,主要定义文件和文件夹都具有的属性和方法。
public abstract class File {
private String name;//文件名称
private int size;//文件大小,如果是文件夹,则为其中所有文件的大小之和
private int height;//文件相对于根目录的深度,主要用于展示时体现层次结构
public File(String name,int size){
this.name = name;
this.size = size;
}
//显示当前文件
public abstract void display();
//计算当前文件的大小
public abstract int countSize();
public String getName() {
return name;
}
public int getSize() {
return size;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
}
TxtFile.java:Leaf:文本文件类,可视作叶子节点。
display() 方法就直接输出自己的 name 即可。
countSize() 方法直接返回自己的 size 。
public class TxtFile extends File {
public TxtFile(String name, int size) {
super(name+".txt", size);
}
@Override
public void display() {
String prex = "";
for(int i = 0;i<super.getHeight();i++){
prex = prex+"--";
}
System.out.println(prex+super.getName());
}
@Override
public int countSize() {
return super.getSize();
}
}
Folder.java:CompositeComponent:文件夹类,其中组合了一些文件和文件夹。
display() 方法不仅要输出自己的名称,还要循环调用当前文件夹中的子文件夹和文件的 display() 方法。
countSize() 方法需要把所有子文件夹的大小和所有文件的大小相加。
add(File file) 和 remove(File file) 方法用来向当前文件夹中新增 File(文件或文件夹) 和删除 File。
public class Folder extends File{
private List<File> fileList;
public Folder(String name, int size) {
super(name, size);
fileList = new ArrayList<>();
}
public Folder(String name) {
super(name, 0);
fileList = new ArrayList<>();
}
@Override
public void display() {
//显示当前文件夹的名称
String prex = "";
for(int i = 0;i<super.getHeight();i++){
prex = prex+"--";
}
System.out.println(prex+super.getName());
//循环显示该文件夹下的所有 File (可能是 TxtFile 或 Folder)
for(File file:fileList){
file.display();
}
}
@Override
public int countSize() {
//当前目录下所有文件的大小之和
int sum = 0;
for(File file:fileList){
sum = file.countSize() + sum;
}
return sum;
}
public void add(File file){
file.setHeight(super.getHeight()+1);
fileList.add(file);
}
public void remove(File file){
fileList.remove(file);
}
}
Client.java:测试类。
public class Client {
public static void main(String[] args) {
Folder firstFolder = new Folder("firstFolder");
firstFolder.setHeight(0);
TxtFile firstTxt = new TxtFile("firstTxt",1);
firstFolder.add(firstTxt);
Folder secondFolder = new Folder("secondFolder");
firstFolder.add(secondFolder);
TxtFile secondTxt = new TxtFile("secondTxt",3);
TxtFile thirdTxt = new TxtFile("thirdTxt",4);
secondFolder.add(secondTxt);
secondFolder.add(thirdTxt);
Folder thirdFolder = new Folder("thirdFolder");
firstFolder.add(thirdFolder);
TxtFile fourthTxt = new TxtFile("fourthTxt",6);
thirdFolder.add(fourthTxt);
//显示文件树
firstFolder.display();
//显示firstFolder文件夹所占空间大小
System.out.println("firstFolder所占空间:"+firstFolder.countSize()+"M" );
}
}
运行结果截图:
输出的文件树与我们使用 tree 命令显示的一样。firstFolder 文件夹所占空间也是其中所有的文件大小之和。
在测试类中,我们也可以调用 secondFolder 的 display() 方法来展示它的文件树等。