JavaDog程序狗
发布于 2026-11-09 / 1 阅读
0
0

【设计模式】跟着狗哥复习设计模式(八):组合模式

前几天整理博客文件,桌面上散着封面图、UML 图和 Markdown 文件。

一个个文件还好说。碰上文件夹套文件夹,统计大小、打印目录就有点绕了。

组合模式专门处理这种一层套一层的树形结构。

加瓦狗整理电脑里的文件和文件夹

先看一棵目录树

拿“狗哥博客”这个文件夹来说,里面有图片文件夹,也有文章文件。

图片文件夹里还能继续放封面和 UML 图。

狗哥博客文件夹目录树

这里有两种东西:文件和文件夹。

文件已经到头了。文件夹还能装文件,也能再装文件夹。济南话说,就是一层又一层,怪容易绕。

先看关键 UML 图

组合模式的类不多,关键是三者的关系。

组合模式关键 UML 类图

FileSystemNode 是统一接口。

FileItem 表示普通文件,属于叶子节点。Folder 表示文件夹,内部保存一组 FileSystemNode。

所以文件夹里面放文件、放子文件夹,都行。

先把它们叫成同一种节点

文件和文件夹都要能算大小,也都要能显示名称。

public interface FileSystemNode {
    String getName();
    int getSize();
    void show(String indent);
}

接口先保持简单,只有三个操作。

调用方拿到节点以后,不用先判断这是文件还是文件夹。

客户端用同一个接口处理文件和文件夹

这就是组合模式最省心的地方:单个对象和一组对象,可以用同一种方式处理。

普通文件很简单

文件的大小在创建时就确定了,直接返回即可。

public class FileItem implements FileSystemNode {
    private final String name;
    private final int size;

    public FileItem(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public String getName() {
        return name;
    }

    public int getSize() {
        return size;
    }

    public void show(String indent) {
        System.out.println(indent + "- " + name + " (" + size + "MB)");
    }
}

FileItem 不能再添加子节点。走到这里就结束了,所以它叫叶子节点。

文件夹负责装孩子

文件夹自己没有固定大小,它的大小来自里面的内容。

import java.util.ArrayList;
import java.util.List;

public class Folder implements FileSystemNode {
    private final String name;
    private final List<FileSystemNode> children = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    public void add(FileSystemNode node) {
        children.add(node);
    }

    public String getName() {
        return name;
    }
}

children 的类型是 FileSystemNode,因此文件和文件夹都能放进去。

叶子节点和容器节点的区别

有些资料会把 Folder 叫“组合节点”或“容器节点”。名字不同,说的是同一个角色。

大小就让孩子们自己报

文件夹统计大小时,挨个询问子节点,再把结果加起来。

public int getSize() {
    int total = 0;
    for (FileSystemNode child : children) {
        total += child.getSize();
    }
    return total;
}

如果 child 是文件,它直接返回自己的大小。

如果 child 是文件夹,它会继续询问自己的孩子。这样一层层往下走,就是递归。

文件夹递归统计大小的过程

别被“递归”两个字吓着。这里可以理解成:我不知道总数,就问我下面的人;下面的人也照着做。

把目录打印出来

打印目录也是同一个思路。

public void show(String indent) {
    System.out.println(indent + "+ " + name + " (" + getSize() + "MB)");
    for (FileSystemNode child : children) {
        child.show(indent + "  ");
    }
}

每进入一层文件夹,就多两个空格。目录层级看着就清楚了。

Folder project = new Folder("狗哥博客");
Folder images = new Folder("图片");

images.add(new FileItem("封面.png", 3));
images.add(new FileItem("UML图.png", 2));
project.add(images);
project.add(new FileItem("组合模式.md", 1));

project.show("");

运行结果:

+ 狗哥博客 (6MB)
  + 图片 (5MB)
    - 封面.png (3MB)
    - UML图.png (2MB)
  - 组合模式.md (1MB)

代码没有写 if (是文件) 或 if (是文件夹)。

因为每个节点都知道自己该怎么算、该怎么显示。青岛话说,这样写挺恣儿,后面加新目录也不费劲。

啥时候适合用组合模式

看到树形结构时,可以想一下组合模式,但别见到列表就硬套。

什么时候适合使用组合模式

文件系统、菜单、部门组织、评论回复,都有明显的上下级关系。

如果数据只是平铺的一排商品,没有“里面还能继续放同类对象”的关系,普通列表更省事。

狗哥收个尾

组合模式解决的是:怎么用同一种方式处理单个对象和一组对象。

FileItem 是叶子,Folder 是容器,它们都实现 FileSystemNode。文件夹只管保存子节点,再把操作一层层交下去。

这篇先记住目录树就够了。下一篇继续聊 装饰器模式,看看不改原类,怎么给一杯咖啡慢慢加上牛奶和糖。


评论