1. 组合模式
Compose objects into tree structures to represent part-whole hierarchies. Composite lets chilents treat individual objects and compositions of objects uniformly. 将对象组合成树形结构以表示"整体-部分“的层次结构,使得用户对单个对象和组合对象的使用具有一致性. 也称为整体部分模式
1.1 核心思想
- 将整体和部分合二为一, 整合成树形结构, 用同一个对象来表示整体和部分的关系
- 整体和部分操作方式一致, 可进行递归操作
1.2 组合模式类图
- AbsComponent: 节点操作抽象类. 用于声明组件的公用方法和子组件的管理方法, 也可给予默认实现。
- Leaf:叶子节点, 没有子节点. 无须重写子组件管理的相关方法
- Composite: 非叶子节点,重写子组件管理的相关方法
1.3 适用场景
整体与部分关系能抽象为树形结构关系的场景, 如树形菜单, 文件系统管理等.
2. Java 实现组合模式
Linux 系统的文件结构就是一个树状结构,我们可以使用组合模式实现一下.我们首先分析一下linux的文件特点:
- 文件可以抽象为两种节点: 目录节点(非叶子节点)和文件节点(叶子节点).
- 根节点为/, 是目录节点为null的特殊情况
- 任意非根节点, 名称不能为空, 且都拥有一个父目录
- 对于目录节点,可进行子节点管理, 因此需要重写子节点管理相关方法
- 对于文件节点, 不可进行子节点管理
2.1 类图分析
- AbsFile: AbsComponent角色,
- 对于无须子类重写的方法,笔者设置了final修饰符, 防止子类窜改
- 对于需要非叶子节点重写的方法, 笔者给予了默认实现.
- 对于子节点管理API, 笔者采取了链式编程风格进行设计
- MyFile: 叶子节点, 无须重写任何方法.
- MyDirectory: 非叶子节点, 需要实现子节点相关管理节点.
2.2 抽象类-AbsFile
- 笔者将子类不应该重写的方法使用final修饰, 以防止子类篡改实现
- 笔者将子节点管理的相关方法给予了默认实现, 但是抛出了异常. 这样既能保证叶子节点无须重写方法, 也能保证叶子节点不能调用此方法
- 笔者采用链式编程风格设计了子节点管理方法, 这样在使用时更为便捷
public abstract class AbsFile{
private String name;
private AbsFile parent ;
public AbsFile(String name) {
this.name = name;
}
// 子类不能重写的方法
public final void setParent(AbsFile parent) {
this.parent = parent;
}
public final AbsFile getParent() {
return parent;
}
public final String getName() {
return name;
}
public final String getPath(){
String path = "";
// 如果节点父节点存在,则先获取父节点路径
if(getParent() != null) path += getParent().getPath();
// 如果父节点为根节点, 则不拼接"/"
if(!"/".equals(path)) path += "/";
// 如果不是根节点, 则拼接名称. 只允许根节点名称为null
if(this.name != null) path +=name;
return path;
}
// 需要子类实现的方法
public AbsFile add(AbsFile... absFile) {
throw new UnsupportedOperationException();
}
public AbsFile remove(AbsFile... absFile) {
throw new UnsupportedOperationException();
}
public AbsFile getChild(int index) {
throw new UnsupportedOperationException();
}
public List<AbsFile> getChildren() {
throw new UnsupportedOperationException();
}
// 重写hash和equals 方法
@Override
public boolean equals(Object o) {
return Objects.equals(getPath(),((AbsFile)o).getPath());
}
@Override
public int hashCode() {
return Objects.hash(getPath());
}
}
2.3 目录节点-MyDirectory
目录节点需要重新实现抽象类AbsFile中定义的子节点管理方法.
public class MyDirectory extends AbsFile {
// 存储子节点
private List<AbsFile> children = new ArrayList<AbsFile>();
public MyDirectory(String name) {
super(name);
}
@Override
public AbsFile add(AbsFile... absFiles) {
for (AbsFile absFile : absFiles) {
// 判断非根目录节点不能为空
if (null == absFile.getName() || "".equals(absFile.getName().trim())) {
throw new UnsupportedOperationException("不支持非根节点名称为空");
}
// 设置父节点
absFile.setParent(this);
// 添加节点
children.add(absFile);
}
return this;
}
@Override
public AbsFile remove(AbsFile... absFiles) {
for (AbsFile absFile : absFiles) {
children.remove(absFile);
}
return this;
}
@Override
public AbsFile getChild(int index) {
return children.get(index);
}
@Override
public List<AbsFile> getChildren() {
return this.children;
}
}
2.4 文件节点-MyFile
叶子节点, 无须重新实现任何接口.
public class MyFile extends AbsFile{
public MyFile(String name) {
super(name);
}
}
2.5 测试用例
遍历输出节点及其所有子节点信息时,采用递归.
public class TestComposite {
/** 构建目录树 */
private static AbsFile createTree() {
// 创建根目录:/
AbsFile rootDir = new MyDirectory(null);
// 创建家目录:/home
AbsFile homeDir = new MyDirectory("home");
// 创建用户目录: /home/zongf
AbsFile userDir = new MyDirectory("zongf");
// 创建视频目录: /home/zongf/videos
AbsFile videoDir = new MyDirectory("videos");
// 创建文档目录: /home/zongf/documents
AbsFile documentDir = new MyDirectory("documents");
// 创建下载目录: /home/zongf/download
AbsFile downloadDir = new MyDirectory("downloads");
// 添加文件
videoDir.add(new MyFile("video1.avi"))
.add(new MyFile("video2.avi"))
.add(new MyFile("video3.avi"));
documentDir.add(new MyFile("word1.doc"))
.add(new MyFile("word2.doc"))
.add(new MyFile("word3.doc"));
downloadDir.add(new MyFile("word1.doc"))
.add(new MyFile("word2.doc"))
.add(new MyFile("word3.doc"));
// 维护目录关系
userDir.add(videoDir, documentDir, downloadDir);
rootDir.add(homeDir.add(userDir));
return rootDir;
}
// 递归遍历输出文件
private void printAbsFile(AbsFile absFile, String prefix) {
// 输出目录
System.out.println(prefix + absFile.getPath());
// 遍历子节点
if (absFile instanceof MyDirectory) {
for (AbsFile child : absFile.getChildren()) {
printAbsFile(child, " " + prefix);
}
}
}
/** 测试输出节点及其子节点信息 */
@Test
public void test_print(){
AbsFile fileTree = createTree();
printAbsFile(fileTree, "");
}
/** 测试删除 */
@Test
public void test_remove(){
AbsFile fileTree = createTree();
// 获取目录: /home/zongf
AbsFile user = fileTree.getChild(0).getChild(0);
// 获取videos 和 documents
AbsFile videos = user.getChild(0);
AbsFile documents = user.getChild(1);
// 删除videos 和 documents
user.remove(videos, documents);
printAbsFile(user, "");
}
// 测试文件是否相同
@Test
public void testEquals() {
AbsFile dir = new MyDirectory("aaa");
MyFile file = new MyFile("aaa");
Assert.assertEquals(true, dir.equals(file));
// 建立目录关系之后,文件便不再相同
dir.add(file);
Assert.assertEquals(false, dir.equals(file));
}
}