[设计模式] 装饰者模式

Posted1 by 呼延十 on January 11, 2019 Hot:

一句话总结

通过继承自同一父类,来实现给某一个类动态的添加新的职责,原理是每一个装饰者持有被装饰者的实例,并可以用自身替代他.

前言

本文写于阅读《Head First 设计模式》第三章之后,因此文中举例大部分是”复盘”书中所写,以起到加深理解和记忆的作用.

介绍

定义

装饰模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

设计原则

  1. 多组合,少继承。
  2. 类应设计的对扩展开放,对修改关闭。

类图

举个栗子(以书中”星巴慈咖啡”为例)

其实日常我们经常会去一些饮品店买饮料,咖啡等,有没有想过这个订单系统是如何实现的呢?

项目背景: 店里有多种饮料(咖啡,茶等),还有多种调料(奶,糖,椰果,珍珠等),比较暴躁的方法当然是遍历一下所有的,,比如新建”双份奶一糖0椰果2珍珠奶茶”等等好多类,然后根据用户的需求下订单,但是这也太蠢了8.

而装饰者可谓是及其适用这个场景了.

首先,将咖啡,茶等饮料定义为被装饰者,奶,糖,椰果,珍珠等定义为装饰者,当用户下单时,用户购买了一个”被装饰者”以及若干个“装饰者”,我们只需要将奶,糖,椰果,珍珠等东西装饰在被用户购买的这个被装饰者上即可.

1. 定义基类

在装饰者模式中,被装饰者和装饰者继承自同一个基类,我们定义为Component,每个Component有自己的名字以及价钱.

public class Component {

  private String name;
  private float prize;

  public String getName() {
    return name;
  }

  public float getPrize() {
    return prize;
  }

}

2. 定义咖啡类(本文被装饰者只实现咖啡,装饰者只实现’奶’,’糖’两个)

在构造方法中添加咖啡的名字以及价格.

public class Coffee extends Component {


  public Coffee() {
    this.name = "coffee";
    this.prize = 10.0f;
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public float getPrize() {
    return prize;
  }
}

3.定义”装饰者”类

其实在这里有一问题,装饰者被装饰者同样继承自Component,为什么被装饰者直接继承,而装饰者却还需要再写一个子类呢?

我的理解是,对咖啡来说,名字及价格直接返回自身的名字及价格即可,与Component定义相同,而对于装饰者Decorator来说,需要再这个类中,重写返回名字及价格的方法,在返回名字时,将他所持有的饮料实例的名字也进行返回.

比如我们希望拿到的是1份奶+咖啡或者咖啡,而不是1份奶这样单独的价格.

public class Decorator extends Component {

  protected Component component;

  @Override
  public String getName() {
    return name + "," + component.getName();
  }

  @Override
  public float getPrize() {
    return prize + component.getPrize();
  }

  public Decorator(Component component) {
    this.component = component;
  }
}

4. 定义奶和糖

public class Milk extends Decorator {

  public Milk(Component component) {
    super(component);
    this.name = "Milk";
    this.prize = 1.0f;
  }

}
public class Sugar extends Decorator {

  public Sugar(Component component) {
    super(component);
    this.name = "Sugar";
    this.prize = 2.0f;
  }
}

测试代码-生成订单

好,现在基本的定义已经完成,我们在测试类里进行一次下单的模拟.

假设用户需要的是:2份糖1份奶的咖啡.

public static void main(String[] args) {
  //一杯咖啡
  Component coffee = new Coffee();
  //加一份奶
  coffee = new Milk(coffee);
  //再加一份奶
  coffee = new Milk(coffee);
  //加一份糖
  coffee = new Sugar(coffee);
  //将最后的总价值及所有名字列出来
  System.out.print(coffee.getName() + coffee.getPrize());

}

上述代码的输出结果为:

Sugar,Milk,Milk,coffee14.0

这样的实现方法在类少的时候看不出来优势,甚至有点麻烦,但是普通的饮料店,饮料种类动辄几十种,粗暴方法肯定是解决不了的.

而使用装饰者模式,可以很轻松的处理各种附加要求.

特点

通过上面的例子,我们可以总结一下装饰者模式的特点。 (1)装饰者和被装饰者有相同的接口(或有相同的父类)。 (2)装饰者保存了一个被装饰者的引用。 (3)装饰者接受所有客户端的请求,并且这些请求最终都会返回给被装饰者(参见韦恩图)。 (4)在运行时动态地为对象添加属性,不必改变对象的结构。

优缺点

优点

  1. 扩展性好
  2. 符合开闭原则

缺点

  1. 会有许多的装饰类,导致程序复杂性提高

装饰者模式在JDK中的应用

在书中介绍完”星巴慈咖啡”的例子后,提到了在java.io包中大量使用了装饰者模式,这里对io包内的FileInputStream类及其相关类进行一个了解及学习.

这是我画的一个InputStream相关类的类图,当然没有画完整,但是已经足够用了.

在图中,InputStream是所有类的基类,相当于Component.

左边的FileInputStream,StringBufferInputStream等是具体的被装饰者,相当于Coffee.

右侧的FilterInputStream是一个抽象的装饰者,相当于Decorator.

继承自FilterInputStreamBufferedInputStreamDataInputStream就是具体的装饰者了.

可以理解为,我们拿到一个输入流,这个输入流负责从不同的数据源读取到数据,之后我们以各种装饰者装饰它,可以为其加上各种功能,比如,将读取到的每一个大写字符转换成小写字符.

那么我们就实现这个装饰器吧!

public class CuteInputStream extends FilterInputStream {

  /**
   * Creates a <code>FilterInputStream</code> by assigning the  argument <code>in</code> to the
   * field
   * <code>this.in</code> so as to remember it for later use.
   *
   * @param in the underlying input stream, or <code>null</code> if this instance is to be created
   * without an underlying stream.
   */
  protected CuteInputStream(InputStream in) {
    super(in);
  }

  @Override
  public int read() throws IOException {
    int i = super.read();
    return Character.toLowerCase((char) i);
  }

  @Override
  public int read(byte[] b, int off, int len) throws IOException {
    int i = super.read(b, off, len);
    return Character.toLowerCase((char) i);
  }
}

测试代码:

InputStream in = new CuteInputStream(new FileInputStream("/Users/pfliu/study/test/test.txt"));
int c;
try{
  while ( (c=in.read()) >=0){
    System.out.print((char)(c));
  }
  in.close();
}catch (Exception e){
  e.printStackTrace();
}

完。





ChangeLog

2019-01-06 完成

以上皆为个人所思所得,如有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文链接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见个人博客——>呼延十