Design Patterns Of Decorator And Composite


一、装饰器模式

上班族大多都有睡懒觉的习惯,每天早上上班时间都很紧张,于是很多人为了多睡一会,就会用方便的方式解决早餐问题。有些人早餐可能会吃煎饼,煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么“加码”,都还是一个煎饼。在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等,都是装饰器模式。

在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰器模式来实现。

1、定义

装饰器模式的定义与特点

装饰器(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。

装饰器模式的主要优点有:

  • 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
  • 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果
  • 装饰器模式完全遵守开闭原则

其主要缺点是:装饰器模式会增加许多子类,过度使用会增加程序得复杂性。

2、结构和实现

通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰器模式的目标。下面来分析其基本结构和实现方法。

结构

装饰器模式主要包含以下角色。

  • 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

实现

类图:

3、案例分析

星巴克咖啡订单项目(咖啡馆)

  • 咖啡种类/单品咖啡:Espresso、ShortBlack、LongBlack、Decaf
  • 调料:Milk、Soy、Chocolate
  • 要求在扩展 新的咖啡种类 时,具有良好的扩展性、改动方便、维护方便
  • 使用递归来计算不同种类咖啡的 费用:客户可以点 单品咖啡,也可以 单品咖啡 + 调料组合

传统解决方案

最简单的就是利用继承体系

但是考虑到 单品咖啡 + 调料有很多种组合,会造成类爆炸

  • Drink 是一个抽象类,表示饮料
  • des 对咖啡描述
  • cost()计算费用,Drink 类中抽象方法

改进方案一

考虑到传统继方法(单品咖啡 + 调料 组合)会造成子类过多,我们可以将调料内置到 Drink 类

  • 方案一可以控制类的数量,不至于造成很多类
  • 在增加或删除调料种类时,代码维护量很大
  • 考虑到用户可以添加多份调料时,可以将 hasMilk 返回一个 int

综上:考虑使用装饰者模式

4、改进方案:装饰者模式

装饰者模式定义

  • 装饰者模式:动态的将新功能 附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则
  • 这里提到 动态的将新功能附加到对象ocp 原则

原理

  • 装饰者模式就像打包一件快递
    • 主体:比如:衣服 被装饰者
    • 包装:比如:报纸填充物、塑料泡沫、纸板、木板等等 装饰者
  • Component:主体:比如 Drink
  • ConcreteComponent 和 Decorator
    • ConcreteComponent:具体的主体:例如 单品咖啡
    • Decorator:装饰者,比如各种调料
  • 我们会发现一个特点:装饰者聚合了被装饰者,而不是被装饰者聚合装饰者
  • 如果 抽象主体(Component)与 具体主体(ConcreteComponent)类很多,还可以设计一个缓冲层,将共有的部分提取出来,抽象成一个类

看代码感受感受:确实保证了程序的可扩展、可维护

5、装饰器模式应用:I/O

Java 的 I/O 结构,FilterInputStream 就是一个装饰者

  • public abstract class InputStream implement Closeable
    • 抽象类,即 Component 主体
  • public class FilterInputStream extends InputStream
    • 是一个装饰器类:Decorator
    • { protected volatile InputStream in } 被装饰者
  • class DataInputStream extends FilterInputStream implements DataInput
    • FilterInputStream 的子类,继承了被装饰者 in,即:ConcreteDecorator

6、装饰器模式的应用场景

装饰器模式通常在以下几种情况使用。

  • 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
  • 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰器模式却很好实现。
  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

装饰器模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。

下面代码是为 FileReader 增加缓冲区而采用的装饰类 BufferedReader 的例子:

BufferedReader in = new BufferedReader(new FileReader("filename.txt"));
String s = in.readLine();

7、装饰器模式的扩展

装饰器模式所包含的 4 个角色不是任何时候都要存在的,在有些应用环境下模式是可以简化的,如以下两种情况。

  • 如果只有一个具体构件而没有抽象构件时,可以让抽象装饰继承具体构件
    • 即 ConcreteComponment 充当 Componment
  • 如果只有一个具体装饰时,可以将抽象装饰和具体装饰合并
  • 即 ConcreteDecorator 充当 Decorator

二、组合模式

在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣服与衣柜、以及厨房中的锅碗瓢盆等。在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。

1、定义与特点

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

组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点

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

这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。

优点和缺点

  • 优点
    • 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
    • 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
  • 缺点:
    • 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
    • 不容易限制容器中的构件;
    • 不容易用继承的方法来增加构件的新功能;

2、模式的结构和实现

模式的结构

组合模式主要包含以下主要角色:

  • 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
  • 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。
  • 树枝构件(Composite)角色 / 中间构件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。

组合模式分为 透明式 的组合模式和 安全式 的组合模式。

3、案例分析

学校院系展示需求:

要在每一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系。

传统解决方案

使用继承的关系,按 组织大小 分层

这种方法不方便管理

改进:把 学校、学院、系 都看作是 组织结构,它们之间没有继承关系,而是一个树型的结构,可以很方便的实现管理操作。 (组合模式)

基本介绍

  • 组合模式(Composite Pattern),又叫 部分整体模式,它创建了对象组的属性结构,将对象组合成树状结构以表示 “整体 - 部分” 的层次关系
  • 组合模式依据树形结构来组合对象,用来表示部分以及整体层次
  • 这种类型的设计模式属于结构型模式
  • 组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象

解决的问题:

  • 组合模式解决这样的问题:当我们要处理的对象可以生成树形结构,而我们要对树上的节点和叶子进行操作时,它能够提供一致的方式,而不用考虑它是节点还是叶子

4、分类

透明方式

在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild() 方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题。

在这个打印学院信息的案例中使用的就是透明式的组合模式,只不过抽象组件定义为类,在抽象组件中可以这样写:

protected void add(OrganizationComponent organizationComponent) {
    // 默认实现
    throw new UnsupportedOperationException();
}

protected void  remove(OrganizationComponent organizationComponent) {
    // 默认实现
    throw new UnsupportedOperationException();
}

安全方式

在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性。

5、JDK 源码应用:HashMap

  • Java 的集合类 HashMap 使用了组合模式

分析:

  • ```
    Map 就是一个抽象构件(类似 Component)

    
    - ```
      HashMap 是一个中间的构件(Composite),实现了相关方法,put、putAll
  • Node 是 HashMap 的静态内部类(类似 LeafNode),它没有 put、putAll 等等方法
    

6、应用场景和扩展

应用场景:

  • 在需要表示一个对象整体与部分的层次结构的场合。
  • 要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。

扩展:

  • 如果对前面介绍的组合模式中的树叶节点和树枝节点进行抽象,也就是说树叶节点和树枝节点还有子节点,这时组合模式就扩展成复杂的组合模式了
  • 如 Java AWT/Swing 中的简单组件 JTextComponent 有子类 JTextField、JTextArea,容器组件 Container 也有子类 Window、Panel。

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC