1.案例引入

这里引用(博主 木瓜芒果)的例子,里脊肉夹馍是我们常见的一种小吃,这里使用里脊肉夹馍为例介绍装饰器模式。

1.1情景模型

摊主在肉夹馍中可以加入里脊(便宜), 鸡蛋、烤肠……,对这个有些印象,摊主每次都是根据顾客定制的需求来算价格的然后将定制的肉夹馍提供给用户。

1.2抽象模型

对于里脊肉夹馍的价格问题也是可以抽象成类图来表示:
image.png
定义一个抽象类ChineseHamburger代表肉夹馍,小摊卖的所有夹馍都需继承自此类,有两个方法:

  • getDescription(),抽象方法,可以返回是什么肉夹馍,由子类实现;
  • cost()方法是抽象的,由子类来实现;

  FilletChineseHamburger继承自ChineseHamburger,代表里脊肉夹馍,实现cost()方法来返回肉夹馍的价格。
  好了,这只是最简单的模型,我们常常会有比如加个鸡蛋、加根烤肠等等需求,对应肉夹馍的价格也是不一样的,这样怎么办呢,我们可以直接增加几个子类代表对应的夹馍,这时候类图就像下面这样了:
5-装饰器模式 - 图2
这样看起来是一种简单的实现方式,但是如果我们要引入新品,或者多种配菜搭配起来,这样排列组合形成的子类就会爆炸,而且后期的维护也比较头疼。

1.3模型改进

以夹馍为主体,然后在运行时用材料来“装饰”肉夹馍。比如说,如果顾客想要里脊鸡蛋肉夹馍,那么,可以这样,先来一个夹馍,以里脊对象装饰它,再以鸡蛋对象装饰它,调用cost()方法,里面会依赖委托将所材料的加钱加上去。这样,每次有不一样的需求,只需要将对应的材料进行装饰即可,类似如下的步骤:
5-装饰器模式 - 图3

  我们可以用类图抽象一下来表示:
5-装饰器模式 - 图4
  如上图,我们定义一个普通夹馍(SimpleChineseHamburger)。再定义一个装饰器Decorator,其包含一个夹馍对象,并可以对其进行“装饰”(就是委托其进行计价),这样一来我们每多加一种材料,只需要多装饰一次即可,避免了重复设计大量的相似类。这就是装饰器模式的应用。

2.定义 —是什么?

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
1.创建了一个装饰类,用来包装原有的类,
2.保持类方法签名完整性的前提下,提供了额外的功能。
装饰器模式的说明:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

3.介绍 —为什么?

主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
何时使用:在不想增加很多子类的情况下扩展类。
如何解决:将具体功能职责划分,同时继承装饰者模式。
关键代码: 1、Component 类充当抽象角色,不应该具体实现。 2、修饰类引用和继承 Component 类,具体扩展类重写父类方法。
应用实例: 1、孙悟空有 72 变,当他变成”庙宇”后,他的根本还是一只猴子,但是他又有了庙宇的功能。 2、不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
使用场景: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。
注意事项:可代替继承。

4.实现方式 —有什么用?

例子:我买了一个机器人,它拥有唱歌,对话,放音乐的功能,但是我希望它能够拥有更多的功能,我希望他可以帮助我扫地,唱歌的时候可以跳舞等。于是我联系了厂家,厂家说目前机器人只有这三个功能,不过再研究第二代产品了,是在第一代产品的基础上进行了修改和升级,拥有了用于行走的滑轮,和灵活的两只手,这个第二代机器人可以帮助我们进行扫地/… 。
但是有个手工大师张三,感觉没有必要等第二代产品,他自己就可以对第一代进行改进。他将第一代的产品在外层加了一层桶装包装,然后直接在桶上安装滑轮和两只手,来完成我们的需要。自己进行灵活的扩展,不需要等待厂家的重新研究设计。

  • 第一种机制:继承机制,通过继承父类,在子类中实现功能的拓展。
  • 第二中机制:关联机制,把一个类对象嵌入到另一个类对象,相当于把机器人嵌入到桶里,来拓展功能。

桶就是装饰器
两种机制在我们日常的开发中都经常使用,
第一种是一种静态的方式,一定要写一个新的子类,对父类功能进行扩展。
第二种机制是动态的,我们拿到其中的对象就可以对其进行扩展,不需要修改原有逻辑。
image.png
在类图中,各个角色的说明如下:

Component,抽象构件

  Component是一个接口或者抽象类,是定义我们最核心的对象,也可以说是最原始的对象,比如上面的肉夹馍,机器人。

ConcreteComponent,具体构件,或者基础构件

  ConcreteComponent是最核心、最原始、最基本的接口或抽象类Component的实现,可以单独用,也可将其进行装饰,比如上面的简单肉夹馍,第一代机器人。

Decorator,装饰角色

  一般是一个抽象类,继承自或实现Component,在它的属性里面有一个变量指向Component抽象构件,我觉得这是装饰器最关键的地方。

ConcreteDecorator,具体装饰角色

  ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,它们可以把基础构件装饰成新的东西,比如把一个普通肉夹馍装饰成鸡蛋里脊肉夹馍,有手有脚的机器人。

  1. package decorator;
  2. public class DecoratorPattern {
  3. public static void main(String[] args) {
  4. new RobotDecorator(new FirstRobot()).domorething();
  5. }
  6. }
  7. //Component,抽象构件 --机器人
  8. interface Robot{
  9. void dosomething();
  10. }
  11. //ConcreteComponent,具体构件 --第一代机器人
  12. class FirstRobot implements Robot{
  13. @Override
  14. public void dosomething() {
  15. System.out.println("放音乐");
  16. System.out.println("对话");
  17. }
  18. }
  19. //ConcreteDecorator,具体装饰角色 --第二代机器人
  20. class RobotDecorator implements Robot{
  21. private Robot robot;
  22. public RobotDecorator(Robot robot) {
  23. this.robot = robot;
  24. }
  25. @Override
  26. public void dosomething() {
  27. robot.dosomething();
  28. }
  29. public void domorething(){
  30. robot.dosomething();
  31. System.out.println("扫地");
  32. System.out.println("跳舞");
  33. }
  34. }

这里没有将装饰器写成抽象模式,是一种简化模式。将具体的装饰对象实现了抽象接口,除了原有方法可以添加扩展功能。

案例二
创建 Component,抽象构件

  1. public abstract class Component{
  2. // 抽象地方法
  3. public abstract void cost();
  4. }

ConcreteComponent,具体构件

  1. public class ConcreteComponent extends Component{
  2. @Override
  3. public void cost(){
  4. // do something ...
  5. }
  6. }

抽象装饰角色:

  1. public abstract class Decorator extends Component{
  2. private Component component = null;
  3. public Decorator(Component component){
  4. this.component = component;
  5. }
  6. @Override
  7. public void cost(){
  8. this.component.cost();
  9. }
  10. }

 具体装饰角色:

  1. public class ConcreteDecorator extends Decorator{
  2. public ConcreteDecorator(Component component){
  3. super(component);
  4. }
  5. // 定义自己的修饰逻辑
  6. private void decorateMethod(){
  7. // do somethind ...
  8. }
  9. // 重写父类的方法
  10. public void cost(){
  11. this.decorateMethod();
  12. super.cost();
  13. }
  14. }
  1. public class DecoratorDemo{
  2. public static void main(String[] args){
  3. Component component = new ConcreteComponent();
  4. // 第一次修饰,比如,加鸡蛋,加1块
  5. component = new ConcreteDecorator(component);
  6. // 第二次修饰,比如,加烤肠,加2块
  7. component = new ConcreteDecorator(component);
  8. // 修饰后运行,将钱加在一起
  9. component.cost();
  10. }
  11. }

5.I/O应用

在平时的留意中我发现Java I/O系统的设计中用到了这一设计模式,因为Java I/O类库需要多种不同功能的组合。这里我就以InputStream为例简单说明一下,同样我们还是来看一下其类图:
5-装饰器模式 - 图6
  InputStream作为抽象构件,其下面大约有如下几种具体基础构件,从不同的数据源产生输入:

  • ByteArrayInputStream,从字节数组产生输入;
  • FileInputStream,从文件产生输入;
  • StringBufferInputStream,从String对象产生输入;
  • PipedInputStream,从管道产生输入;
  • SequenceInputStream,可将其他流收集合并到一个流内;

  FilterInputStream作为装饰器在JDK中是一个普通类,其下面有多个具体装饰器比如BufferedInputStream、DataInputStream等。我们以BufferedInputStream为例,使用它就是避免每次读取时都进行实际的写操作,起着缓冲作用。我们可以在这里稍微深入一下,站在源码的角度来管中窥豹。
  FilterInputStream内部封装了基础构件:

  1. protected volatile InputStream in;

  而BufferedInputStream在调用其read()读取数据时会委托基础构件来进行更底层的操作,而它自己所起的装饰作用就是缓冲,

6.总结:

  • 装饰类和被装饰类可以独立发展,而不会相互耦合。换句话说,Component类无需知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。
  • 装饰器模式是继承关系的一个替代方案。我们看装饰类Decorator,不管装饰多少层,返回的对象还是Component(因为Decorator本身就是继承自Component的),实现的还是is-a的关系。
  • 装饰模式可以动态地扩展一个实现类的功能,比如在I/O系统中,我们直接给BufferedInputStream的构造器直接传一个InputStream就可以轻松构件一个带缓冲的输入流,如果需要扩展,我们继续“装饰”即可。