组合模式(Composite Pattern)是一种结构型设计模式。在组合模式中,每个对象都有相同的接口,这使得客户端不需要知道对象的具体类型,而只需要调用对象的通用接口即可。
组合模式会将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者)可以统一单个对象和组合对象的处理逻辑。
组合模式包含以下几个角色:
下面是一个简单的组合模式示例代码,用于表示文件系统中的文件和文件夹。
Component(抽象构件),定义文件系统的抽象接口
public interface FileSystem {
/**
* 展示文件
*/
void display();
}
Leaf(叶子节点),抽象构建的具体实现文件类,它没有子节点。
@AllArgsConstructor
@NoArgsConstructor
@Data
@Slf4j
public class File implements FileSystem {
private String fileName;
private Long size;
@Override
public void display() {
log.info("fileName:{}, size:{}", fileName, size);
}
}
Composite组合节点,抽象构建的具体实现文件夹类,它用来组合其他实现对象,它允许有子节点。
@Slf4j
public class Folder implements FileSystem {
private String fileName;
private List<FileSystem> children;
public Folder(String fileName) {
this.fileName = fileName;
this.children = new ArrayList<>();
}
public void addFileSystem(FileSystem fileSystem) {
this.children.add(fileSystem);
}
@Override
public void display() {
log.info("folder[{}]", fileName);
for (FileSystem fileSystem : children) {
fileSystem.display();
}
}
}
客户端使用组合对象的示例代码:
/**
* 类描述:组合设计模式测试案例
*
* @Author crysw
* @Version 1.0
* @Date 2023/12/24 16:35
*/
@Slf4j
public class CompositePatternTest {
@Test
public void test() {
// folder1包含file1,file2
FileSystem file1 = new File("file1.txt", 1L);
FileSystem file2 = new File("file2.txt", 2L);
Folder folder1 = new Folder("folder1");
folder1.addFileSystem(file1);
folder1.addFileSystem(file2);
// folder2包含folder1,file3,file4
FileSystem file3 = new File("file3.txt", 3L);
FileSystem file4 = new File("file4.txt", 4L);
Folder folder2 = new Folder("folder2");
folder2.addFileSystem(file3);
folder2.addFileSystem(file4);
folder2.addFileSystem(folder1);
folder2.display();
// 面向FileSystem编程
}
}
在这个示例中:
FileSystem 是抽象构件,它定义了组合对象的通用接口方法 display。
File 是叶子节点,表示文件,它实现了 FileSystem 接口,并在 display 方法中输出文件名。
Folder 是组合节点,表示文件夹,它实现了 FileSystem 接口,并维护了一个子节点列表 children,可以添加和删除子节点。在 display 方法中,它首先输出文件夹名,然后依次调用子节点的 display 方法输出子节点信息。
在客户端(测试案例)代码中,创建了一些文件和文件夹,然后将它们组合成一个树形结构;最后调用根节点( folder2)的 display 方法输出了整个文件系统的信息。这样就可以通过组合模式,使用相同的方式来处理单个文件和整个文件系统。
组合模式的优点:
组合模式的缺点:
将文件目录的案例做一个升级,如何设计实现支持递归遍历的文件系统
目录树结构?设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:
把文件和目录统一用FileSystemNode 类来表示,并且通过 isFile 属性来区分.
/**
* 类描述:文件系统类(用来表示文件,目录)
*
* @Author crysw
* @Version 1.0
* @Date 2023/12/24 22:18
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class FileSystemNode {
private String path;
private boolean isFile;
private List<FileSystemNode> subNodes = new ArrayList<>();
/**
* 统计文件个数
*
* @return
*/
public int countNumOfFiles() {
if (isFile) {
return 1;
}
int numOfFiles = 0;
for (FileSystemNode fileOrDir : subNodes) {
numOfFiles += fileOrDir.countNumOfFiles();
}
return numOfFiles;
}
/**
* 统计文件大小
*
* @return
*/
public long countSizeOfFiles() {
if (isFile) {
File file = new File(path);
if (!file.exists()) {
return 0;
} else {
return file.length();
}
} else {
long fileSize = 0;
for (FileSystemNode subNode : subNodes) {
fileSize += subNode.countSizeOfFiles();
}
return fileSize;
}
}
/**
* 添加子目录或文件
*
* @param fileOrDir
*/
public void addSubNode(FileSystemNode fileOrDir) {
subNodes.add(fileOrDir);
}
/**
* 删除子目录或文件
*
* @param fileOrDir
*/
public void removeSubNode(FileSystemNode fileOrDir) {
int size = subNodes.size();
for (int i = 0; i < size; ++i) {
if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
subNodes.remove(i);
i--;
}
}
}
}
单纯从功能实现角度看,上面的代码没有问题,已经实现了我们想要的功能。但如果我们开发的是一个大型系统,从扩展性(文件或目录可能会对应不同的操作)、业务建模(文件和目录从业务上是两个概念)、代码的可读性(文件和目录区分对待更加符合人们对业务的认知)的角度来说,我们最好对文件和目录进行区分设计,定义为 File 和 Directory 两个类。
按照这个设计思路,我们对代码进行拆分重构:
@NoArgsConstructor
@AllArgsConstructor
@Data
public abstract class FileSystemNode2 {
protected String path;
public abstract int countNumOfFiles();
public abstract long countSizeOfFiles();
}
表示文件的类, 继承FileSystemNode2,重写抽象方法
public class File2 extends FileSystemNode2 {
public File2(String path) {
super(path);
}
/**
* 统计文件数量
*
* @return
*/
@Override
public int countNumOfFiles() {
return 1;
}
/**
* 统计文件大小
*
* @return
*/
@Override
public long countSizeOfFiles() {
File file = new File(path);
if (!file.exists()) {
return 0;
}
return file.length();
}
}
表示目录的类, 继承FileSystemNode2,重写抽象方法。且添加自己的成员方法添加,删除子目录或文件。
public class Directory2 extends FileSystemNode2 {
private List<FileSystemNode2> subNodes = new ArrayList<>();
public Directory2(String path) {
super(path);
}
/**
* 统计目录下的文件数量
*
* @return
*/
@Override
public int countNumOfFiles() {
int numOfFiles = 0;
for (FileSystemNode2 subNode : subNodes) {
numOfFiles += subNode.countNumOfFiles();
}
return numOfFiles;
}
/**
* 统计目录下的文件大小
*
* @return
*/
@Override
public long countSizeOfFiles() {
long fileSize = 0;
for (FileSystemNode2 subNode : subNodes) {
fileSize += subNode.countSizeOfFiles();
}
return fileSize;
}
/**
* 添加子目录或文件
*
* @param fileOrDir
*/
public void addSubNode(FileSystemNode2 fileOrDir) {
subNodes.add(fileOrDir);
}
/**
* 删除子目录或文件
*
* @param fileOrDir
*/
public void removeSubNode(FileSystemNode2 fileOrDir) {
int size = subNodes.size();
for (int i = 0; i < size; ++i) {
if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
subNodes.remove(i);
i--;
}
}
}
}
测试案例:
@Slf4j
public class CompositePatternTest {
@Test
public void test2() {
// 根目录
Directory2 fileSystemTree = new Directory2("/");
// 创建目录 dir dir2
Directory2 dir = new Directory2("/dir/");
Directory2 dir2 = new Directory2("/dir2/");
fileSystemTree.addSubNode(dir);
fileSystemTree.addSubNode(dir2);
// 创建文件
File2 aFile = new File2("/dir/a.txt");
File2 bFile = new File2("/dir/b.txt");
dir.addSubNode(aFile);
dir.addSubNode(bFile);
// 创建子目录
Directory2 moviesDir = new Directory2("/dir/movies");
File2 movieFile = new File2("/dir/movies/c.avi");
moviesDir.addSubNode(movieFile);
dir.addSubNode(moviesDir);
log.info("files number:{}", fileSystemTree.countNumOfFiles());
log.info("files size: {}", fileSystemTree.countSizeOfFiles());
}
}
这种组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。
假设我们在开发一个 OA 系统。公司的组织结构包含部门和员工两种数据类型。其中,部门又可以包含子部门和员工。
@NoArgsConstructor
@AllArgsConstructor
@Data
public abstract class HumanResource {
protected long id;
protected double salary;
public HumanResource(long id) {
this.id = id;
}
/**
* 计算薪资
*
* @return
*/
public abstract double calculateSalary();
}
// 员工
@ToString
@EqualsAndHashCode
public class Employee extends HumanResource {
public Employee(long id, double salary) {
super(id);
this.salary = salary;
}
@Override
public double calculateSalary() {
return salary;
}
}
// 部门
@ToString
@EqualsAndHashCode
public class Department extends HumanResource {
private List<HumanResource> subNodes = new ArrayList<>();
public Department(long id) {
super(id);
}
/**
* 添加部门或员工
*
* @param humanResource
*/
public void addSubNode(HumanResource humanResource) {
subNodes.add(humanResource);
}
/**
* 计算部门的总薪资
*
* @return
*/
@Override
public double calculateSalary() {
double totalSalary = 0;
for (HumanResource subNode : subNodes) {
totalSalary += subNode.getSalary();
}
this.salary = totalSalary;
return totalSalary;
}
}
HumanResource 是部门类(Department)和员工类(Employee)抽象出来的父类,为的是能统一薪资的处理逻辑。
将一组对象(员工和部门)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(部门与子部门的嵌套结构)。组合模式让客户端可以统一单个对象(员工)和组合对象(部门)的处理逻辑(递归遍历)。
组合模式在 JDK 源码中也有很多应用。以下是一些常见的使用场景:
在 SSM(Spring + Spring MVC + MyBatis)框架中,组合模式也有一些应用场景,以下是一些常见的使用场景: