「设计模式」🌲组合模式(Composite)

1,914 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。

模式动机

众所周知,在 Windows 文件资源管理器中可以划分为 文件 与 文件夹 两种对象。一个文件夹中可以包含多个子文件或多个子文件夹,其子文件夹还可以再包含一些子文件与子文件夹,以此类推...

如果你想计算一个文件夹所占磁盘空间大小,只需 ”右键—>属性—>大小“,该文件夹的所占磁盘空间总大小为多少一目了然:

但这是 Windows 资源管理器给你提供的便捷操作,如果让你自己用程序实现:计算每个磁盘(C 盘、D 盘、E 盘...)下所占磁盘空间大小,你会如何实现呢?

文件 和 文件夹 将作为两类对象

磁盘中可能有各种文件,这些文件放置在文件夹中,然后又被放入一层又一层更大的文件夹中,使得整个结构看上去像是一棵倒过来的对象树🌳

你可以尝试直接计算:打开所有的文件夹,找到其中每个文件,获取其大小,然后累加获得总大小。这在真实世界或许可行,但在程序中,你不能简单使用循环语句来完成该工作,因为 文件 与 文件夹 是两种不同的类,而且你必须事先知道所有文件夹的嵌套层数以及其他细枝末节的信息,因此通过这种想法计算极其不方便,甚至说不可行。

那有没有一种灵活的方式来处理这类情况?有的!

组合模式可以很好地对其进行设计,我们可以使用一个通用接口来与 文件 和 文件夹 进行交互,并且在该接口中声明一个计算大小的方法:

  • 对于一个文件,该方法直接返回其所占磁盘空间大小
  • 对于一个文件夹,该方法遍历文件夹中所有项,询问每项的大小(如果某项是子文件夹,则该子文件夹也会继续遍历其中所有项,以此类推),然后返回该文件夹的所占总磁盘空间大小

组合模式以递归方式处理对象树中所有项

⭐该方式的最大优点就在于你无需了解树状结构中对象的具体类,你也无需了解对象是简单的文件还是复杂的文件夹。你只需要调用通用接口中的方法使其以相同的方式进行处理即可,调用后,对象会将请求沿着树结构传递下去...

定义

组合模式又称为整体-部分模式,属于对象结构型模式。

组合模式能够将对象组合成树状结构,并且能像使用独立对象一样使用它们,即组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用一致。

UML 类图

模式结构

组合模式包含如下角色:

  • Component:组件接口描述了树中简单项和复杂项所共有的操作。
  • Leaf:叶子节点是树的基本结构,它不包含子项目。
  • Container:容器 (Container) 又名组合 (Composite),是包含叶子节点或其他容器的单位;容器不知道子项目所属的具体类,它只能通过通用的组件接口与其子项目进行交互。
  • Client:客户端通过组件接口与所有项目进行交互。

更多实例

下图使用透明组合模式来复现 ”模式动机“ 部分中阐述的文件系统;而上图中使用的是安全组合模式(这两种模式的区别请见下文)。

示例代码

Component.java

public abstract class Component {
    public abstract void operation();
}

Leaf.java

public class Leaf extends Component {
    @Override
    public void operation() {
        // TODO
    }
}

Container.java

public class Container extends Component {
    private List<Component> children = new ArrayList<>();

    public void add(Component component) {
        children.add(component);
    }

    public void remove(Component component) {
        children.remove(component);
    }

    public Component getChild(int index) {
        try {
            return children.get(index);
        } catch (IndexOutOfBoundsException e) {
            throw new ArrayIndexOutOfBoundsException("No such child: " + index);
        }
    }

    @Override
    public void operation() {
        for (Component child : children) {
            child.operation();
        }
    }
}

优缺点

✔可以利用多态和递归机制更方便地使用复杂对象树结构。

✔新增新元素无需修改现有代码,就可以使其成为对象树的一部分,遵循”开闭原则“。

✔客户端可以一致地使用组合结构或单个对象,调用简单。

❌对于功能差异较大的类,提供通用接口有些困难。有可能你需要过度一般化组件接口,结果就是过于抽象使得难以理解。

适用场景

在以下情况推荐使用组合模式:

(1)客户端希望以相同方式处理简单元素和复杂元素,针对抽象编程,而无需关系对象层次结构细节。

(2)组合多个对象形成树形结构以表示具有 “整体—部分” 关系的层次结构,但又希望一种方式忽略整体与部分的差异。

「组合模式」落地

操作系统中的目录结构

”模式动机“ 部分就是一个很好的例子。

操作系统中的目录结构是一个树形结构,因此在对文件和文件夹进行操作时可以应用组合模式。

Swing / AWT

JDK 的 Swing / AWT 是组合模式在 Java 类库中的一个典型实际应用。

我们先来看一个简单的 AWT 实例(好在大一时有稍微研究过 Java Swing/AWT🤣)

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class MyFrame extends Frame {

    public MyFrame(String title) {
        super(title);
    }

    public static void main(String[] args) {
        MyFrame frame = new MyFrame("This is a Frame");

        // 定义 3 个构件,添加到 Frame 中
        Button button = new Button("Button A");
        Label label = new Label("This is an AWT Label~");
        TextField textField = new TextField("This is an AWT TextField~");

        frame.add(button, BorderLayout.EAST);
        frame.add(label, BorderLayout.SOUTH);
        frame.add(textField, BorderLayout.NORTH);

        // 定义一个 Panel,在 Panel 中添加 3 个构件,然后再把 Panel 添加到 Frame 中
        Panel panel = new Panel();
        panel.setBackground(Color.pink);

        Label lable1 = new Label("Username");
        TextField textField1 = new TextField("Input your account:", 20);
        Button button1 = new Button("Confirm");
        panel.add(lable1);
        panel.add(textField1);
        panel.add(button1);

        frame.add(panel, BorderLayout.CENTER);

        // 设置 Frame 的属性
        frame.setSize(500, 300);
        frame.setBackground(Color.orange);
        // 设置点击关闭事件
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        frame.setVisible(true);
    }
}

渲染效果:

我们在 Frame 容器中添加了三个不同的构件 Button、Label、TextField,还添加了一个 Panel 面板容器,Panel 容器中又添加了 Button、Label、TextField 三个构件。为什么容器 Frame 和 Panel 可以添加类型不同的构件和容器呢?

不急,先上 AWT Component (简易) 类图:

GUI 组件根据作用分为两种:基本组件和容器组件。

  • 基本组件又称构件,诸如 Button、Label、TextField 之类的图形界面元素。
  • 容器是一种比较特殊的组件,可以容纳其他基本组件(按钮、文本框)、容器(窗口、对话框);所有容器类都是 java.awt.Container 的直接或间接子类。

这么一说之后,是不是就会意识到原来 Java GUI(Swing / AWT) 就是组合模式的一种典型应用,以上类图没有画全,不过你大概也能猜想到,它趋向于复杂的组合模式。

⭐父类容器 Container 部分源码如下:

⭐基本组件的继承关系:

从中可以看出 Container 父类容器中定义了一个用于存储 Component 对象的集合,而容器组件 Container 和基本组件如 Button、Label、TextField 等都是 Component 的子类。

而 Component 类中封装了组件通用的方法和属性,如下:

getComponentAt(int x, int y)
getFont()
getForeground()
getName()
getSize()
paint(Graphics g)
repaint()
update()
setVisible(boolean b)
setSize(Dimension d)
setName(String name)

因此许多组件类也就继承了 Component 类的成员方法和成员变量。

总而言之,Java GUI 应用了组合模式!

模式扩展

组合模式可分为透明组合模式和安全组合模式,当然可能还有一种复杂组合模式(再抽象一层)。

更为复杂的组合模式

假设 Leaf 为 File,那么 File 可有多种文件类型作为其子类,Composite 同理。这就衍生出了如下更为复杂的组合模式。

透明组合模式

在透明组合模式中,由于抽象构件 Component 声明了所有子类中的全部方法,所以客户端无须区别 Leaf 对象和 Composite 对象,对客户端而言是透明的。

但其缺点是:树叶构件 Leaf 本身没有 add()、remove() 及 getChild() 方法,却要实现它们(空实现 / 抛出异常),这样会带来一些安全性问题。

安全组合模式

与透明组合模式对应的是安全组合模式,该方式将管理子构件的方法移动到 Composite 容器构件中,抽象构件 Component 和树叶构件 Leaf 没有管理子构件的方法,这样就避免了透明组合模式的安全性问题。

但由于 Leaf 与 Composite 构件存在不同的接口(add、remove、getChild),客户端在调用时需要知道树叶对象和容器对象的存在,所以失去了透明性。

⭐一般情况下,更推荐使用透明组合模式,它完全发挥了组合模式的优点。

最后

👆上一篇:「设计模式」🌉桥接模式(Bridge)

👇下一篇:「设计模式」🏳️‍🌈代理模式(Proxy)

❤️ 好的代码无需解释,关注「手撕设计模式」专栏,跟我一起学习设计模式,你的代码也能像诗一样优雅!

❤️ / END / 如果本文对你有帮助,点个「赞」支持下吧,你的支持就是我最大的动力!