18、装饰器模式(Decorator Pattern)
1. 装饰器模式
1.1 简介
Decorator模式就是在不改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。这些功能需要由用户动态决定加入的方式和时机,Decorator提供了"即插即用"的方法,在运行期间决定何时增加何种功能。
通过创建一个包装对象(装饰),来包裹真实的对象。软件开发的某个阶段和装修房子像极了!系统的基本功能实现后,需要完善功能和修饰界面,但基本功能和流程框架不会做大的改动。房屋的装修,是在毛胚房的基础上层层 wrapper(包装),先刷刷墙面漆,再铺铺木地板,再购置家具布置一下等,可以看作是对毛胚房层层包装。所以Decorator 设计模式也被称为 Wrapper 设计模式。
1.2
Decorator 设计模式正如毛胚房的装修,不会改变原毛胚房的基本框架,只是增加新的外观、功能等,且随着时间的推移,可以不断的实施装修工程:增加新的家具、根据心情换换新鲜的墙纸等等。在面向对象的程序设计中,扩展系统的原有功能也可以采用继承、组合的方式。继承也不会改变毛胚房(父类),但是由于装修工程的复杂和很多不可预测的改变,比如不同墙纸和地板样式的组合数量简直无法想想,难道我们要为每一种组合都定义一个子类吗?显然这是不现实的,即通过继承的方式来应对未来的功能和外观改变通常是吃力不讨好的事情。组合的方式也不可取,因为这要求不断的修改父类的结构,相当于对毛胚房大动干戈,房屋的可维护性和可靠性就大大降低了。
让我们回顾一下设计模式的重要原则:Classes should be open for extenstion, but closed for modification。Decorator 设计模式很好的诠释了这个原则。
1.3 Decorator模式结构
Decorator模式uml:
Decorator模式角色:
- Component为统一接口,也是装饰类和被装饰类的基本类型。
- ConcreteComponent为具体实现类,也是被装饰类,他本身是个具有一些功能的完整的类。
- Decorator是装饰类,实现了Component接口的同时还在内部维护了一个ConcreteComponent的实例,并可以通过构造函数初始化。
- ConcreteDecorator是具体的装饰产品类,每一种装饰产品都具有特定的装饰效果。
2. Decorator模式示例
我们以毛胚房的装修为例,我们可以在毛坯房的基础上刷墙漆、铺地板、安装电器等等。
Component接口:
public interface Room {
public String showRoom();
}
毛坯房 ConcreteComponent:
public class BlankRoom implements Room {
@Override
public String showRoom() {
return "毛坯房";
}
}
虚拟装修器RoomDecorator:
// 装修工程的模板
abstract public class RoomDecorator implements Room {
// wrapper 的具体体现,每个独立的装修工序都是在上一个装修工序
// 的基础上进行的,装修就是这样层层包装完成的
protected Room roomToBeDecorated;
public RoomDecorator(Room roomToBeDecorated) {
this.roomToBeDecorated = roomToBeDecorated;
}
@Override
public String showRoom() {
// 委托(delegate)
return roomToBeDecorated.showRoom();
}
}
PaintedDecorator:
public PaintedDecorator(Room roomToBeDecorated) {
super(roomToBeDecorated);
}
public String showRoom(){
doPainting();
return super.showRoom() + "刷墙漆";
}
// 刷墙漆
private void doPainting(){}
}
FlooredDecorator:
public class FlooredDecorator extends RoomDecorator {
public FlooredDecorator(Room roomToBeDecorated) {
super(roomToBeDecorated);
}
public String showRoom(){
doFlooring();
return super.showRoom() + "铺地板";
}
// 铺地板
private void doFlooring(){}
}
调用示例:
public static void main(String[] args) {
// 毛胚房
Room blankRoom = new BlankRoom();
// 刷了墙的毛胚房
Room paintedRoom = new PaintedDecorator(new BlankRoom());
// 先刷墙再铺地板的毛胚房
// 注意到连续的 new 操作,这就是 wrapper,最内层的一般是毛胚房
Room paintedAndFlooredRoom = new FlooredDecorator(new
PaintedDecorator(new BlankRoom()));
// 先铺地板再刷墙的毛胚房
Room flooredAndPaintedRoom = new PaintedDecorator(new
FlooredDecorator(new BlankRoom()));
System.out.println(blankRoom.showRoom());
System.out.println(paintedRoom.showRoom());
System.out.println(paintedAndFlooredRoom.showRoom());
System.out.println(flooredAndPaintedRoom.showRoom());
}
Decorator模式优点:
- 装饰器模式和继承的共同特点就是扩展对象的功能,而装饰器模式比静态继承更加灵活.
- 通过使用不同的具体装饰器类,及其不同的排列组合,可以产生出大量不同的组合.
- 避免在层次结构高层的类有太多的特征
- Decorator与其他的Component不一样,Decorator说一个透明的包装。
Decorator模式缺点:
- 会出现一些小类(小程序),过度使用会使程序变得复杂.
3. CDI 对 Decorator模式的支持
Decorator 设计模式虽然降低了需求变更对软件开发的影响,但是通过层层包装,即层层 new 操作创建对象的方式不够优雅。CDI 容器可以管理组件的生命周期,在大部分情况下我们无须通过 new 操作创建所需要的对象。CDI 中的 Decorator/Delegate 注解很大程度上简化了 Decorator 设计模式的代码编写量,比如实现上面相同的功能,借助于 CDI,就无须 RoomDecorator 这个抽象类了,所有的 Decorator 类直接实现 Room 接口并使用注解声明为 Decorator 即可,比如 PaintedDecorator 类:
** PaintedDecorator:**
import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;
@Decorator
public class PaintedDecorator implements Room {
@Inject
@Delegate
Room roomToBeDecorated;
public String showRoom(){
doPainting();
return roomToBeDecorated.showRoom() + "刷墙漆";
}
// 刷墙漆
private void doPainting(){}
}
** RoomController 中是这样使用 Decorator 类的:**
import javax.inject.Inject;
import javax.inject.Named;
@Named("room")
public class RoomController {
@Inject
Room room;
public String showRoom(){
return room.showRoom();
}
}
一切看起来简单多了,秘密就在于 CDI 的 beans.xml 文件,CDI 会根据 beans.xml 文件中对 Decorator 的声明顺序加载并构造相应的 Decorator 对象,完全复现了传统方式的 Decorator 模式中通过 new 操作层层包装“毛胚房”的过程。
beans.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<!-- To activate CDI decorator, it must be specified below -->
<decorators>
<class>cn.edu.sdut.r314.PaintedDecorator</class>
<class>cn.edu.sdut.r314.FlooredDecorator</class>
</decorators>
</beans>
完整的 CDI 版本的 Decorator 示例参见:https://github.com/subaochen/weld-tutorial,具体运行方式参见其中的 README.md 文件。
4. Decorator模式实际应用
在 Java IO API 中,其实大量的使用了 Decorator 模式。试想一下,不同的输入输出源,对输入输出数据的不同处理方式和不同流程,Decorator 模式正是大显身手的时候。基础的输入输出类就好比是毛胚房,比如下面的用法:
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("filename"));
在Java IO 模式图中可以更清楚的了解 Java IO 是如何使用 Decorator 模式的。
Java IO 模式图:
根据Java IO模式图中的Decorator 模式,我们可以实现一个 Java IO 的 Decorator,比如读入一个文件,将每个字母的 ASCII 码都后移一位,代码如下:
** EncodeInputStream:**
public class EncodeInputStream extends FilterInputStream {
protected EncodeInputStream(InputStream in) {
super(in);
}
public int read() throws IOException {
int c = super.read();
return c + 1;
}
}
** 调用示例:**
public class EncodeInputStream extends FilterInputStream {
public class InputTest {
public static void main(String[] args) {
int c;
try {
InputStream in = new EncodeInputStream(new
BufferedInputStream(InputTest.class.getResourceAsStream("test.txt")));
while ((c = in.read()) >= 0) {
System.out.print((char) c);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行后的输出结果是(test.txt 文件的内容是 hello,world!):ifmmp!xpsme"。Java IO 的 Decorator 在 CDI 环境下和传统环境下写法上没有多大区别,只是一般在 CDI 环境下自己写的 Decorator 可以当作组件使用,即可以通过 Decorator 的类型而不是名称查询组件而已,不再赘述。