装饰者模式

每天我们出门前,一定都会选择今天上衣穿什么,裤子穿什么,搭配什么鞋子,大衣穿什么。最后一定是做好选择,打扮好才会出门。这个过程其实就是装饰者模要做的事情 ---- 对一个对象增加额外的功能。

我们再看一个例子。我们都吃过煎饼,除了面饼之外,我们还要加鸡蛋、加葱花、香菜、面酱、辣酱。现在还有新花样,加辣条、加鸡柳。一切都始于一张面饼,摊煎饼的过程就是在不断对这张面饼添加新特性。
图片描述

我们通过继承也可以为对象增加功能,比如我们有个煎饼的父类,默认已经有面饼、面酱、鸡蛋啊。那么我们可以派生出 全都放的普通煎饼、不辣的普通煎饼、不辣不放香菜的普通煎饼、不辣不放葱的普通煎饼、全都放的辣条煎饼、全都放的鸡柳煎饼…… 这只是很小一部分。通过继承的话,由于情况太多,会造成对象爆炸。

那我们还可以通过组合的方式来扩展类啊,比如煎饼对象中,我们可以设置不同属性,比如是否有葱、是否有香菜、是否有辣条、是否有鸡柳等等。这样看起来也能很好的解决摊煎饼的问题。但如果想要加肠、加油条怎么办?想要加两个鸡蛋怎么办?我们只能修改煎饼对象。这就违反了开闭原则。显然这样也是不够灵活的。

装饰者模式能够很好的解决对象的动态扩展,不管你想穿什么,都可以随便搭配。不过这个煎饼要怎么做,也都能随意的扩展支持,而不需要改已有的代码。接下来我们就来看看如何通过装饰者模式来摊煎饼的。

1. 实现装饰者模式

对于摊煎饼来说,我们都是对于一个基础的煎饼对象做装饰,比如我想要一套两个鸡蛋、有辣椒、葱、辣条的煎饼,那么我只需要先声明一个基本的煎饼对象,然后用加鸡蛋装饰类装饰它,然后再用加辣酱装饰类装饰它,再用加葱的装饰类装饰它,最后再用加辣条的装饰类装饰它。最终就得到了我想要的煎饼。不过请注意,不管你怎么装饰,最终得到的还是煎饼,并不是其他东西。

装饰者模式的核心思想是对已有的对象,一层一层的用装饰类去装饰它,扩展它的特性。这样做可以更为动态的为对象增加功能。我们看看代码如何实现:

先定义煎饼接口:

public interface Pancake {
    void cook();
}

接口里只定义了一个制作方法。

煎饼接口的实现类:

public class BasicPancake implements Pancake {
    @Override
    public void cook() {
        System.out.println("加一勺面");
        System.out.println("加一个鸡蛋");
    }
}

作为一个最基本的煎饼,总得有面,有鸡蛋吧。其他的材料留给装饰类来实现。

接下来我们定义装饰抽象类:

public abstract class PancakeDecorator implements Pancake {
    protected Pancake pancake;

    public void setPancake(Pancake pancake) {
        this.pancake = pancake;
    }

    public void cook() {
        if (pancake != null) {
            pancake.cook();
        }
    }
}

可以看到 PancakeDecorator 同样要实现 Pancke 接口。并且持有 Pancke 类型的引用,自己实现的 cook 方法实际调用了持有的 Pancake 对象的 cook 方法。

加辣酱的装饰类代码如下,其他装饰实现类是类似的。

public class AddSpicyDecorator extends PancakeDecorator{
    @Override
    public void cook(){
        super.cook();
        System.out.println("加辣酱");
    }
}

cook 方法首先调父类的 cook 方法,然后再加入自己的特性。

客户端代码如下,我们看看如何利用装饰类来生成你想要的煎饼。

public class Client {
    public static void main(String[] args) {
        Pancake pancake = new BasicPancake();
        PancakeDecorator addEggPancake = new AddEggDecorator();
        addEggPancake.setPancake(pancake);

        PancakeDecorator addSaucePancake = new AddSauceDecorator();
        addSaucePancake.setPancake(addEggPancake);

        PancakeDecorator addLaTiaoPancake = new AddLaTiaoDecorator();
        addLaTiaoPancake.setPancake(addSaucePancake);

        addLaTiaoPancake.cook();
    }
}

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

加一勺面
加一个鸡蛋
加一个鸡蛋
加面酱
加辣条

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

类图如下:
图片描述

2. 装饰者模式优缺点

2.1 优点

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

2.2 缺点

  1. 由于不是通过继承实现添加职责,所以被装饰后的对象并不能通过对象本身就能了解其特性。而需要分析所有对其装饰过的对象;
  2. 装饰模式会造成有很多功能类似的小对象。通过组合不同的装饰实现,来达成不同的需求。这样对于不了解系统的人,比较难以学习。过多的装饰类进行装饰,也稍显繁琐。

3. 装饰者模式适用场景

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

  1. 需要一个装饰的载体。不能将全部特性都放在装饰类中。换句话讲得有个装饰主体,核心特性在主体对象中实现。例如浏览器窗口,不管是加边框还是滚动条,都是基于窗口的;
  2. 有多种特性可以任意搭配,对主体进行扩展。并且你想以动态、透明的方式来实;
  3. 不能以生成子类的方式扩展。可能有两种情况,一是对大量子类带来的类爆炸有所顾虑。二是类定义被隐藏,或者不能用于生成子类。

4. 小结

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