设计模式之组合模式

本文中涉及到的完整代码存放于以下 GitHub 仓库中LearningCode

1. 理论部分

组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。

组合模式又称为部分-整体模式(Part-Whole Pattern)

1.1 结构与实现

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

  • Compoent(抽象构件)
    • 职责:为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。
    • 实现:通常声明为抽象类。
  • Leaf(叶子构件)
    • 职责:实现了 Compoent 中声明的接口,定义与叶子构件有关的行为,在组合结构中表示叶子结点对象。
    • 实现:通常声明为具体类。
  • Composite(容器构件)
    • 职责:实现了 Compoent 中声明的接口,定义与容器构件有关的行为,在组合结构中表示容器结点对象。
    • 实现:通常声明为具体类。

组合模式可以分为两类:

  • 透明组合模式
  • 安全组合模式

1.1.1 透明组合模式

在透明组合模式中,Component 一般会声明以下方法:

  • 访问构件 ------ operation()
  • 增加子构件 ------ add(Component c)
  • 删除子构件 ------ remove(Component c)
  • 获取子构件 ------ getChlid(int i)

Leaf 构件继承 Component ,并在实现访问和操作子构件的方法时,一般采用抛出异常的方式,来告诉调用者 Leaf 构件调用该方法的这一行为是错误的。Composite 继承 Component ,并在内部还会维护一个子构件的集合。其 UML 类图如下所示:

在透明组合模式中,Component 声明了所有用于管理成员对象的方法:

  • 好处:是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以一致地对待所有的对象。
  • 缺点:不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一个层次的对象,因此为其提供操作和访问子构件的方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)。

可以添加 isComposite 方法来辅助对子构件访问或操作。

1.1.2 安全组合模式

在安全组合模式中,访问或操作子构件的方法不在 Component 中声明,而在 Composite 中声明。其 UML 类图如下所示:

在安全组合模式中,Component 没有声明任何用于管理子构件的方法,而是在 Composite 类中声明并实现这些方法:

  • 好处:这种做法是安全的,因为根本不需要向叶子对象提供这些管理成员对象的方法,对于叶子对象,客户端不可能调用到这些方法。
  • 缺点:不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。

1.1.3 更多实现细节

  • 保持从子构件到父构件的引用能简化组合结构的遍历和管理。通常在 Compoent 类中定义父构件引用,Leaf 和 Composite 类可以继承这个引用以及管理这个引用的那些操作。
  • 共享构件可以减少对存储的需求。共享构件的实现有两种解决办法:
    • 为子部件存储多个父构件,但当一个请求在结构中向上传递时,这种方式会导致多义性。
    • 与享元模式一同使用,将指向父构件的引用作为外部状态,将其它信息作为内部状态。
  • 如果需要对子构件进行频繁地查找,应当使用缓存改善其性能。
  • 在没有垃圾回收机制的语言中,当一个 Composite 被销毁时,最好由 Composite 负责删除其子结点,共享 Leaf 除外。
  • Composite 应当基于自身选择合适的数据结构来存储它们的子结点。

1.2 优缺点与适用场景

组合模式具有以下优点:

  • 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  • 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合开闭原则。

组合模式存在以下缺点:在增加新构件时很难对容器中的构件类型进行限制。有时候希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,在使用组合模式时不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。

组合模式适用于以下场景:

  • 向表示对象的部分-整体层次结构。
  • 希望客户端忽略组合对象和单个对象的不同,客户端将统一地使用组合结构中的所有对象。

2. 实现部分

以 Java 代码为例,演示组合模式的实现。

案例介绍:

2.1 透明组合模式

定义抽象构件 ------ AbstractFile

java 复制代码
public abstract class AbstractFile {
    public abstract void add(AbstractFile file);
    public abstract void remove(AbstractFile file);
    public abstract AbstractFile getChild(int i);
    public abstract void killVirus();
}

定义叶子构件,以VideoFile为例

java 复制代码
public class VideoFile extends AbstractFile{
    private String name;

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

    @Override
    public void add(AbstractFile file) {
        System.out.println("对不起,不支持该方法!");
    }

    @Override
    public void remove(AbstractFile file) {
        System.out.println("对不起,不支持该方法!");
    }

    @Override
    public AbstractFile getChild(int i) {
        System.out.println("对不起,不支持该方法!");
        return null;
    }

    @Override
    public void killVirus() {
        System.out.println("---- 对视频文件'" + name + "'进行杀毒");
    }
}

定义容器构件------Folder

java 复制代码
public class Folder extends AbstractFile{
    private final ArrayList<AbstractFile> fileList = new ArrayList<>();
    private String name;

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

    @Override
    public void add(AbstractFile file) {
        fileList.add(file);
    }

    @Override
    public void remove(AbstractFile file) {
        fileList.remove(file);
    }

    @Override
    public AbstractFile getChild(int i) {
        return fileList.get(i);
    }

    @Override
    public void killVirus() {
        System.out.println("****对文件夹'" + name + "'进行杀毒");
        for (AbstractFile file : fileList) {
            file.killVirus();
        }
    }
}

客户端调用

java 复制代码
public class Main {
    public static void main(String[] args) {
        AbstractFile folder1 = new Folder("Sunny 的资料");
        AbstractFile folder2 = new Folder("图像文件");
        AbstractFile folder3 = new Folder("文本文件");
        AbstractFile folder4 = new Folder("视频文件");

        AbstractFile file1 = new ImageFile("小龙女.jpg");
        AbstractFile file2 = new ImageFile("张无忌.gif");
        AbstractFile file3 = new ImageFile("九阴真经.txt");
        AbstractFile file4 = new ImageFile("葵花宝典.doc");
        AbstractFile file5 = new ImageFile("笑傲江湖.rmvb");

        folder2.add(file1);
        folder2.add(file2);
        folder3.add(file3);
        folder3.add(file4);
        folder4.add(file5);
        folder1.add(folder2);
        folder1.add(folder3);
        folder1.add(folder4);

        folder1.killVirus();
    }
}

完整的 UML 类图如下所示:

2.2 安全组合模式

更改抽象构件 ------ AbstractFile

java 复制代码
public abstract class AbstractFile {
    public abstract void killVirus();
}

修改叶子构件,以VideoFile为例

java 复制代码
public class VideoFile extends AbstractFile {
    private String name;

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


    @Override
    public void killVirus() {
        System.out.println("---- 对视频文件'" + name + "'进行杀毒");
    }
}

修改容器构件------Folder

java 复制代码
public class Folder extends AbstractFile {
    private final ArrayList<AbstractFile> fileList = new ArrayList<>();
    private String name;

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

    public void add(AbstractFile file) {
        fileList.add(file);
    }

    public void remove(AbstractFile file) {
        fileList.remove(file);
    }

    public AbstractFile getChild(int i) {
        return fileList.get(i);
    }

    @Override
    public void killVirus() {
        System.out.println("****对文件夹'" + name + "'进行杀毒");
        for (AbstractFile file : fileList) {
            file.killVirus();
        }
    }
}

客户端调用修改

java 复制代码
public class Main {
    public static void main(String[] args) {
        Folder folder1 = new Folder("Sunny 的资料");
        Folder folder2 = new Folder("图像文件");
        Folder folder3 = new Folder("文本文件");
        Folder folder4 = new Folder("视频文件");

        AbstractFile file1 = new ImageFile("小龙女.jpg");
        AbstractFile file2 = new ImageFile("张无忌.gif");
        AbstractFile file3 = new ImageFile("九阴真经.txt");
        AbstractFile file4 = new ImageFile("葵花宝典.doc");
        AbstractFile file5 = new ImageFile("笑傲江湖.rmvb");

        folder2.add(file1);
        folder2.add(file2);
        folder3.add(file3);
        folder3.add(file4);
        folder4.add(file5);
        folder1.add(folder2);
        folder1.add(folder3);
        folder1.add(folder4);

        folder1.killVirus();
    }
}

完整的 UML 类图如下所示:

3. 参考资料

学习视频:

  1. 设计模式快速入门 ------ 图灵星球TuringPlanet ------ 组合模式
  2. Java设计模式详解 ------ 黑马程序员 ------ 组合模式(P82 ~ P86)
  3. Java设计模式 ------ 尚硅谷 ------ 组合模式(P77 ~ P80)

学习读物:

  1. 《设计模式:可复用面向对象软件的基础》------ Erich Gamma 著 ------ 李英军 译 ------ 第 4.3 节(P123)
  2. 《Java 设计模式》 ------ 刘伟 著 ------ 第 11 章(P147)
  3. 《设计模式之美》------ 王争 著 ------ 第 7.6 节(P233)
  4. 《设计模式之禅》 ------ 第 2 版 ------ 秦小波 著 ------ 第 21 章(P240)
  5. 《图解设计模式》------ 结城浩 著 ------ 杨文轩 译 ------ 第 11 章(P117)

电子文献:

  1. 设计模式教程 ------ 菜鸟教程 ------ 组合模式
  2. 99+ 种软件模式 ------ long2ge ------ 组合模式