设计模式拾荒之组合模式(Composite Pattern): 复杂的树形结构

  1. 参考书籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》

设计模式用前须知

  • 设计模式种一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
  • 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
  • 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。

组合模式(Composite Pattern)

  • 设计意图
    • GOF: 将对象组合成树状结构来体现“部分与整体“ 的层次关系。 组合模式使得客户端代码可以一致地对待 单体对象 和 由单体对象组合出的复合对象
    • 以上的描述比较抽象, 但是有两个关键词可以帮助我们理解组合模式
      1. “树状结构”
      2. “一致地对待”
  • GOF举例:
    • 图形类应用软件可以令用户用一些简单的组件构建出复杂的图表。 用户可以把不同的组件组合在一起, 形成一个更大的组件, 然后可以再次将该组件和其他组件进一步组合形成“更更大”的组件。
    • 最容易想到的编写方法是, 为基础图形元素定义一些类, 然后再定义一些类代表容器。
      • 这样设计的问题是, 使用这些类时, 必须把容器类和基础元素类区别对待, 尽管大部分时间, 用户对待他们的方式都是一样的(不管是容器还是基础图形元素, 都是可以拖动,可以编辑,可以旋转, 需要被显示)
      • 区别对待这些类, 会增加应用的复杂度。
  • 解决方案

    • 组合模式描述了如何使用递归组合的方式, 使得用户可以一致地对待单体对象复合对象
  • 图例说明

    • 图片中的空心三角箭头,代表着继承(extends)或实现(Implement)关系, 由继承者/实现者 指向 被继承者/被继承者。
    • 图片中的实心三角箭头且箭头末尾没有圆圈的, 代表着单一的引用关系, 但是被引用的对象也有可能被其他对象引用。
    • 图片中的实心三角箭头且箭头末尾有圆圈的, 代表着一对多的引用关系。
    • 图片中的末端有圆圈的虚线是一个对方法体内容用伪代码说明的关系
  • 图例分析

    • 组合模式的关键之处在于, 定义了一个抽象类同时代表了基础元素类(primitives)和容器类( containers )。
      • 在图例中, 这个关键的抽象类是 Gaphic 。
        • 它不仅定义了单个图形元素的基本方法, 如绘制 draw() , 还定义了所有的组合对象共享的,用于访问和管理其子元素的方法, 如getChild(), add( Graphic g), remove( Graphic g)
      • Graphic 的子类 Line, Rectangle, Text 定义了的基本图形类。
        • 由于这些基本图形类不会扮演容器的角色, 所以就没有实现和子元素管理有关的方法 , 即 getChild, add, remove 等方法。 ( 这一点决定, Graphic 不能是C++中的纯虚基类, 也不能是Java 中的接口, 因为在纯虚基类和接口中定义的所有方法都必须被实现, 而不能只实现一部分)
      • Picture 类定义了 Graphic 的聚合体。
        • Picture 类实现了 draw() 方法,draw() 方法中会调用其子元素的 draw() 方法。
        • Picture 类还实现了跟对象组合相关的方法, 如getChild, add, remove 方法 , 使得Picture 对象可以用来组合基本图形对象

组合模式再分析

  • 组合模式中有一个最基础的抽象类(Graphic ), 不仅要定义单体对象类 (Line, Text, Rectangle)的方法, 还需要定义复合对象类 ( Picture )的方法。
    • 问题 1: 如何实现那些单体对象类不会重写的, 只存在于复合对象类的方法(addChild, removeChild, getChild)?
    • 解决方案:
      • 为仅属于复合对象类的操作提供一种默认实现, 可以将处于叶子节点的单体对象类看做是一种没有子节点的特殊复合对象类, getChild 的默认实现可以返回空, 而真正的复合对象类则可以重写 getChild 方法,使其返回真正的子节点。
    • 问题 2: 如何让客户端避免在单体对象类上调用只对复合对象类有意义的操作(addChild, removeChild)
      • 首先要回答的是, 为什么需要担心这个问题。 毕竟, 基础抽象类提供的默认实现可以什么都不做, 即便在单体对象上调用了addChild, removeChild 等方法, 也不会有什么影响啊?
        • 这样做的问题是: 当用户代码试图在一个单体对象上调用仅存在于复合对象类的方法时, 这可能已经是一个Bug 了。 如果默认的方法什么都不做, 正确地予以返回, 用户代码可能没有办法意识到问题的来源。
        • 例子: 你获得了一个复杂的复合对象 Graphic , 想要访问其孙子节点 Picture 对象 p, 向其中添加一个 Line 对象 l , 但是你搞错了它的深度, 结果访问到了一个Rectangle 对象 r, 调用了 r.add( l ) , 你编译运行了代码, 发现图形界面在你执行了添加动作以后并无变化, 此时,你便会困惑到底发生了什么。
      • 所以通常来说更好的做法是:
        • 让 addChild , removeChild 的默认实现返回失败,或抛出异常, 仅仅在复合对象类中重定义这些操作, 使其可以正常返回。
        • 这样, 一旦用户代码不小心在单体对象上调用了复合对象才有的方法时, 就会失败, 编写用户代码的程序员可以马上意识到问题所在。

组合模式总结

  • 组合模式最核心特点就是组合对象的树状结构, 组合对象的叶子节点为基本的单体对象, 非叶子节点则为组合对象。
  • 组合模式可以让用户无差别地对待单体对象和组合对象。

你可能感兴趣的:(设计模式)