每天我们出门前,一定都会选择今天上衣穿什么,裤子穿什么,搭配什么鞋子,大衣穿什么。最后一定是做好选择,打扮好才会出门。这个过程其实就是装饰者模要做的事情 —— 对一个对象增加额外的功能。
我们再看一个例子。我们都吃过煎饼,除了面饼之外,我们还要加鸡蛋、加葱花、香菜、面酱、辣酱。现在还有新花样,加辣条、加鸡柳。一切都始于一张面饼,摊煎饼的过程就是在不断对这张面饼添加新特性。
装饰者模式 - 图1
我们通过继承也可以为对象增加功能,比如我们有个煎饼的父类,默认已经有面饼、面酱、鸡蛋啊。那么我们可以派生出 全都放的普通煎饼、不辣的普通煎饼、不辣不放香菜的普通煎饼、不辣不放葱的普通煎饼、全都放的辣条煎饼、全都放的鸡柳煎饼…… 这只是很小一部分。通过继承的话,由于情况太多,会造成对象爆炸。
那我们还可以通过组合的方式来扩展类啊,比如煎饼对象中,我们可以设置不同属性,比如是否有葱、是否有香菜、是否有辣条、是否有鸡柳等等。这样看起来也能很好的解决摊煎饼的问题。但如果想要加肠、加油条怎么办?想要加两个鸡蛋怎么办?我们只能修改煎饼对象。这就违反了开闭原则。显然这样也是不够灵活的。
装饰者模式能够很好的解决对象的动态扩展,不管你想穿什么,都可以随便搭配。不过这个煎饼要怎么做,也都能随意的扩展支持,而不需要改已有的代码。接下来我们就来看看如何通过装饰者模式来摊煎饼的。

1. 实现装饰者模式

对于摊煎饼来说,我们都是对于一个基础的煎饼对象做装饰,比如我想要一套两个鸡蛋、有辣椒、葱、辣条的煎饼,那么我只需要先声明一个基本的煎饼对象,然后用加鸡蛋装饰类装饰它,然后再用加辣酱装饰类装饰它,再用加葱的装饰类装饰它,最后再用加辣条的装饰类装饰它。最终就得到了我想要的煎饼。不过请注意,不管你怎么装饰,最终得到的还是煎饼,并不是其他东西。
装饰者模式的核心思想是对已有的对象,一层一层的用装饰类去装饰它,扩展它的特性。这样做可以更为动态的为对象增加功能。我们看看代码如何实现:
先定义煎饼接口:

  1. public interface Pancake {
  2. void cook();
  3. }

接口里只定义了一个制作方法。
煎饼接口的实现类:

  1. public class BasicPancake implements Pancake {
  2. @Override
  3. public void cook() {
  4. System.out.println("加一勺面");
  5. System.out.println("加一个鸡蛋");
  6. }
  7. }

作为一个最基本的煎饼,总得有面,有鸡蛋吧。其他的材料留给装饰类来实现。
接下来我们定义装饰抽象类:

  1. public abstract class PancakeDecorator implements Pancake {
  2. protected Pancake pancake;
  3. public void setPancake(Pancake pancake) {
  4. this.pancake = pancake;
  5. }
  6. public void cook() {
  7. if (pancake != null) {
  8. pancake.cook();
  9. }
  10. }
  11. }

可以看到 PancakeDecorator 同样要实现 Pancke 接口。并且持有 Pancke 类型的引用,自己实现的 cook 方法实际调用了持有的 Pancake 对象的 cook 方法。
加辣酱的装饰类代码如下,其他装饰实现类是类似的。

  1. public class AddSpicyDecorator extends PancakeDecorator{
  2. @Override
  3. public void cook(){
  4. super.cook();
  5. System.out.println("加辣酱");
  6. }
  7. }

cook 方法首先调父类的 cook 方法,然后再加入自己的特性。
客户端代码如下,我们看看如何利用装饰类来生成你想要的煎饼。

  1. public class Client {
  2. public static void main(String[] args) {
  3. Pancake pancake = new BasicPancake();
  4. PancakeDecorator addEggPancake = new AddEggDecorator();
  5. addEggPancake.setPancake(pancake);
  6. PancakeDecorator addSaucePancake = new AddSauceDecorator();
  7. addSaucePancake.setPancake(addEggPancake);
  8. PancakeDecorator addLaTiaoPancake = new AddLaTiaoDecorator();
  9. addLaTiaoPancake.setPancake(addSaucePancake);
  10. addLaTiaoPancake.cook();
  11. }
  12. }

我们声明了三个包装类,对 BasicPancake 层层包装,最后得到一套两个鸡蛋、加辣酱、加辣条的煎饼。运行后输出如下:

  1. 加一勺面
  2. 加一个鸡蛋
  3. 加一个鸡蛋
  4. 加面酱
  5. 加辣条

如果你研发了新煎饼,要加新的辅料,比如香肠、榨菜之类,那么只需要增加装饰类的实现即可。从而实现了开闭原则。
类图如下:装饰者模式 - 图2

2. 装饰者模式优缺点

2.1 优点

  1. 动态的为对象添加额外职责:通过组合不同装饰类,非常灵活的为对象增加额外的职责;
  2. 避免子类爆炸:当不同的特性组合,构成不同的子类时,必然造成子类爆炸。但通过装饰者灵活组合,可以避免这个问;
  3. 分离核心功能和装饰功能:核心业务保留在 Component 的子类中。而装饰特性在 Decorator 的实现类中去实现。面对装饰特性的变化,实现了开闭原则,只需要增加装饰实现类;
  4. 很方便的重复添加特性:我想要一套两个鸡蛋,双份辣条的煎饼。是不是只需要多装饰一次就可以了?就是这么简单。

    2.2 缺点

  5. 由于不是通过继承实现添加职责,所以被装饰后的对象并不能通过对象本身就能了解其特性。而需要分析所有对其装饰过的对象;

  6. 装饰模式会造成有很多功能类似的小对象。通过组合不同的装饰实现,来达成不同的需求。这样对于不了解系统的人,比较难以学习。过多的装饰类进行装饰,也稍显繁琐。

    3. 装饰者模式适用场景

    使用装饰者模式,有以下几种情况:

  7. 需要一个装饰的载体。不能将全部特性都放在装饰类中。换句话讲得有个装饰主体,核心特性在主体对象中实现。例如浏览器窗口,不管是加边框还是滚动条,都是基于窗口的;

  8. 有多种特性可以任意搭配,对主体进行扩展。并且你想以动态、透明的方式来实;
  9. 不能以生成子类的方式扩展。可能有两种情况,一是对大量子类带来的类爆炸有所顾虑。二是类定义被隐藏,或者不能用于生成子类。

    4. 小结

    装饰者模式的优势在于动态、透明的添加特性。要记住装饰者装饰完的对象还是之前的对象类型。通过分离核心特性和装饰特性,客户端代码可以灵活的搭配使用包装对象,从而得到具有想要行为的对象。不过要注意,有些时候装饰的顺序是要保证的。比如先放鸡蛋,再放芝麻,芝麻就不会掉下去了。最好的做法是保证装饰类的独立。