前言
装饰器模式可以动态地给对象加上一些职责,比起继承更加的灵活(文章后面慢慢体会)。在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。
在学习具体概念之前,我们先看熟悉的内容 — — Java IO 中装饰器的使用。
Java IO 装饰器模式使用例子:
// 获取文件内容作为 InputStream
InputStream inputStream = new FileInputStream("/root/t.txt");
// 装饰 InputStream, 获取缓冲的能力
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
// 装饰 InputStream, 获取计算 CheckSum 能力
CheckedInputStream checkedInputStream = new CheckedInputStream(inputStream, new Adler32());
// 获取带缓存并可计算 CheckSum 的 InputStream
CheckedInputStream bufferCheckedInputStream = new CheckedInputStream(bufferedInputStream, new Adler32());
InputStream 可以使用 BufferedInputStream 装饰获取缓存和标记的能力;使用 CheckedInputStream 装饰获取 checksum 计算的能力,;同时使用两者获得缓存与 checksum 计算的能力。
Java 提供类似的装饰器还有很多,有兴趣的可以去查查文档。
上述代码中,bufferCheckedInputStream 既可以使用 InputStream 的方法功能,又新增了 CheckSum 计算的功能和缓存,这就体现了装饰器的灵活之处。
实例
所谓实践出真知,我们通过一个实际的例子来体会一下装饰器模式的好处。
比如一家咖啡店要做一个系统去出售咖啡。
直觉我们可以设计出 订单系统1.0,下面是类图。
抽象出一个 Cafe 接口,所有咖啡都继承它。
随着顾客的增多,需求有了变化,有很多顾客需要牛奶巧克力咖啡、 抹茶牛奶咖啡、奶泡咖啡、抹茶奶泡咖啡等,所以要更新一下系统。
然后就有了 订单系统 2.0
2.0 看似解决了问题,但其实并不然,客户的需求会不断的改变,所以后续类图将变得极为庞大。并且如果有客户需要一种 两倍奶泡的咖啡 的时候,系统就无能为力了。
下面就看看装饰者模式如何去解决这个问题吧。
设计的根本在于分出了一个配料的概念,因为牛奶、巧克力等对于咖啡来说只是附加的内容,这样的设计就可以很灵活地搭配出不同种类的咖啡(只要使用不同的配料修饰就好了)。
看下使用效果:
Main.java 测试类
import java.util.Locale;
/**
* 测试
* Created by jay on 19/01/2017.
*/
public class Main {
public static void main(String[] args) {
Cafe normalCafe = new NormalCafe();
// 巧克力咖啡
ChocolateCafe chocolateCafe = new ChocolateCafe(normalCafe);
printCafe(chocolateCafe);
// 牛奶咖啡
MikeCafe mikeCafe = new MikeCafe(normalCafe);
printCafe(mikeCafe);
// 牛奶巧克力咖啡
MikeCafe mikeChocolateCafe = new MikeCafe(chocolateCafe);
printCafe(mikeChocolateCafe);
// 两倍牛奶咖啡
MikeCafe doubleMikeCafe = new MikeCafe(mikeCafe);
printCafe(doubleMikeCafe);
// 三倍牛奶咖啡
MikeCafe tripleMikeCafe = new MikeCafe(doubleMikeCafe);
printCafe(tripleMikeCafe);
// ... 你还可以随心所欲搭配出你自己喜欢的咖啡~
}
private static void printCafe(Cafe cafe){
System.out.println(String.format(Locale.getDefault(), "Description: %s, price: $%.2f", cafe.description(), cafe.cost()));
}
}
测试结果:
Description: Chocolate Cafe, price: $5.55
Description: Mike Cafe, price: $4.55
Description: Mike Chocolate Cafe, price: $7.88
Description: Mike Mike Cafe, price: $6.88
Description: Mike Mike Mike Cafe, price: $9.21
Process finished with exit code 0
下面通过代码分析一下:
所有的类均实现了 Cafe 接口,所以所有类都可以当作 Cafe 使用
Cafe类定义了咖啡共有的一些方法,包括价格与描述,所有的子类都必须实现它。
重点在于 Composition 类,下面就来看看 Composition 的具体实现:
/**
* 配料基类
* Created by jay on 17/01/2017.
*/
public abstract class Composition implements Cafe {
protected Cafe cafe;
public Composition(Cafe cafe) {
this.cafe = cafe;
}
}
可以看出这是一个抽象类(没去实现 cost 跟 description 方法), 重点在于该类持有了一个 Cafe 对象的引用(一般通过构造函数的方式传入), Cafe 对象的作用看具体的 Composition 实现类
MikeCafe.java
/**
* 牛奶装饰器
* Created by jay on 17/01/2017.
*/
public class MikeCafe extends Composition {
private static final double PRICE = 2.33;
public MikeCafe(Cafe cafe) {
super(cafe);
}
@Override
public String description() {
return "Mike " + cafe.description();
}
@Override
public double cost() {
return cafe.cost() + PRICE;
}
}
从上述代码可以看出,牛奶装饰器对象实现了 cost 跟 description 方法,并且在实现过程中调用了被装饰对象 cafe 的方法,所以可以实现装饰的效果。
其余代码均类似,具体完整源代码参看 https://github.com/liujiescut/DecoratorDemo.git
这样,我们就比较完整地实现了一个灵活的订单系统。
概念
从上面一个例子我们先体会了装饰器模式的灵活之处,下面就来抽象一下,认识一下装饰器的的定义和相关术语。
装饰器模式有几个概念(参考来源: http://blog.youkuaiyun.com/yanbober/article/details/45395747):
Component(抽象构件)
它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。也就是上面的 Cafe 类,客户端不需要知道 Cafe 类具体是什么,只需要调用相应的 cost、 description 方法即可。
ConcreteComponent(具体构件)
它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。也就是上面 NormalCafe 类,实现了未被装饰的咖啡类。
Decorator(抽象装饰类)
它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。 也就是上面 Composition 类。
ConcreteDecorator(具体装饰类)
它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。也就是上述 MikeCafe、ChocolateCafe类等。
整体UML类图,可以细细体会下~