“补课”进行时:设计模式(14)——组合模式

1. 前文汇总

“补课”进行时:设计模式系列

2. 某东的菜单

前段时间双十一,不知道各位的战果如何,反正我是屯了两盒口罩凑个数。

电商平台为我们提供的方便快捷的搜索框入口,我想大多数人在使用的时候应该都会使用这个入口,但其实电商平台还为我们提供了另一个入口,就是它的分类体系,如下:

我简单抽象一下:

- 服装
    - 男装
        - 衬衣
        - 夹克
    - 女装
        - 裙子
        - 套装

可以看到,这是一个树结构,在前端实现一个这种菜单树可以选用 ZTree 插件,做过前端的都知道。

下面,用 Java 代码实现一下,输出一下上面的这个树状结构:

观察这个树状结构,可以看到节点分为三种类型:

  • 根节点
  • 树枝节点
  • 叶子节点(没有子节点)

根节点和树枝节点的构造是比较类似的,都是可以有子节点,这两个节点可以抽象成一个对象。

首先定义一个叶子节点:

public class Leaf {
    // 叶子对象的名字
    private String name;
    // 构造方法
    public Leaf(String name) {
        this.name = name;
    }
    // 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
    public void printStruct(String preStr) {
        System.out.println(preStr + " - " + name);
    }
}

接着定义一个组合对象:

public class Composite {
    // 组合对象集合
    private Collection<Composite> childComposite = new ArrayList<>();
    // 叶子对象集合
    private Collection<Leaf> childLeaf = new ArrayList<>();
    // 组合对象名称
    private String name;
    // 构造函数
    public Composite(String name) {
        this.name = name;
    }
    // 向组合对象加入被它包含的其它组合对象
    public void addComposite(Composite c){
        this.childComposite.add(c);
    }
    // 向组合对象加入被它包含的叶子对象
    public void addLeaf(Leaf leaf){
        this.childLeaf.add(leaf);
    }
    // 输出自身结构
    public void printStruct(String preStr){
        System.out.println(preStr + " + " + this.name);
        preStr+=" ";
        for(Leaf leaf : childLeaf){
            leaf.printStruct(preStr);
        }
        for(Composite c : childComposite){
            c.printStruct(preStr);
        }
    }
}

来一个测试类:

public class Test {
    public static void main(String[] args) {
        //定义所有的组合对象
        Composite root = new Composite("服装");
        Composite c1 = new Composite("男装");
        Composite c2 = new Composite("女装");

        //定义所有的叶子对象
        Leaf leaf1 = new Leaf("衬衣");
        Leaf leaf2 = new Leaf("夹克");
        Leaf leaf3 = new Leaf("裙子");
        Leaf leaf4 = new Leaf("套装");

        //按照树的结构来组合组合对象和叶子对象
        root.addComposite(c1);
        root.addComposite(c2);
        c1.addLeaf(leaf1);
        c1.addLeaf(leaf2);
        c2.addLeaf(leaf3);
        c2.addLeaf(leaf4);

        //调用根对象的输出功能来输出整棵树
        root.printStruct("");
    }
}

上面的这种实现方案,虽然能实现了我们希望看到的功能,但是有一个很明显的问题:那就是必须区分组合对象和叶子对象,并进行有区别的对待。

3. 组合模式

3.1 定义

组合模式(Composite Pattern)也叫合成模式,有时又叫做部分-整体模式(Part-Whole),主要是用来描述部分与整体的关系,其定义如下:

Compose objects into tree structures to represent part-wholehierarchies.Composite lets clients treat individual objects and compositionsof objects uniformly.(将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。)

3.2 通用类图

  • Component 抽象构件角色: 定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性。
  • Leaf 叶子构件: 叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。
  • Composite 树枝构件: 树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。

3.3 通用代码

public abstract class Component {
    // 整体和个体都共享的逻辑
    void doSomething() {
        // 具体的业务逻辑
    }
}

public class Leaf extends Component {
    // 可以复写父类的方法
    @Override
    void doSomething() {
        super.doSomething();
    }
}

public class Composite extends Component {
    // 构建容器
    private ArrayList<Component> componentArrayList = new ArrayList<>();
    // 增加一个叶子构件或者树枝构件
    public void add(Component component) {
        this.componentArrayList.add(component);
    }
    // 删除一个叶子构件或者树枝构件
    public void remove(Component component) {
        this.componentArrayList.remove(component);
    }
    // 获得分支下所有叶子构件或者树枝构件

    public ArrayList<Component> getChildren() {
        return this.componentArrayList;
    }
}

3.4 优点

  1. 高层模块调用简单:

一棵树形机构中的所有节点都是 Component ,局部和整体对调用者来说没有任何区别,也就是说,高层模块不必关心自己处理的是单个对象还是整个组合结构,简化了高层模块的代码。

  1. 节点自由增加:

使用了组合模式后,我们可以看看,如果想增加一个树枝节点、树叶节点是不是都很容易,只要找到它的父节点就成,非常容易扩展,符合开闭原则,对以后的维护非常有利。

4. 示例改进(安全模式)

把最上面的示例修改成组合模式,首先需要定义一个抽象组件:

public abstract class Component {
    // 输出组件名称
    abstract void printStruct(String preStr);
}

叶子节点:

public class Leaf extends Component {

    private String name;

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

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " - " + name);
    }
}

树枝节点:

public class Composite extends Component{

    // 组合对象集合
    private Collection<Component> childComponents;

    // 组合对象的名字
    private String name;

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

    public void addChild(Component child) {
        if (this.childComponents == null) {
            this.childComponents = new ArrayList<>();
        }
        this.childComponents.add(child);
    }

    void removeChild(Component child) {
        this.childComponents.remove(child);
    }

    Collection getChildren() {
        return this.childComponents;
    }

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " + " + this.name);
        if (this.childComponents != null) {
            preStr+=" ";
            for(Component c : this.childComponents){
                //递归输出每个子对象
                c.printStruct(preStr);
            }
        }
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        // 定义根节点
        Composite root = new Composite("服装");
        // 创建两个树枝节点
        Composite c1 = new Composite("男装");
        Composite c2 = new Composite("女装");

        // 定义所有的叶子对象
        Leaf leaf1 = new Leaf("衬衣");
        Leaf leaf2 = new Leaf("夹克");
        Leaf leaf3 = new Leaf("裙子");
        Leaf leaf4 = new Leaf("套装");

        // 按照树的结构来组合组合对象和叶子对象
        root.addChild(c1);
        root.addChild(c2);
        c1.addChild(leaf1);
        c1.addChild(leaf2);
        c2.addChild(leaf3);
        c2.addChild(leaf4);
        // 调用根对象的输出功能来输出整棵树
        root.printStruct("");
    }
}

5. 透明模式

组合模式有两种不同的实现,安全模式和透明模式,上面的示例是安全模式,下面是透明模式的通用类图:

和上面的那个安全模式的通用类图对比一下区别,就非常明显了,只是单纯的把几个方法 addChild()removeChild()getChildren() 几个方法放到了抽象类中。

在透明模式中不管叶子对象还是树枝对象都有相同的结构,通过判断是否存在子节点来判断叶子节点还是树枝节点,如果处理不当,这个会在运行期出现问题。

而在安全模式中,它是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方案比较安全。

抽象角色:

public abstract class Component {
    // 输出组件名称
    abstract void printStruct(String preStr);

    // 向组合对象中加入组件对象
    abstract void addChild(Component child);

    // 从组合对象中移出某个组件对象
    abstract void removeChild(Component child);

    // 返回组件对象
    abstract Collection getChildren();
}

叶子节点:

public class Leaf extends Component {

    private String name;

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

    // 向组合对象中加入组件对象
    @Deprecated
    public void addChild(Component child) {
        // 缺省实现,如果子类未实现此功能,由父类抛出异常
        throw new UnsupportedOperationException("对象不支持这个功能");
    }

    // 从组合对象中移出某个组件对象
    @Deprecated
    public void removeChild(Component child){
        // 缺省实现,如果子类未实现此功能,由父类抛出异常
        throw new UnsupportedOperationException("对象不支持这个功能");
    }

    @Deprecated
    Collection getChildren() {
        throw new UnsupportedOperationException("对象不支持这个功能");
    }

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " - " + name);
    }
}

这里使用 Deprecated 注解是为了在编译期告诉调用者,这方法我有,可以调用,但是已经失效了,如果一定要调用,那么在运行期会抛出 UnsupportedOperationException 的错误。

树枝节点:

public class Composite extends Component {

    // 组合对象集合
    private Collection<Component> childComponents;

    // 组合对象的名字
    private String name;

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

    public void addChild(Component child) {
        if (this.childComponents == null) {
            this.childComponents = new ArrayList<>();
        }
        this.childComponents.add(child);
    }

    @Override
    void removeChild(Component child) {
        this.childComponents.remove(child);
    }

    @Override
    Collection getChildren() {
        return this.childComponents;
    }

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " + " + this.name);
        if (this.childComponents != null) {
            preStr+=" ";
            for(Component c : this.childComponents){
                //递归输出每个子对象
                c.printStruct(preStr);
            }
        }
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        //定义所有的组合对象
        Component root = new Composite("服装");
        Component c1 = new Composite("男装");
        Component c2 = new Composite("女装");

        //定义所有的叶子对象
        Component leaf1 = new Leaf("衬衣");
        Component leaf2 = new Leaf("夹克");
        Component leaf3 = new Leaf("裙子");
        Component leaf4 = new Leaf("套装");

        //按照树的结构来组合组合对象和叶子对象
        root.addChild(c1);
        root.addChild(c2);
        c1.addChild(leaf1);
        c1.addChild(leaf2);
        c2.addChild(leaf3);
        c2.addChild(leaf4);
        //调用根对象的输出功能来输出整棵树
        root.printStruct("");
    }
}

6. 参考

//www.jianshu.com/p/dead42334033