结构型-组合(Composite)

3/27/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 商品类别树

考虑这样一个实际的应用:管理商品类别树。

在实现跟商品有关的应用系统的时候,一个很常见的功能就是商品类别树的管理,比如有如下所示的商品类别树:

- 服装
    - 男装
        - 衬衣
        - 夹克
    - 女装
        - 裙子
        - 套装
1
2
3
4
5
6
7

仔细观察上面的商品类别树,有以下几个明显的特点:

有一个根节点,比如服装,它没有父节点,它可以包含其它的节点;

树枝节点,有一类节点可以包含其它的节点,称之为树枝节点,比如男装、女装;

叶子节点,有一类节点没有子节点,称之为叶子节点,比如衬衣、夹克、裙子、套装;

现在需要管理商品类别树,假如就要求能实现输出如上商品类别树的结构的功能,应该如何实现呢?

# 1.2 不用模式的解决方案

要管理商品类别树,就是要管理树的各个节点,现在树上的节点有三类,根节点、树枝节点和叶子节点,再进一步分析发现,根节点和树枝节点是类似的,都是可以包含其它节点的节点,把它们称为容器节点。

这样一来,商品类别树的节点就被分成了两种,一种是容器节点,另一种是叶子节点。容器节点可以包含其它的容器节点或者叶子节点。把它们分别实现成为对象,也就是容器对象和叶子对象,容器对象可以包含其它的容器对象或者叶子对象,换句话说,容器对象是一种组合对象。

然后在组合对象和叶子对象里面去实现要求的功能就可以了,看看代码实现。

叶子对象代码实现:

/**
 * 叶子对象
 */
public class Leaf {
    /**
     * 叶子对象的名字
     */
    private String name = "";

    /**
     * 构造方法,传入叶子对象的名字
     *
     * @param name 叶子对象的名字
     */
    public Leaf(String name) {
        this.name = name;
    }

    /**
     * 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
     *
     * @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
     */
    public void printStruct(String preStr) {
        System.out.println(preStr + "-" + name);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

组合对象的代码实现,组合对象里面可以包含其它的组合对象或者是叶子对象,由于类型不一样,需要分开记录:

/**
 * 组合对象,可以包含其它组合对象或者叶子对象
 */
public class Composite {

    /**
     * 用来记录包含的其它组合对象
     */
    private Collection<Composite> childComposite = new ArrayList<Composite>();

    /**
     * 用来记录包含的其它叶子对象
     */
    private Collection<Leaf> childLeaf = new ArrayList<Leaf>();

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

    /**
     * 构造方法,传入组合对象的名字
     *
     * @param name 组合对象的名字
     */
    public Composite(String name) {
        this.name = name;
    }

    /**
     * 向组合对象加入被它包含的其它组合对象
     *
     * @param c 被它包含的其它组合对象
     */
    public void addComposite(Composite c) {
        this.childComposite.add(c);
    }

    /**
     * 向组合对象加入被它包含的叶子对象
     *
     * @param leaf 被它包含的叶子对象
     */
    public void addLeaf(Leaf leaf) {
        this.childLeaf.add(leaf);
    }

    /**
     * 输出组合对象自身的结构
     *
     * @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
     */
    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);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

客户端:

public class Client {
    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("");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 1.3 有何问题

上面的实现,虽然能实现要求的功能,但是有一个很明显的问题:那就是必须区分组合对象和叶子对象,并进行有区别的对待,比如在Composite和Client里面,都需要去区别对待这两种对象。

区别对待组合对象和叶子对象,不仅让程序变得复杂,还对功能的扩展也带来不便。实际上,大多数情况下用户并不想要去区别它们,而是认为它们是一样的,这样他们操作起来最简单。

换句话说,对于这种具有整体与部分关系,并能组合成树形结构的对象结构,如何才能够以一个统一的方式来进行操作呢?

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是组合模式。

组合模式:将对象组合成树形结构以表示 "部分-整体" 的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

# 2.1 解决思路

仔细分析上面不用模式的例子中,要区分组合对象和叶子对象的根本原因,就在于没有把组合对象和叶子对象统一起来,也就是说,组合对象类型和叶子对象类型是完全不同的类型,这导致了操作的时候必须区分它们。

组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来了,用户使用的时候,始终是在操作组件对象,而不再去区分是在操作组合对象还是在操作叶子对象。

组合模式的关键就在于这个抽象类,这个抽象类既可以代表叶子对象,也可以代表组合对象,这样用户在操作的时候,对单个对象和组合对象的使用就具有了一致性。

# 2.2 模式结构和说明

组合模式的结构如图所示:

classDiagram direction TB Component <.. Client Component <|-- Leaf Component <|-- Composite Composite "1" o--> "0..*" Component class Component{ <<abstract>> +someOperation()* void +addChild(Component) void +removeChild(Component) void +getChildren(int) Component } class Leaf{ +someOperation() void } class Composite{ -List~Component~ childComponents +someOperation() void +addChild(Component) void +removeChild(Component) void +getChildren(int) Component } class Client{ }
  • Component:抽象的组件对象,为组合中的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以在里面为定义的功能提供缺省的实现。
  • Leaf:叶子节点对象,定义和实现叶子对象的行为,不再包含其它的子节点对象。
  • Composite:组合对象,通常会存储子组件,定义包含子组件的那些组件的行为,并实现在组件接口中定义的与子组件有关的操作。
  • Client:客户端,通过组件接口来操作组合结构里面的组件对象。

# 2.3 示例代码

组合对象的定义,示例代码如下:

/**
 * 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为
 */
public abstract class Component {

    /**
     * 示意方法,子组件对象可能有的功能方法
     */
    public abstract void someOperation();

    /**
     * 向组合对象中加入组件对象
     *
     * @param child 被加入组合对象中的组件对象
     */
    public void addChild(Component child) {
        // 缺省的实现,抛出例外,因为叶子对象没有这个功能,
        // 或者子组件没有实现这个功能
        throw new UnsupportedOperationException("对象不支持这个功能");
    }

    /**
     * 从组合对象中移出某个组件对象
     *
     * @param child 被移出的组件对象
     */
    public void removeChild(Component child) {
        // 缺省的实现,抛出例外,因为叶子对象没有这个功能,
        // 或者子组件没有实现这个功能
        throw new UnsupportedOperationException("对象不支持这个功能");
    }

    /**
     * 返回某个索引对应的组件对象
     *
     * @param index 需要获取的组件对象的索引,索引从0开始
     * @return 索引对应的组件对象
     */
    public Component getChildren(int index) {
        // 缺省的实现,抛出例外,因为叶子对象没有这个功能,
        // 或者子组件没有实现这个功能
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

Composite的对象,示例代码如下:

/**
 * 组合对象,通常需要存储子对象,定义有子部件的部件行为,
 * 并实现在Component里面定义的与子部件有关的操作
 */
public class Composite extends Component {

    /**
     * 用来存储组合对象中包含的子组件对象
     */
    private List<Component> childComponents = null;

    /**
     * 示意方法,通常在里面需要实现递归的调用
     */
    @Override
    public void someOperation() {
        if (childComponents != null) {
            for (Component c : childComponents) {
                // 递归的进行子组件相应方法的调用
                c.someOperation();
            }
        }
    }

    @Override
    public void addChild(Component child) {
        // 延迟初始化
        if (childComponents == null) {
            childComponents = new ArrayList<>();
        }
        childComponents.add(child);
    }

    @Override
    public void removeChild(Component child) {
        if (childComponents != null) {
            childComponents.remove(child);
        }
    }

    @Override
    public Component getChildren(int index) {
        if (childComponents != null) {
            if (index >= 0 && index < childComponents.size()) {
                return childComponents.get(index);
            }
        }
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

叶子对象的定义,示例代码如下:

/**
 * 叶子对象,叶子对象不再包含其它子对象
 */
public class Leaf extends Component {
    /**
     * 示意方法,叶子对象可能有自己的功能方法
     */
    public void someOperation() {
        // do something
    }
}
1
2
3
4
5
6
7
8
9
10
11

客户端:

public class Client {
    public static void main(String[] args) {
        //定义多个Composite对象
        Component root = new Composite();
        Component c1 = new Composite();
        Component c2 = new Composite();
        //定义多个叶子对象
        Component leaf1 = new Leaf();
        Component leaf2 = new Leaf();
        Component leaf3 = new Leaf();

        //组合成为树形的对象结构
        root.addChild(c1);
        root.addChild(c2);
        root.addChild(leaf1);
        c1.addChild(leaf2);
        c2.addChild(leaf3);

        //操作Component对象
        Component o = root.getChildren(1);
        o.someOperation();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.4 重写案例

结构如下图所示:

classDiagram direction TB Component <.. Client Component <|-- Leaf Component <|-- Composite Composite "1" o--> "0..*" Component class Component{ <<abstract>> +printStruct(String)* void +addChild(Component) void +removeChild(Component) void +getChildren(int) Component } class Leaf{ -String name +Leaf(String) +printStruct(String) void } class Composite{ -List~Component~ childComponents -String name +Composite(String) +addChild(Component) void +printStruct(String) void } class Client{ }

为组合对象和叶子对象添加一个抽象的父对象做为组件对象,在组件对象里面,定义一个输出组件本身名称的方法以实现要求的功能,示例代码如下:

/**
 * 抽象的组件对象
 */
public abstract class Component {
    /**
     * 输出组件自身的名称
     */
    public abstract void printStruct(String preStr);

    /**
     * 向组合对象中加入组件对象
     *
     * @param child 被加入组合对象中的组件对象
     */
    public void addChild(Component child) {
        throw new UnsupportedOperationException("对象不支持这个功能");
    }

    /**
     * 从组合对象中移出某个组件对象
     *
     * @param child 被移出的组件对象
     */
    public void removeChild(Component child) {
        throw new UnsupportedOperationException("对象不支持这个功能");
    }

    /**
     * 返回某个索引对应的组件对象
     *
     * @param index 需要获取的组件对象的索引,索引从0开始
     * @return 索引对应的组件对象
     */
    public Component getChildren(int index) {
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

叶子节点,示例代码如下:

/**
 * 叶子对象
 */
public class Leaf extends Component {
    /**
     * 叶子对象的名字
     */
    private String name = "";

    /**
     * 构造方法,传入叶子对象的名字
     *
     * @param name 叶子对象的名字
     */
    public Leaf(String name) {
        this.name = name;
    }

    /**
     * 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
     *
     * @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
     */
    public void printStruct(String preStr) {
        System.out.println(preStr + "-" + name);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

组合对象的实现,示例代码如下:

/**
 * 组合对象,可以包含其它组合对象或者叶子对象
 */
public class Composite extends Component {
    /**
     * 用来存储组合对象中包含的子组件对象
     */
    private List<Component> childComponents = null;
    /**
     * 组合对象的名字
     */
    private String name = "";

    /**
     * 构造方法,传入组合对象的名字
     *
     * @param name 组合对象的名字
     */
    public Composite(String name) {
        this.name = name;
    }

    public void addChild(Component child) {
        //延迟初始化
        if (childComponents == null) {
            childComponents = new ArrayList<>();
        }
        childComponents.add(child);
    }

    /**
     * 输出组合对象自身的结构
     *
     * @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
     */
    public void printStruct(String preStr) {
        //先把自己输出去
        System.out.println(preStr + "+" + this.name);
        //如果还包含有子组件,那么就输出这些子组件对象
        if (this.childComponents != null) {
            //然后添加一个空格,表示向后缩进一个空格
            preStr += " ";
            //输出当前对象的子对象了
            for (Component c : childComponents) {
                //递归输出每个子对象
                c.printStruct(preStr);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

客户端:

public class Client {
    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("");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

运行结果:

通过使用组合模式,把一个“部分-整体”的层次结构表示成了对象树的结构,这样一来,客户端就无需再区分操作的是组合对象还是叶子对象了,对于客户端而言,操作的都是组件对象

# 三:模式讲解

# 3.1 认识组合模式

1. 组合模式的目的

组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一个统一的方式来操作

实现这个目标的关键之处,是设计一个抽象的组件类,让它可以代表组合对象和叶子对象。这样一来,客户端就不用区分到底是组合对象还是叶子对象了,只需要全部当成组件对象进行统一的操作就可以了。

2. 对象树

通常,组合模式会组合出树形结构来,组成这个树形结构所使用的多个组件对象,就自然的形成了对象树

这也意味着凡是可以使用对象树来描述或操作的功能,都可以考虑使用组合模式,比如读取XML文件,或是对语句进行语法解析等。

3. Component中是否应该实现一个Component列表

大多数情况下,一个Composite对象会持有子节点的集合。有些朋友可能就会想,那么能不能把这个子节点集合定义到Component中去呢?因为在Component中还声明了一些操作子节点的方法,这样一来,大部分的工作就可以在Component中完成了。

事实上,这种方法是不太好的,因为在父类来存放子类的实例对象,对于Composite节点来说没有什么,它本来就需要存放子节点,但是对于叶子节点来说,就会导致空间的浪费,因为叶节点本身不需要子节点

因此只有当组合结构中叶子对象数目较少的时候,才值得使用这种方法。

4. 最大化Component定义

前面讲到了组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作。

由于要统一两种对象的操作,所以Component里面的方法也主要是两种对象对外方法的和,换句话说,有点大杂烩的意思,组件里面既有叶子对象需要的方法,也有组合对象需要的方法

其实这种实现是与类的设计原则相冲突的,类的设计有这样的原则:一个父类应该只定义那些对它的子类有意义的操作。但是看看上面的实现就知道,Component中的有些方法对于叶子对象是没有意义的。那么怎么解决这一冲突呢?

常见的做法是在Component里面为对某些子对象没有意义的方法,提供默认的实现,或是默认抛出不支持该功能的例外。这样一来,如果子对象需要这个功能,那就覆盖实现它,如果不需要,那就不用管了,使用父类的默认实现就可以了。

从另一个层面来说,如果把叶子对象看成是一个特殊的Composite对象,也就是没有子节点的组合对象而已。这样看来,对于Component而言,子对象就全部看作是组合对象,因此定义的所有方法都是有意义的了

5. 子部件排序

在某些应用中,使用组合模式的时候,需要按照一定的顺序来使用子组件对象,比如进行语法分析的时候,使用组合模式构建的抽象语法树,在解析执行的时候,是需要按照顺序来执行的。

对于这样的功能,需要在设计的时候,就要把组件对象的索引考虑进去,并仔细的设计对子节点的访问和管理接口,通常的方式是需要按照顺序来存储,这样在获取的时候就可以按照顺序得到了。可以考虑结合Iterator模式来实现按照顺序的访问组件对象

# 3.2 优缺点

1. 定义了包含基本对象和组合对象的类层次结构

在组合模式中,基本对象可以被组合成更复杂的组合对象,而组合对象又可以组合成更复杂的组合对象,可以不断地递归组合下去,从而构成一个统一的组合对象的类层次结构。

2. 统一了组合对象和叶子对象

在组合模式中,可以把叶子对象当作特殊的组合对象看待,为它们定义统一的父类,从而把组合对象和叶子对象的行为统一起来

3. 简化了客户端调用

组合模式通过统一组合对象和叶子对象,使得客户端在使用它们的时候,就不需要再去区分它们,客户不关心使用的到底是什么类型的对象,这就大大简化了客户端的使用。

4. 更容易扩展

由于客户端是统一的面对Component来操作,因此,新定义的Composite或Leaf子类能够很容易的与已有的结构一起工作,而客户端不需要为增添了新的组件类而改变。

5. 很难限制组合中的组件类型

容易增加新的组件也会带来一些问题,比如很难限制组合中的组件类型。这在需要检测组件类型的时候,使得我们不能依靠编译期的类型约束来完成,必须在运行期间动态检测。

# 3.3 相关模式

1. 组合模式和装饰模式

这两个模式可以组合使用。

装饰模式在组装多个装饰器对象的时候,是一个装饰器找下一个装饰器,下一个再找下一个,如此递归下去。那么这种结构也可以使用组合模式来帮助构建,这样一来,装饰器对象就相当于组合模式的Composite对象了。

要让两个模式能很好的组合使用,通常会让它们有一个公共的父类,因此装饰器必须支持组合模式需要的一些功能,比如:增加、删除子组件等等。

2. 组合模式和享元模式

这两个模式可以组合使用。

如果组合模式中出现大量相似的组件对象的话,可以考虑使用享元模式来帮助缓存组件对象,这可以减少对内存的需要。

使用享元模式也是有条件的,如果组件对象的可变化部分的状态能够从组件对象里面分离出去,而且组件对象本身不需要向父组件发送请求的话,就可以采用享元模式。

3. 组合模式和迭代器模式

这两个模式可以组合使用。

在组合模式中,通常可以使用迭代器模式来遍历组合对象的子对象集合,而无需关心具体存放子对象的聚合结构

4. 组合模式和访问者模式

这两个模式可以组合使用。

访问者模式能够在不修改原有对象结构的情况下,给对象结构中的对象增添新的功能。将访问者模式和组合模式合用,可以把原本分散在Composite和Leaf类中的操作和行为都局部化。

如果在使用组合模式的时候,预计到今后可能会有增添其它功能的可能,那么可以采用访问者模式,来预留好添加新功能的方式和通道,这样以后在添加新功能的时候,就不需要再修改已有的对象结构和已经实现的功能了。

5. 组合模式和职责链模式

这两个模式可以组合使用。

职责链模式要解决的问题是:实现请求的发送者和接收者之间解耦。职责链模式的实现方式是把多个接收者组合起来,构成职责链,然后让请求在这条链上传递,直到有接收者处理这个请求为止。

可以应用组合模式来构建这条链,相当于是子组件找父组件,父组件又找父组件,如此递归下去,构成一条处理请求的组件对象链。

6. 组合模式和命令模式

这两个模式可以组合使用。

命令模式中有一个宏命令的功能,通常这个宏命令就是使用组合模式来组装出来的。

# 四:JDK

# 五:参考文献

最后更新: 3/27/2022, 8:16:35 PM