1. 背景
在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等。在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰模式来实现
假设我们现在要设计一批高达用于执行各项任务,例如近程格斗,远程射击和飞行任务,怎么使用程序来模拟设计?
首先是有一个原型基础机器人,只能执行最简单的任务,其他的机器人都是在他的基础上进行扩展,那么最容易想到的就是使用继承关系进行扩展
看似合理,但是存在两点问题:
① 如果这个时候又来了一个机器人,它既能飞行又能射击,那怎么办?
可以看到我们新增加的高达并没有对shoot()
,fly()
方法进行复用,代码冗余
② 如果我现在想设计一个全能机器人,那我就要在设计类的时候预先考虑好所有的功能并指定下来,如果之后又出现了新功能,就又要去修改原始代码
继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀
2. 定义与特点
定义
在不改变现有对象结构的情况下,动态地
给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式
在对象的扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则
优点
- 采用装饰模式扩展对象的功能比采用继承方式更加灵活
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合
缺点
- 由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另一方面,使用装饰模式会产生比使用继承关系更多的装饰者对象,而他们之间去区别主要在对象的装饰方式,更多的对象会使得查错变得困难,特别是这些对象看上去都很相像
应用场景
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标
-
当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
-
当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
-
当对象的功能要求可以动态地添加,也可以再动态地撤销时
抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象
具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任
3. 装饰者模式高达问题
先想一下什么叫做装饰?在一个毛坯上面覆盖一层一层的装饰物就是装饰,在开头的例子中这点很好体现
武器系统把机器人包装起来了,我们最后创建出来的是组合了原始机器人的武器系统
- 武器系统是完整的机器人作战系统,需要继承机器人的基础功能
- 机器人是武器系统的核心组成部分
扩展功能
AbstractGundam
:抽象构件(Component)角色,我这里没有按照定义使用接口而是使用的抽象类
Gundam
:具体构件(Concrete Component)角色,也就是被装饰的基础高达
EquipmentSystem
:抽象装饰(Decorator)角色,包裹了基础机器人,可以通过其子类扩展具体构件的功能
ShootSystem...
:具体装饰(ConcreteDecorator)角色
public class DecoratorPattern{
public static void main(String[] args){
AbstractGundam p=new Gundam ();//原型机器热
p.operation();
AbstractGundam d=new ShootSystem(p);//装饰
d.operation();
}
}
//抽象构件角色 也可以是接口
public abstract class AbstractGundam{
public void operation();
}
//具体构件角色
class Gundam extends AbstractGundam{
public Gundam (){
System.out.println("原型高达");
}
public void operation(){
System.out.println("调用原型高达的方法operation()");
}
}
//抽象装饰角色
abstract class EquipmentSystem extends AbstractGundam{
protected AbstractGundam abstractGundam;
public AbstractGundam(AbstractGundam abstractGundam){
this.abstractGundam=abstractGundam;
}
public void operation(){
abstractGundam.operation();
}
}
//具体装饰角色
class ShootSystem extends EquipmentSystem{
public ShootSystem (AbstractGundam abstractGundam){
super(abstractGundam);
}
public void operation(){
super.operation();
}
public void shoot(){
}
public void addedFunction(){
System.out.println("为具体构件角色增加额外的功能addedFunction()");
}
}
如果我们要进行多次装饰,实际上就是一个递归装饰的过程,为原型机器人动态的附加功能
4. 装饰模式的简化
装饰模式所包含的 4 个角色不是任何时候都要存在的,在有些应用环境下模式是可以简化的
① 如果只有一个具体构件而没有抽象构件时,可以让抽象装饰继承具体构件
② 如果只有一个具体装饰时,可以将抽象装饰和具体装饰合并
5. 装饰模式在JAVA I/O库中的应用
由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式
抽象构件(Component)角色: 由InputStream
扮演。这是一个抽象类,为各种子类型提供统一的接口。
具体构件(ConcreteComponent)角色: 由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream
等类扮演。它们实现了抽象构件角色所规定的接口。
抽象装饰(Decorator)角色: 由FilterInputStream
扮演。它实现了InputStream
所规定的接口
具体装饰(ConcreteDecorator)角色: 由几个类扮演,分别是BufferedInputStream、DataInputStream
以及两个不常用到的类LineNumberInputStream、PushbackInputStream
public class IOTest {
public static void main(String[] args) throws IOException {
// 流式读取文件
DataInputStream dis = null;
try{
dis = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
//读取文件内容
byte[] bs = new byte[dis.available()];
dis.read(bs);
String content = new String(bs);
System.out.println(content);
}finally{
dis.close();
}
}
}
观察上面的代码,会发现最里层是一个FileInputStream
对象,然后把它传递给一个BufferedInputStream
对象,经过BufferedInputStream
处理,再把处理后的对象传递给了DataInputStream
对象进行处理
这个过程其实就是装饰器的组装过程,FileInputStream
对象相当于原始的被装饰的对象,而BufferedInputStream
对象和DataInputStream
对象则相当于装饰器
6. 装饰模式和适配器模式的区别
装饰器与适配器都有一个别名叫做 包装模式(Wrapper),它们看似都是起到包装一个类或对象的作用,但是使用它们的目的很不一一样。适配器模式的意义是要将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的
而装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方式而提升性能。所以这两个模式设计的目的是不同的