设计模式(10):组合模式

尺有所短,寸有所长;不忘初心,方得始终。

一、组合模式是什么

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

树状结构如下:

设计模式(10):组合模式

由上图可以看出,

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

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

二、组合模式的适用场景

适用场景

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

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

现实案例:

  • 文件夹

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

  • 超市购物袋

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

  • 算术表达式

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

三、组合模式结构

3.1 组合模式主要角色

  • 抽象构件(Component)角色

    • 主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为
    • 透明式的组合模式中抽象构件声明访问和管理子类的接口
  • 安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成,只定义一些通用的方法。

  • 树叶构件(Leaf)角色

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

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

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

  • 客户端(Client)

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

3.1 组合模式的两种类型

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

  • 透明式的组合模式

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

    • 透明式缺点

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

设计模式(10):组合模式
  • 安全式的组合模式

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

    • 安全式的缺点

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

设计模式(10):组合模式

四、组合模式实现方式

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

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

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

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

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

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

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

五、组合模式的两种实现

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

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

    • 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元)
    }
    }
    }

案例结构图如下

设计模式(10):组合模式

5.1 透明式的组合模式

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

  • 抽象构件(Component)角色

    /**
    * 抽象构件(Component)角色
    */

    public interface Article {

    /**
    * 树枝构件特有的方法: 访问和管理子类的接口 大袋子装小袋子
    */

    public void add(Article article);

    /**
    * 计算价格
    */

    public Double calculation();

    /**
    * 显示商品
    */

    public void show();

    }
  • 树叶构件(Leaf)角色

    /**
    * 树叶构件: 商品
    */

    public class 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() {
    return this.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;

    /**
    * 树枝构件: 袋子
    */

    public class 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()是空实现或抛异常的(案例是空实现)。

  • 案例结果输出

设计模式(10):组合模式

5.2 安全式的组合模式

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

  • 抽象构件(Component)角色

    /**
    * 抽象构件(Component)角色
    */

    public interface Article {
    /**
    * 计算价格
    */

    public Double calculation();

    /**
    * 显示商品
    */

    public void show();

    }
  • 树叶构件(Leaf)角色

    /**
    * 树叶构件: 商品
    */

    public class 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() {
    return this.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;

    /**
    * 树枝构件: 袋子
    */

    public class 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()。

  • 案例结果输出

设计模式(10):组合模式

5.3 组合模式的扩展

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

设计模式(10):组合模式

六、组合模式的优缺点

优点

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

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

缺点

  1. 设计较复杂,需要明确类之间的层次关系;
  2. 不容易限制容器中的构件;
  3. 不容易用继承的方法来增加构件的新功能;

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

  • 创建复杂组合树时使用生成器模式,使构造步骤以递归的方式运行。
  • 责任链模式一般和组合模式结合使用。
  • 迭代器模式可以用来遍历组合树。
  • 访问者模式可以用来对整个组合树执行操作。
  • 享元模式可以用来实现组合树的共享叶节点以节省内存。
  • 组合模式和装饰模式的结构图很相似, 两者都依赖递归组合来组织无限数量的对象。
    • 装饰模式只有一个子组件,并且为被封装对象添加了额外的职责
    • 组合模式只是操作对子节点的原有行为得到结果。

八、总结

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


原文始发于微信公众号(星河之码):设计模式(10):组合模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/27158.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!