设计模式 - 组合模式

162 阅读3分钟

本文已参加【新人创作礼】活动,一起开启掘金创作之路。

概念

组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“部分—整体”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,又可以称为“部分—整体”(Part-Whole)模式,它是一种对象结构型模式。

角色

  • Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。
  • Leaf(叶子构件):它在组合模式结构中表示叶子节点对象。叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过捕获异常等方式进行处理。
  • Composite(容器构件):它在组合模式结构中表示容器节点对象。容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点。它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。

image.png

实例代码

以文件系统为例,文件简单分为2类,一个是文件夹Folder,一个具体文件LeafFile(例如文本文件、可执行文件等)。其中文件夹又可以包含若干个具体文件。抽象一个文件父类File,让文件夹和具体文件继承抽象父类。对于File中包含的增加、删除和获取子文件只对Folde有效,LeafFile需要通过捕获异常等方式进行处理这几个方法。

// 文件抽象类
abstract class File {
    public abstract void add(File f); // 增加成员
    public abstract void remove(File f); // 删除成员
    public abstract File getChild(int i); // 获取成员
    public abstract void operation(); // 业务方法
}

// 文件具体子类:文件夹类
class Folder extends File {
    private ArrayList<File> list = new ArrayList<>();
    public void add(File f) {
        list.add(f);
    }
    public void remove(File f) {
        list.remove(f);
    }
    public File getChild(int i) {
        return (File) list.get(i);
    }
    public void operation() {
        // 具体业务
        for (File f: list) {
            f.operation();
        }
    }
}

// 具体文件
class LeafFile extends File {
    public void add(File f) {
        // 异常处理或错误提示
    }

    public void remove(File f) {
        // 异常处理或错误提示
    }

    public File getChild(int i) {
        // 异常处理或错误提示
        return null;
    }

    public void operation() {
        // 具体文件业务方法实现
    }
}

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

透明组合模式

概念文件系统的案例就是透明组合模式,抽象文件File中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。

1652521703(1).png 上面案例中,具体文件LeafFile只有一种,如果扩展开来可以细分为文本文件、图片文件等,这样每个具体文件都需要去处理add()、remove()以及getChild()方法的异常。针对这种情况,可以直接在抽象类File中统一处理add()、remove()以及getChild()等访问及管理子构件的方法异常。

abstract class File {
    public void add(File f) {
        // 异常处理或错误提示
    }

    public void remove(File f) {
        // 异常处理或错误提示
    }

    public File getChild(int i) {
        // 异常处理或错误提示
        return null;
    }
    public abstract void operation(); // 业务方法
}

对于文件夹Folder子类再覆盖其中add()、remove()以及getChild()方法即可。 透明组合模式总会存在一个问题:具体叶子类中add()、remove()以及getChild()方法是没有意义的。

安全组合模式

将抽象父类中的访问及管理子构件的方法放到对应的子类中去。

abstract class File {
    public abstract void operation(); // 业务方法
}

// 文件具体子类:文件夹类
class Folder extends File {
    private ArrayList<File> list = new ArrayList<>();
    public void add(File f) {
        list.add(f);
    }
    public void remove(File f) {
        list.remove(f);
    }
    public File getChild(int i) {
        return (File) list.get(i);
    }
    public void operation() {
        // 具体业务
        for (File f: list) {
            f.operation();
        }
    }
}

1652522399.png 这样做也存在一个问题:在客户端必须使用Folder类生命构建对象,否者无法调用add()、remove()等方法。

问题记录

  • 在组合模式结构图中,如果聚合关联关系不是从Composite到Component的,而是从Composite到Leaf的,会产生怎样的结果? 如果聚合关联关系是从Composite到Leaf,对应文件的案例,就意味着文件夹下只能是具体文件,不能嵌套文件夹。

总结

优点

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

缺点

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

参考资料

[1] 刘伟. 设计模式的艺术[M]. 清华大学出版社, 2020.