概述
一般情况下,当我们想给一个类或对象添加功能的时候,有两种常用的方式:
- 继承:通过使用继承,我们可以使子类既能拥有父类的功能,也能实现本身的功能。
- 组合:而组合的方式,是在某一个类中包装另一个类的对象,然后通过这个对象引用来决定是否拥有该类的某些功能,我们把这个包装的对象可以称为装饰器 (Decorator);
由于继承是一种静态的行为,而组合则可以实现动态的往一个类中添加的新的行为。并且就功能而言,组合相比继承更加灵活,这样可以给某个对象而不是整个类添加一些功能。而装饰者模式就是基于组合来实现的:
装饰者模式是一种动态地往一个类中添加新的行为的设计模式。
装饰者模式简介
也就是说,通过使用装饰者模式,可以在运行时扩充一个类的功能。大概原理是:增加一个装饰类包装原来的类,包装的方式一般是通过在将原来的对象作为装饰类的构造函数的参数。装饰类实现新的功能,但是,在不需要用到新功能的地方,它可以直接调用原来的类中的方法。修饰类必须和原来的类有相同的接口。
修饰模式是类继承的另外一种选择。类继承在编译时候增加行为,而装饰模式是在运行时增加行为。
当有几个相互独立的功能需要扩充时,这个区别就变得很重要。在有些面向对象的编程语言中,类不能在运行时被创建,通常在设计的时候也不能预测到有哪几种功能组合。这就意味着要为每一种组合创建一个新类。相反,装饰者模式是面向运行时候的对象实例的,这样就可以在运行时根据需要进行组合。
结构
我们先通过一张图来看下装饰者模式的结构:
图片来源:图说设计模式 - 装饰模式 - 结构图解
通过以上结构,我们可以大概了解到装饰者模式的几个角色:
- Component,最基础的底层接口,包含基础的功能方法;
- ConcreteComponent ,底层接口Component原始的实现,我们要装饰的对象就是这部分,这部分也就是被装饰者;
- Decorator,装饰角色,一般情况下该对象是一个抽象类,实现自Component,在该类中一般会有一个指向Component接口的对象,指向被装饰的对象,通过组合的形式对其进行包装;
- ConcreteDecorator,具体的装饰角色,继承自Decorator,对具体的被装饰者进行包装,并且可以添加新的功能;
我们先通过简单的代码来看一下:
-
Component
接口:
public interface Component {
/**
* 底层基础接口
*/
void operation();
}
-
ConcreteComponent
实现类:
public class ConcreteComponent implements Component {
/**
* 基础接口的实现
*/
@Override
public void operation() {
System.out.println("concrete component");
}
}
-
Decorator
装饰角色,抽象类,实现Component:
public abstract class Decorator implements Component {
/** 维护一个被装饰的对象的引用*/
protected Component component;
/**
* 通过构造方法传入被装饰的对象
* @param component
*/
public Decorator(Component component) {
this.component = component;
}
/**
* 调用被装饰的对象的方法
*/
@Override
public void operation() {
component.operation();
}
}
-
ConcreteDecorator
具体装饰角色,继承自Decorator抽象类:
public class ConcreteDecorator extends Decorator {
/**
* 自定义自己的属性
*/
private String addState = "test";
public ConcreteDecorator(Component component) {
super(component);
}
@Override
public void operation() {
component.operation();
System.out.println("addState:" + addState);
}
/**
* 自定义新的方法,实现新的功能
*/
public void addedBehavior() {
System.out.println("addState:" + addState);
}
// 省略掉get,set方法
}
-
Main
,用于测试:
public class Main {
public static void main(String[] args) {
Component component = new ConcreteDecorator(new ConcreteComponent());
component.operation();
}
}
优缺点
- 我们先来看下优点:
a. 装饰者模式可以用来代替继承关系,可以动态的扩展一个实现类的功能,且不影响到其他对象;
b. 装饰者模式不管最终装饰了多少层,最终返回的对象还是Component;
c. 遵循设计模式的原则:对扩展开放,对修改关闭。
- 有优点就免不了会有缺点,我们再来看下缺点,缺点的话其实就比较明显了:
装饰者模式采用组合的形式比继承灵活,但也比继承相对复杂,如果装饰的层数过多,出现问题排查的时候也会相对困难些;因此,尽量减少装饰类的数量,以便降低系统的复杂度;
适用场景
装饰者模式的适用场景可以根据它的优点来进行选择:
- 当需要在不影响其他对象的情况下,动态的为一个类扩展功能的时候;
- 当不能使用继承或者不适合采用继承的方式进行类的扩展和维护时(比如类定义为final类型),可以采用装饰者模式;
在JAVA I/O流中的应用
而装饰者模式在Java中应用最广泛最出名的恐怕就是Java中的I/O的API了。如果我们在学习I/O体系前,没有了解过装饰者模式的话,那么由于I/O体系本身复杂的结构,学习的时候或许会有一种很头疼的感觉。我们还是先来看下I/O体系大致结构:
注:没有完全展示所有I/O相关的接口。 图片来源:google 图片
我们可以大致把上图分为几层:
- Reader、Writer、InputStream、OutputStream,对应装饰者角色中最基础的接口:Component,这里面包含了基础的操作;
- FileInputStream,FileOutputStream等第二层没有其他实现类的类,对应于装饰者中被装饰的角色:ConcreteComponent;
- FilterReader,FilterOutputStream等,对应于装饰者中的抽象装饰角色:Decorator;
- BufferedOutputStream,DataOutputStream等,对应于装饰者中的具体装饰角色:ConcreteDecorator;
我们接下来可以通过一个代码简单看下实现:
public class Main {
public static void main(String[] args) throws Exception {
// 1. 定义输入流
InputStream inputStream = null;
try {
// 2. 实例 被装饰者 : 文件读取
inputStream = new FileInputStream("E://test.txt");
// 3. 具体装饰角色:缓冲流
inputStream = new BufferedInputStream(inputStream);
// 4. 进行操作:开始读文件
byte[] text = new byte[inputStream.available()];
inputStream.read(text);
System.out.println(new String(text));
} catch (IOException e) {
e.printStackTrace();
} finally {
inputStream.close();
}
}
}
当然,装饰者模式在Java中应用的地方还有许多地方,比如最近学习的Mybatis中Executor体系结构:Executor
, BaseExecutor
, CachingExecutor
等。
总结
学习了装饰者模式之后,我们来简单回顾总结下:
- 装饰者模式可以动态的扩展类的功能,就这点来说,装饰者模式要比使用继承更为灵活,按照GoF设计模式的划分,装饰者模式属于结构型模式的一种。
- 装饰者模式遵循了
对扩展开放,对修改关闭
的设计原则,在不修改原有代码的基础上,通过组合的形式扩展新的功能。- 装饰者模式虽然比继承的形式要灵活,但由于其本身的繁琐性,所以也会相对继承复杂些,如果装饰的层数过多,一旦出现问题,将会提高我们排查问题的难度,所以要尽量减少装饰的层数,以便降低系统的复杂度。
本文参考自:
装饰器模式--继承的另一个选择
维基百科-装饰模式