目录

一、组合模式是什么

二、组合模式的适用场景

三、组合模式结构

3.1 组合模式主要角色

3.1 组合模式的两种类型

四、组合模式实现方式

五、组合模式的两种实现

5.1 透明式的组合模式

5.2 安全式的组合模式

5.3 组合模式的扩展

六、组合模式的优缺点

七、组合模式和其他模式的关联

八、总结



858bd30d7f0c479cae1517b99af9b533.png

一、组合模式是什么

组合模式:又叫作整体-部分(Part-Whole)模式,它将对象组合成树状的层次结构,用来表示整体-部分的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型模式。

树状结构如下:

777238153cd540f09f61e65da7f9861c.png

由上图可以看出,

  • 根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用。

  • 叶子节点与树枝节点在语义上不属于用一种类型。在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。

组合模式在树型结构中,模糊了简单元素(叶子节点)和复杂元素(树枝节点)的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。

二、组合模式的适用场景

适用场景

  • 需要体现部分与整体的树状层次结构时,可以使用组合模式。

  • 希望客户端忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

现实案例:

  • 文件夹

    文件夹可以有子文件夹和文件。

  • 超市购物袋

    大袋子装商品和小袋子,小袋子可以装小袋子和商品

  • 算术表达式

    包括操作数、操作符和另一个操作数,另一个操作数也可以是操作数、操作符和另一个操作数

三、组合模式结构

3.1 组合模式主要角色

  • 抽象构件(Component)角色

    • 主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为

    • 透明式的组合模式中抽象构件声明访问和管理子类的接口

  • 安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成,只定义一些通用的方法。

  • 树叶构件(Leaf)角色

    在组合中表示叶节点,叶节点没有子节点,用于继承或实现抽象构件基本行为。

  • 树枝构件(Composite)角色 / 中间构件

    是组合中的分支节点对象,有子节点,用于继承和实现抽象构件基本行为,它的主要作用是存储存储子部件并在Component接口实现与子部件有关的操作。

  • 客户端(Client)

    通过Component接口操作组合部件的对象。

3.1 组合模式的两种类型

组合模式分为透明式的组合模式和安全式的组合模式。这两种类型的主要区别在于抽象构件(Component)角色上的差别。

  • 透明式的组合模式

    在透明式的组合模式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。

    • 透明式缺点

      树叶构件本身没有子节点,但是由于继承抽象构件,需要实现树枝构件所特有的行为(比如文件夹的新增子文件夹的方法),此时只能空实现或抛异常。其结构图如下

    • 568c63c97ef74272bca0ad70a6401a8b.png

  • 安全式的组合模式

    在安全式的组合模式中,将管理子构件的方法移到树枝构件中,抽象构件只定义树枝构件和树叶构件所共同的方法。避免了透明式的组合模式的空实现或抛异常问题。

    • 安全式的缺点

      由于叶子节点和树枝节点有不同的行为方法,客户端在调用时要知道树叶对象和树枝对象的存在,所以对对于客户端失去了透明性。其结构图如下

e9117c0fc13147f7b71f295f559ed6da.png

四、组合模式实现方式

组合模式实现的前提:确保应用的核心模型能够以树状结构表示,并将其分解为简单元素和容器,容器必须能够同时包含简单元素和子容器

  • 定义一个接口或抽象类作为抽象构件角色,声明组件接口及其一系列方法。

  • 定义一个叶节点类表示简单元素。实现抽象构件角色, 程序中可以有多个不同的叶节点类。

    叶节点可以作为一个接口,以不同的叶子结点类去实现接口

  • 定义一个树枝节点(容器)类表示复杂元素,实现抽象构件角色

    • 在该类中创建一个数组成员变量来存储对于其子元素的引用。 该数组必须能够同时保存叶节点和容器, 因此必须将其声明为组合接口类型

    • 树枝节点也可以作为一个接口,以不同的树枝节点类去实现接口

五、组合模式的两种实现

  • 【案例】用组合模式实现在超市购物后,显示并计算所选商品信息与总价。

  • 【案例说明】张三在超市购物,购物清单如下

    • 1号小袋子装了2 包芒果干(单价15.8元),1包薯片(单价9.8元)

    • 2号小袋子装了3 包山楂(单价7.8元),2包牛肉脯(单价19.8元)

    • 中型袋子装了1号小袋子,1盒巧克力(单价39.8元)

    • 大型袋子装了中型袋子,2号小袋子,1箱牛奶(单价79.8元)

  • 【大袋子的东西】

    {
        1箱牛奶(单价79.8元)
        2号小袋子{
        	3 包山楂(单价7.8元
        	2包牛肉脯(单价19.8元)
    	}
        中型袋子:{
            1盒巧克力(单价39.8元)
            1号小袋子:{
            	2 包芒果干(单价15.8元)
            	1包薯片(单价9.8元)
        	}
        }
    }
    

案例结构图如下

ad1f37a2a5ca4dc2b938feac84df1cb0.png

 

5.1 透明式的组合模式

透明式的组合模式中抽象构件声明访问和管理子类的接口

  • 抽象构件(Component)角色

    /**
     * 抽象构件(Component)角色
     */
    publicinterface Article {
    
        /**
         * 树枝构件特有的方法: 访问和管理子类的接口  大袋子装小袋子
         */
        public void add(Article article);
    
        /**
         * 计算价格
         */
        public Double calculation(); 
    
        /**
         * 显示商品
         */
        public void show();
    
    }
    
  • 树叶构件(Leaf)角色

    /**
     * 树叶构件: 商品
     */
    publicclass Goods implements Article {
    
        /**
         * 商品名称
         */
        private String name;
        /**
         * 购买数量
         */
        private Integer quantity;
        /**
         * 商品单价
         */
        private Double unitPrice;
    
        public Goods(String name, Integer quantity, Double unitPrice) {
            this.name = name;
            this.quantity = quantity;
            this.unitPrice = unitPrice;
        }
    
        /**
         * 树枝构件特有的方法
         * 在树叶构件中是能空实现或者抛异常
         */
        @Override
        public void add(Article article) {
    
        }
    
        @Override
        public Double calculation() {
            returnthis.unitPrice * this.quantity;
        }
    
        @Override
        public void show() {
            System.out.println(name + ": (数量:" + quantity + ",单价:" + unitPrice + "元)," +
                    "合计:"+this.unitPrice * this.quantity+"元");
        }
    }
    
    
  • 树枝构件(Composite)角色 / 中间构件

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * 树枝构件: 袋子
     */
    publicclass Bag implements Article{
    
        /**
         * 袋子名字
         */
        private String name;
    
        public Bag(String name) {
            this.name = name;
        }
    
        /**
         * 袋子中的商品
         */
        private List<Article> bags = new ArrayList<Article>();
    
        /**
         * 往袋子中添加袋子或者商品
         */
        @Override
        public void add(Article article) {
            bags.add(article);
        }
    
        @Override
        public Double calculation() {
            AtomicReference<Double> sum = new AtomicReference<>(0.0);
            bags.forEach(e->{
                sum.updateAndGet(v -> v + e.calculation());
            });
            return sum.get();
        }
    
        @Override
        public void show() {
            bags.forEach(Article::show);
        }
    }
    
    
  • 客户端代码实现

    public static void main(String[] args) throws Exception {
    
       		Article smallOneBag = new Bag("1号小袋子");
            smallOneBag.add(new Goods("芒果干", 2, 15.8));
            smallOneBag.add(new Goods("薯片", 1, 9.8));
    
            Article smallTwoBag = new Bag("2号小袋子");
            smallTwoBag.add(new Goods("山楂", 3, 7.8));
            smallTwoBag.add(new Goods("牛肉脯", 2, 19.8));
    
            Article mediumBag = new Bag("中袋子");
            mediumBag.add(new Goods("巧克力", 1, 39.8));
            mediumBag.add(smallOneBag);
    
            Article BigBag = new Bag("大袋子");
            BigBag.add(new Goods("牛奶", 1, 79.8));
            BigBag.add(mediumBag);
            BigBag.add(smallTwoBag);
    
            System.out.println("张三选购的商品有:");
            BigBag.show();
            Double sum = BigBag.calculation();
            System.out.println("要支付的总价是:" + sum + "元");
        }
    
    

    以上客户端代码中  new Bag(),new Goods()的引用都是Article,无须区别树叶对象和树枝对象,对客户端来说是透明的,此时Article调用add()是空实现或抛异常的(案例是空实现)。

  • 案例结果输出

13a3b4df98ba4312a2f424619f640976.png

5.2 安全式的组合模式

安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成,只定义一些通用的方法。

  • 抽象构件(Component)角色

    /**
     * 抽象构件(Component)角色
     */
    publicinterface Article {
        /**
         * 计算价格
         */
        public Double calculation(); 
    
        /**
         * 显示商品
         */
        public void show();
    
    }
    
  • 树叶构件(Leaf)角色

    /**
     * 树叶构件: 商品
     */
    publicclass Goods implements Article {
    
        /**
         * 商品名称
         */
        private String name;
        /**
         * 购买数量
         */
        private Integer quantity;
        /**
         * 商品单价
         */
        private Double unitPrice;
    
        public Goods(String name, Integer quantity, Double unitPrice) {
            this.name = name;
            this.quantity = quantity;
            this.unitPrice = unitPrice;
        }
    
        @Override
        public Double calculation() {
            returnthis.unitPrice * this.quantity;
        }
    
        @Override
        public void show() {
            System.out.println(name + ": (数量:" + quantity + ",单价:" + unitPrice + "元)," +
                    "合计:"+this.unitPrice * this.quantity+"元");
        }
    }
    
    
  • 树枝构件(Composite)角色 / 中间构件

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * 树枝构件: 袋子
     */
    publicclass Bag implements Article{
        /**
         * 袋子名字
         */
        private String name;
    
        public Bag(String name) {
            this.name = name;
        }
    
        /**
         * 袋子中的商品
         */
        private List<Article> bags = new ArrayList<Article>();
    
        /**
         * 树枝构件特有的方法: 访问和管理子类的接口  大袋子装小袋子
         * 往袋子中添加袋子或者商品
         */
        @Override
        public void add(Article article) {
            bags.add(article);
        }
    
        @Override
        public Double calculation() {
            AtomicReference<Double> sum = new AtomicReference<>(0.0);
            bags.forEach(e->{
                sum.updateAndGet(v -> v + e.calculation());
            });
            return sum.get();
        }
    
        @Override
        public void show() {
            bags.forEach(Article::show);
        }
    }
    
    
  • 客户端代码实现

    public static void main(String[] args) throws Exception {
    
            Bag smallOneBag = new Bag("1号小袋子");
            smallOneBag.add(new Goods("芒果干", 2, 15.8));
            smallOneBag.add(new Goods("薯片", 1, 9.8));
    
            Bag smallTwoBag = new Bag("2号小袋子");
            smallTwoBag.add(new Goods("山楂", 3, 7.8));
            smallTwoBag.add(new Goods("牛肉脯", 2, 19.8));
    
            Bag mediumBag = new Bag("中袋子");
            mediumBag.add(new Goods("巧克力", 1, 39.8));
            mediumBag.add(smallOneBag);
    
            Bag BigBag = new Bag("大袋子");
            BigBag.add(new Goods("牛奶", 1, 79.8));
            BigBag.add(mediumBag);
            BigBag.add(smallTwoBag);
    
            System.out.println("张三选购的商品有:");
            BigBag.show();
            Double sum = BigBag.calculation();
            System.out.println("要支付的总价是:" + sum + "元");
        }
    
    

    以上客户端代码中  new Bag(),new Goods()的引用都是Bag,Goods,客户端在调用时要知道树叶对象和树枝对象的存在。此时只有Bag才能调用add()。

  • 案例结果输出

7033c0984fc7460ebdc40e5a3da5917d.png

5.3 组合模式的扩展

在实际开发过程中,可以对树叶节点和树枝节点分别进行抽象,通过继承的方式让不同的树叶节点和树枝节点子类来实现行为。

95788c8881224f2f86d36d9f7492909e.png

六、组合模式的优缺点

优点

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

  • 开闭原则:在组合体内加入新的对象,客户端不会更改源代码,可以一致地处理单个对象和组合对象。

缺点

  1. 设计较复杂,需要明确类之间的层次关系;

  2. 不容易限制容器中的构件;

  3. 不容易用继承的方法来增加构件的新功能;

七、组合模式和其他模式的关联

  • 创建复杂组合树时使用生成器模式,使构造步骤以递归的方式运行。

  • 责任链模式一般和组合模式结合使用。

  • 迭代器模式可以用来遍历组合树。

  • 访问者模式可以用来对整个组合树执行操作。

  • 享元模式可以用来实现组合树的共享叶节点以节省内存。

  • 组合模式和装饰模式的结构图很相似, 两者都依赖递归组合来组织无限数量的对象。

    • 装饰模式只有一个子组件,并且为被封装对象添加了额外的职责

    • 组合模式只是操作对子节点的原有行为得到结果。

八、总结

组合模式是一种解决树状问题的结构型模式,再使用过程中需要有较强的层次结构,在实现时要注意树枝节点的特有接口以及含有内部属性 List,List里面放 Component。

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐