【设计模式与体系结构】结构型模式-组合模式
简介
组合模式(Composite Pattern)是一种结构型设计模式,它允许将对象组合成树形结构来表现“部分-整体”的关系。当对于用户来说,可以不区分树形结构下子节点和叶子结点时,就可以考虑使用组合模式。
组合模式的角色
- 抽象根节点:定义组合节点和叶子节点的共同接口,包含业务方法(抽象理解为组合节点和叶子节点共有的方法)和管理子节点的方法(抽象理解为组合节点独有的方法)
- 组合节点:表示组合中的分支节点对象,可以包含子节点。包含业务方法和管理子节点的方法
- 叶子节点:表示组合中的叶子节点对象没有子节点。仅包含业务方法
组合模式的类型
- 透明型组合模式:对于叶子节点,也拥有抽象根节点的抽象方法,只不过叶子节点的实现方法只抛出异常。优点:对于客户端而言,可以对子节点和叶子节点进行相同的处理;缺点:方法冗余,且不够安全,可能在运行时出错
- 安全型组合模式:对于子节点和叶子节点实现不同的接口。优点:安全,且减少代码冗余;缺点:在处理构件时,难以对构件类型进行统一处理,因为来源于相同的抽象层,所以只能在运行时进行类型检查,实现较为复杂
组合模式的优点
- 简化客户端逻辑:可以对组合节点和叶子节点进行相同的处理
- 符合开闭原则
- 灵活性强:使用组合模式,可以创建较为复杂的树形结构
组合模式的缺点
- 设计过度一般化:接口统一化,有可能组合节点和叶子节点的逻辑并不是很适合一般化
- 增加系统复杂度
- 性能问题:由于是树形结构,有可能需要进行复杂的遍历操作,可能会消耗大量的时间和空间资源
实践建议
- 安全性与透明性的权衡:安全型组合模式不擅长处理来自相同抽象层的不同构件类型,擅长处理子节点和叶子结点需要区别处理的情况。透明型组合模式与安全型组合模式恰好相反。对于安全型组合模式,可以考虑使用接口隔离原则,来区分组合节点和叶子节点。
- 遍历方式:可以使用迭代器模式进行处理
- 新增节点:对于同一类产品,可以使用工厂方法模式和(或)建造者模式;对于创建一个复杂的(组合)节点的情况,可以使用原型模式(深克隆)
组合模式的使用场景
- 文件系统结构表示:文件系统由目录和文件组成,目录可以包含文件和子目录,这形成了一个典型的树形结构。组合模式可以用于模拟这种结构,使得对文件和目录的操作(如复制、删除、移动等)可以用统一的方式进行处理。
- GUI 组件系统:在 GUI 开发中,窗口、面板和按钮等组件之间存在明显的层次关系。对整个面板进行操作(如设置可见性、调整大小等)时,内部的子组件也会相应地受到影响,就好像它们是一个整体一样。
- 组织结构建模:在企业或机构的组织结构中,存在部门和员工的层次关系。部门可以包含子部门和员工,组合模式可以用于构建这种组织结构模型,方便进行资源分配、权限管理等操作。
正文
文件管理系统-安全型组合模式实践
下面以开发一个文件管理系统为案例,分析安全型组合模式的具体用法。其类图如下:
组合模式是一种处理树形结构的结构型模式,抽象根节点定义的接口包括业务方法和管理子节点的方法两方面的内容。由于安全型组合模式对于组合节点和叶子节点的接口是非透明的(即管理子节点的方法接口对叶子节点不可见),根据接口隔离原则,将文件管理系统的抽象根节点的业务方法和管理子节点的方法定义在不同的接口。
首先罗列文件管理系统的抽象根节点的基本业务需求:
- 新增文件/文件夹
- 删除文件/文件夹
- 移动文件/文件夹
- 显示文件/文件夹内容
其次分析上述基本业务需求:
- 新增文件/文件夹:可以任意创建文件或文件夹,但是无论是文件还是文件夹都是隶属于某个文件夹,因此属于管理子节点的方法接口的内容。而且新增的节点需要知晓其父节点,因此还隐含了设置和获取父节点的接口需求
- 删除文件/文件夹:属于管理子节点的方法接口的内容。但如果删除的是个文件夹,且其文件夹下有文件/文件夹,则也需要一并删除,因此隐含了设置和获取子节点的需求
- 移动文件/文件夹:可以拆解为从原来的父节点中删除该子节点,再将该子节点新增在目标文件夹下。因为文件/文件夹都可以进行移动,因此可以将其归于业务方法。由于文件系统是个树形结构,因此隐含了判断移动后是否会形成环的需求
- 显示文件/文件夹:无论文件还是文件夹,都有显示内容功能,因此属于业务方法。但如果显示的是个文件夹,且其文件夹下有文件/文件夹,则也要一并显示,因此隐含了设置和获取子节点的需求
抽象根节点的管理子节点的方法接口 FileSystemModifiable.java 如下:
public interface FileSystemModifiable { void add(FileSystemMaintainable fileSystemMaintainable);// 添加 boolean remove(FileSystemMaintainable fileSystemMaintainable);// 删除 List<FileSystemMaintainable> getChildren();// 获取子节点 void updateParentsDirectorySize(float changeSize);// 更新父节点的大小 }
抽象根节点的业务方法接口 FileSystemMaintainable.java 如下:
public interface FileSystemMaintainable { boolean move(FileSystemMantainer targetDirectory);// 移动到目标文件夹下 void display(int deep);// 显示信息 void setParents(AbstractDirectory parentsDirectory);//设置父节点 AbstractDirectory getParents();//得到父节点 }
由于 FileSystemMaintainable 接口是所有节点共有的接口,且所有节点也有相同的属性,那么可以维护一个 FileSystemMaintainable 接口的实现类 FileSystemMantainer.java:
public class FileSystemMantainer implements FileSystemMaintainable { protected AbstractDirectory parentsDirectory; private String name; private float size; public String getName() { return name; } public void setName(String name) { this.name = name; } public float getSize() { return size; } public void setSize(float size) { this.size = size; } @Override public boolean move(FileSystemMantainer target) { if (target == null) { System.out.println("移动目标不可为null"); return false; } else if (!(target instanceof AbstractDirectory)) { System.out.println("移动目标不是个文件夹!"); return false; } else if (hasCycle(this, target)) { System.out.println("移动目标是源文件的子文件(夹),无法移动!"); return false; } if (this.getParents() != null) { this.getParents().remove(this); } ((AbstractDirectory) target).add(this); return true; } @Override public void display(int deep) { for (int i = 0; i < deep; ++ i) { System.out.print(" "); } } @Override public void setParents(AbstractDirectory parentsDirectory) { this.parentsDirectory = parentsDirectory; } @Override public AbstractDirectory getParents() throws RuntimeException { return this.parentsDirectory; } // 判断移动后是否会形成环 private boolean hasCycle(FileSystemMantainer source, FileSystemMantainer target) { while (target != null) { if (target == source) { return true; } target = target.getParents(); } return false; } }
文件管理系统中的文件夹视为子节点,文件视为叶子节点,为了更好的扩展性,可以定义一个抽象文件夹类 AbstractDirectory.java 和一个抽象文件类 AbstractFile.java。
抽象文件夹类 AbstractDirectory.java 如下:
public abstract class AbstractDirectory extends FileSystemMantainer implements FileSystemModifiable { protected List<FileSystemMaintainable> list = new ArrayList<FileSystemMaintainable>(); private int number;// the number of files and directories in the directory public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public void add(FileSystemMaintainable fileSystemMaintainable) { if (fileSystemMaintainable == null) { return ; } if (!list.contains(fileSystemMaintainable)) { list.add(fileSystemMaintainable); fileSystemMaintainable.setParents(this); setNumber(list.size()); FileSystemMantainer fileSystemMantainer = (FileSystemMantainer) fileSystemMaintainable; setSize(getSize() + fileSystemMantainer.getSize()); updateParentsDirectorySize(fileSystemMantainer.getSize()); } // else // 拷贝相同文件的就先不写了,具体可以用原型模式进行深克隆 } @Override public boolean remove(FileSystemMaintainable fileSystemMaintainable) { if (list.contains(fileSystemMaintainable)) { list.remove(fileSystemMaintainable); setNumber(list.size()); fileSystemMaintainable.setParents(null); FileSystemMantainer fileSystemMantainer = (FileSystemMantainer) fileSystemMaintainable; setSize(getSize() - fileSystemMantainer.getSize()); updateParentsDirectorySize(- fileSystemMantainer.getSize()); fileSystemMaintainable = null; if (fileSystemMaintainable instanceof AbstractDirectory) { List<FileSystemMaintainable> children = ((AbstractDirectory) fileSystemMaintainable).getChildren(); for (FileSystemMaintainable child: children) { remove(child); } } return true; } return false; } @Override public List<FileSystemMaintainable> getChildren() { if (this instanceof AbstractDirectory) { return list; } return null; } @Override public void updateParentsDirectorySize(float changeSize) { AbstractDirectory parentsDirectory = getParents(); if (parentsDirectory != null) { parentsDirectory.setSize(parentsDirectory.getSize() + changeSize); parentsDirectory.updateParentsDirectorySize(changeSize); } } @Override public void display(int deep) { super.display(deep); System.out.print("+ "); } }
在抽象文件类 AbstractDirectory 中的 remove() 方法,体现到了安全型组合模式的缺点:在处理构件时,难以对构件类型进行统一处理,因为来源于相同的抽象层,所以只能在运行时进行类型检查,实现较为复杂。
值得注意的是,凡是属于安全型组合模式的管理子节点的方法接口,都可能需要在处理来自于同一抽象层的构件的类型检查。
抽象文件类 AbstractFile.java 如下:
public abstract class AbstractFile extends FileSystemMantainer { @Override public void display(int deep) { super.display(deep); System.out.print("> "); } }
文件夹类 Directory.java 如下:
public class Directory extends AbstractDirectory { public Directory(String name, float size) { setName(name); setSize(size); } @Override public void display(int deep) { super.display(deep); System.out.println(getName() + " 文件(夹)数量:" + getNumber() + " 文件夹大小:" + getSize()); ++ deep; for (FileSystemMaintainable it: list) { it.display(deep); } } }
由于文件的类型是多元化的,有图片文件、音频文件和视频文件等等,且图片文件又分为 jpg 格式,png 格式和 gif 格式等等,因此可以定义继承于抽象文件类 AbstractFile 的抽象子类。例如定义继承于抽象文件类 AbstractFile 的抽象子类 ImageFile.java,然后再定义继承于抽象子类 ImageFile 的具体文件类 JPGFile.java 和 PNGFile.java。
抽象图片类 ImageFile.java 如下:
public abstract class ImageFile extends AbstractFile { private String type;// the type of image file public String getType() { return type; } public void setType(String type) { this.type = type; } }
具体图片类 JPGFile.java 如下:
public class JPGFile extends ImageFile { public JPGFile(String name, float size) { setName(name); setSize(size); setType(".jpg"); } @Override public void display(int deep) { super.display(deep); System.out.println("图片名:" + getName() + getType() + " 文件大小:" + getSize()); } }
具体图片类 PNGFile.java 如下:
public class PNGFile extends ImageFile { public PNGFile(String name, float size) { setName(name); setSize(size); setType(".png"); } @Override public void display(int deep) { super.display(deep); System.out.println("图片名:" + getName() + getType() + " 文件大小:" + getSize()); } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 【.NET】调用本地 Deepseek 模型