组合设计模式

6. 组合设计模式

6.1 原理与实现

组合模式(Composite Pattern)是一种结构型设计模式。在组合模式中,每个对象都有相同的接口,这使得客户端不需要知道对象的具体类型,而只需要调用对象的通用接口即可。

组合模式会将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者)可以统一单个对象和组合对象的处理逻辑。

组合模式包含以下几个角色:

  • Component(抽象构件):定义组合对象的通用接口,可以包含其他组合对象或叶子对象。
  • Leaf(叶子节点):表示组合对象中的叶子节点,它没有子节点。
  • Composite(组合节点):表示组合对象中的组合节点,它可以包含其他组合对象或子节点对象。

下面是一个简单的组合模式示例代码,用于表示文件系统中的文件和文件夹。

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 方法输出了整个文件系统的信息。这样就可以通过组合模式,使用相同的方式来处理单个文件和整个文件系统。

组合模式的优点

  • 可以使用相同的方式来处理单个对象和组合对象,客户端无需知道对象的具体类型。
  • 可以方便地增加新的组合对象或叶子对象,同时也可以方便地对组合对象进行遍历和操作。
  • 可以使代码更加简洁和易于维护,因为使用组合模式可以避免大量的 if-else 或switch-case 语句。

组合模式的缺点

  • 可能会包含大量的叶子对象,这可能会导致系统的性能下降。
  • 可能会使设计过于抽象化,使得代码难以理解和维护。

将文件目录的案例做一个升级,如何设计实现支持递归遍历的文件系统目录树结构?设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:

  • 动态地添加、删除某个目录下的子目录或文件;
  • 统计指定目录下的文件个数;
  • 统计指定目录下的文件总大小。

把文件和目录统一用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());
    }
}

这种组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。

6.2 应用场景

假设我们在开发一个 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)抽象出来的父类,为的是能统一薪资的处理逻辑。

将一组对象(员工和部门)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(部门与子部门的嵌套结构)。组合模式让客户端可以统一单个对象(员工)和组合对象(部门)的处理逻辑(递归遍历)。

6.3 源码应用

6.3.1 JDK源码

组合模式在 JDK 源码中也有很多应用。以下是一些常见的使用场景:

  • Java Collection 框架:在 Java Collection 框架中,Collection 接口就是一个抽象构件,它定义了集合对象的通用接口。List、Set 和 Map 等具体集合类就是组合节点或叶子节点,用于存储和操作集合中的元素。
  • Servlet API:在 Servlet API 中,ServletRequest 和 ServletResponse 接口就是一个抽象构件,它定义了 Servlet 的通用接口。HttpServletRequest 和HttpServletResponse 等具体类就是组合节点或叶子节点,用于处理 Web 请求和响应。

6.3.2 SSM源码

在 SSM(Spring + Spring MVC + MyBatis)框架中,组合模式也有一些应用场景,以下是一些常见的使用场景:

  • Spring MVC:在 Spring MVC 中,Controller 就是一个组合节点,它可以包含其他组合对象或叶子对象,用于处理 Web 请求和响应。对于复杂的请求处理逻辑,可以将一个 Controller 分解成多个子 Controller,然后通过组合的方式将它们组合起来,使得请求处理逻辑更加清晰和易于维护。
  • MyBatis:在 MyBatis 中,SqlNode 就是一个抽象构件,它定义了 SQL 节点的通用接口。WhereSqlNode、ChooseSqlNode、IfSqlNode 等具体类就是组合节点或叶子节点,用于构建 SQL 语句,解析动态sql。

你可能感兴趣的:(设计模式,设计模式)