装饰者模式
每天我们出门前,一定都会选择今天上衣穿什么,裤子穿什么,搭配什么鞋子,大衣穿什么。最后一定是做好选择,打扮好才会出门。这个过程其实就是装饰者模要做的事情 ---- 对一个对象增加额外的功能。
我们再看一个例子。我们都吃过煎饼,除了面饼之外,我们还要加鸡蛋、加葱花、香菜、面酱、辣酱。现在还有新花样,加辣条、加鸡柳。一切都始于一张面饼,摊煎饼的过程就是在不断对这张面饼添加新特性。
我们通过继承也可以为对象增加功能,比如我们有个煎饼的父类,默认已经有面饼、面酱、鸡蛋啊。那么我们可以派生出 全都放的普通煎饼、不辣的普通煎饼、不辣不放香菜的普通煎饼、不辣不放葱的普通煎饼、全都放的辣条煎饼、全都放的鸡柳煎饼…… 这只是很小一部分。通过继承的话,由于情况太多,会造成对象爆炸。
那我们还可以通过组合的方式来扩展类啊,比如煎饼对象中,我们可以设置不同属性,比如是否有葱、是否有香菜、是否有辣条、是否有鸡柳等等。这样看起来也能很好的解决摊煎饼的问题。但如果想要加肠、加油条怎么办?想要加两个鸡蛋怎么办?我们只能修改煎饼对象。这就违反了开闭原则。显然这样也是不够灵活的。
装饰者模式能够很好的解决对象的动态扩展,不管你想穿什么,都可以随便搭配。不过这个煎饼要怎么做,也都能随意的扩展支持,而不需要改已有的代码。接下来我们就来看看如何通过装饰者模式来摊煎饼的。
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 优点
- 动态的为对象添加额外职责:通过组合不同装饰类,非常灵活的为对象增加额外的职责;
- 避免子类爆炸:当不同的特性组合,构成不同的子类时,必然造成子类爆炸。但通过装饰者灵活组合,可以避免这个问;
- 分离核心功能和装饰功能:核心业务保留在
Component
的子类中。而装饰特性在Decorator
的实现类中去实现。面对装饰特性的变化,实现了开闭原则,只需要增加装饰实现类; - 很方便的重复添加特性:我想要一套两个鸡蛋,双份辣条的煎饼。是不是只需要多装饰一次就可以了?就是这么简单。
2.2 缺点
- 由于不是通过继承实现添加职责,所以被装饰后的对象并不能通过对象本身就能了解其特性。而需要分析所有对其装饰过的对象;
- 装饰模式会造成有很多功能类似的小对象。通过组合不同的装饰实现,来达成不同的需求。这样对于不了解系统的人,比较难以学习。过多的装饰类进行装饰,也稍显繁琐。
3. 装饰者模式适用场景
使用装饰者模式,有以下几种情况:
- 需要一个装饰的载体。不能将全部特性都放在装饰类中。换句话讲得有个装饰主体,核心特性在主体对象中实现。例如浏览器窗口,不管是加边框还是滚动条,都是基于窗口的;
- 有多种特性可以任意搭配,对主体进行扩展。并且你想以动态、透明的方式来实;
- 不能以生成子类的方式扩展。可能有两种情况,一是对大量子类带来的类爆炸有所顾虑。二是类定义被隐藏,或者不能用于生成子类。
4. 小结
装饰者模式的优势在于动态、透明的添加特性。要记住装饰者装饰完的对象还是之前的对象类型。通过分离核心特性和装饰特性,客户端代码可以灵活的搭配使用包装对象,从而得到具有想要行为的对象。不过要注意,有些时候装饰的顺序是要保证的。比如先放鸡蛋,再放芝麻,芝麻就不会掉下去了。最好的做法是保证装饰类的独立。