在计算机文件系统中,文件夹是一种特殊的文件,它可以放入文件,也可以放入其他文件夹,即子文件夹。在子文件夹中,也可以继续嵌套文件夹,文件夹形成了一种树形的结构。在软件开发中,我们通常使用组合模式来进行这种整体-部分结构的管理。
组合模式
组合模式(Composite):组合模式又被称为整体-部分(Part-Whole)模式,属于结构型模式。它能够将对象组合成为具有树状层次结构,能够使容器(树枝)和内容(叶子)保持一致,便于开发人员进行管理,调用。
组合模式经常用在操作树状数据的场景中。我们称树顶层节点为根节点,根节点下可以包含树枝节点和叶子节点,并且支持循环嵌套,即树枝节点下又可以包含树枝节点和叶子节点。(如:操作系统的文件系统就是一种嵌套递归的树状结构,可以考虑使用组合模式来实现。)
组合模式的结构
组合模式由以下结构实现:
- 叶子(Leaf):表示内容的类,叶子中不能放入其他对象。例如操作系统中的文件不能放入文件夹。
- 树枝(Composite):组合模式中表示容器的对象,可以在树枝下放入叶子和树枝。例如操作系统中的文件夹,既可以放入文件又可以放入子文件夹。
- 抽象组件(Component):使容器和内容具有一致性的对象,它为叶子和树枝定义了公共接口。
实际应用场景
- 文件系统:无论是在操作系统的文件系统中还是在其他例如网盘等文件系统,都需要大量使用递归嵌套的树状结构。这个时候就特别适合使用组合模式进行文件的管理。
- 决策树的定制:例如推荐系统需要根据人群,性别,年龄,标签多个维度来进行决策推荐,就可以使用组合模式来产生决策树进行精准推荐,而不需要使用大量的
if else
语句来判断所需要的选择的分支。
组合模式的两种形式
组合模式分为以下两种形式:
- 透明组合模式
- 安全组合模式
这两种实现形式的区别仅在于提供操作的方法由哪个类进行管理。树的操作方法通常有add()
remove()
getChildren()
等。在透明组合模式中,直接由组件(Component)进行操作方法的管理。在安全组合模式中,操作方法由树枝(Composite)进行管理。
下文我们将以实际例子讲解为什么需要这两种不同模式以及这两种实现的形式。
示例
我们来模拟操作系统的文件系统结构,实现一个最简单的文件系统。这个文件系统能够添加文件夹和文件,并且文件夹中能够包含n个子文件夹。使用组合模式完成。
透明模式
首先,我们创建一个抽象组件(Component):
TreeComponent.java
package com.yeliheng.composite;
import java.util.List;
/**
* 抽象组件:用于保持树枝和叶子的一致性
*/
public interface TreeComponent {
TreeComponent add(TreeComponent node);
List<TreeComponent> getChildren();
void remove(TreeComponent node);
}
这个抽象组件中,我们提供了三个方法,分别是add()
添加,getChildren()
获取所有子节点,remove()
删除方法。待会创建的文件夹和文件都将实现抽象组件中提供的方法。这里,你可能会注意到一个问题。如果在文件中继续添加文件夹或者文件,那会出现什么情况?没错,这就是透明模式的瓶颈所在。透明模式直接将方法定义在了抽象组件中,导致实现它的所有对象都必须实现其提供的方法。在我们定义的规则下,不允许文件中存在子文件夹或者文件,所以实现方法时就必须抛出异常。
接下来,我们创建文件夹对象。文件夹对象属于我们的树枝节点。它下面可以有无数个子节点。
Directory.java
package com.yeliheng.composite;
import java.util.ArrayList;
import java.util.List;
/**
* 树枝节点:文件夹
*/
public class Directory implements TreeComponent{
//文件夹名
private String name;
//列表
private List<TreeComponent> list = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
@Override
public TreeComponent add(TreeComponent node) {
list.add(node);
return this;
}
@Override
public List<TreeComponent> getChildren() {
return list;
}
@Override
public void remove(TreeComponent node) {
list.remove(node);
}
}
我们使用List来管理我们的树形数据。
接着进行文件的实现
File.java
package com.yeliheng.composite;
import java.util.List;
/**
* 叶子节点:文件
*/
public class File implements TreeComponent{
//文件名
private String filename;
public File(String filename) {
this.filename = filename;
}
@Override
public TreeComponent add(TreeComponent node) {
//文件下不能新建文件或文件夹
throw new UnsupportedOperationException();
}
@Override
public List<TreeComponent> getChildren() {
return null;
}
@Override
public void remove(TreeComponent node) {
//文件下没有东西可供删除
throw new UnsupportedOperationException();
}
}
在文件对象中,若试图调用add()
方法或remove()
方法将造成异常的抛出。
最后,我们在main函数中构建这棵文件树,并使用IDEA的Debug工具来查看我们构建好的树形结构。
Main.java
package com.yeliheng.composite;
public class Main {
public static void main(String[] args) {
TreeComponent rootElement = new Directory("文件夹1");
rootElement.add(new Directory("子文件夹1")
.add(new File("文件1"))
.add(new File("文件2"))
);
rootElement.add(new Directory("子文件夹2"));
System.out.println(rootElement);
}
}
创建好树形数据后,我们将断点设置在System.out.println(rootElement)
处,查看rootElement变量的值。
可以看到,这棵树构建正确。
当我们试图对文件对象进行add操作时,异常抛出。如下图所示:
接下来,我们使用安全模式对程序进行修改。
安全模式
实现安全组合模式只需要将定义在Component对象中的方法移动到树枝节点中即可防止子节点的错误调用。
在本例中,我们移动TreeComponent类中的add()
方法和remove()
方法到Directory中,这样就能防止File对add()
和remove()
进行调用。
详细代码如下:
TreeComponent.java
package com.yeliheng.composite;
import java.util.List;
/**
* 抽象组件:用于保持树枝和叶子的一致性
*/
public interface TreeComponent {
List<TreeComponent> getChildren();
}
Directory.java
package com.yeliheng.composite;
import java.util.ArrayList;
import java.util.List;
/**
* 树枝节点:文件夹
*/
public class Directory implements TreeComponent{
//文件夹名
private String name;
//列表
private List<TreeComponent> list = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public TreeComponent add(TreeComponent node) {
list.add(node);
return this;
}
@Override
public List<TreeComponent> getChildren() {
return list;
}
public void remove(TreeComponent node) {
list.remove(node);
}
}
File.java
package com.yeliheng.composite;
import java.util.List;
/**
* 叶子节点:文件
*/
public class File implements TreeComponent{
//文件名
private String filename;
public File(String filename) {
this.filename = filename;
}
@Override
public List<TreeComponent> getChildren() {
return null;
}
}
这样就实现了安全组合模式。
组合模式的优缺点
优点
- 使用组合模式能够使调用者一致地处理容器和对象,无需关心“树枝节点”和“叶子节点”的区别。
- 满足开闭原则,可以在不修改源代码的情况下进行新对象的操作。
缺点
- 过度使用会增加软件的层级关系,提高系统复杂度。
总结
本文以操作系统的文件系统为例,讲解了组合模式的两种实现形式:透明组合模式、安全组合模式。
本文示例的完整源代码参见:Github