装饰者模式 Decorator
装饰者模式(Decorator):动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class.
装饰者模式是怎么给对象添加一些额外的职责的呢?和代理模式有什么区别吗?还是先看 UML
类图吧。
装饰者模式角色如下:
⨳ Component
(抽象组件): 定义一个对象接口,可以给这些对象动态地添加新的职责。
⨳ ConcreteComponent
(具体组件): 实现Component
接口的具体对象,即被装饰的对象。
⨳ Decorator
(抽象装饰者): 维护一个指向Component
对象的引用,并实现和Component
接口一致的接口。
⨳ ConcreteDecorator
(具体装饰者): 对Decorator
进行具体的扩展和装饰,可以添加额外的行为或功能。
由UML
类图可以很清楚的看到装饰者模式角色之间的关系
⨳ 实现关系:装饰者与具体组件都实现抽象组件,也就是说具体组件和装饰者具有同一套规范;
⨳ 关联关系:装饰者内部持有抽象组件的引用,这样才可以对真实组件进行增强。
可以看到装饰者模式和代理模式一样,都和目标对象(具体主题、具体组件)实现同一个接口,而且装饰者和代理者一样都持有目标对象的引用。
装饰者角色和具体组件的关系,就像手套与手的关系,二者具有同样的形状(实现同一个接口),都可以握手、竖中指,但是真正完成这个动作的还得是具体组件,手套只是让握手更舒适(附加功能)。
装饰者模式,旨在将 Component
(被装饰者)和 Decorator
(装饰者)分离,Component
不需要知道 Decorator
的存在,而每个Decorator
也只关心自己的装饰功能即可,至于要装饰哪个 Component
,客户端决定,即使装饰其他Decorator
也无所谓(手套上再套手套),毕竟大家都是 Component
家族的。
总得来说,装饰者模式和代理模式差不多,我认为最大的不同就是语义的不同。代理是为了隐藏目标对象而存在的,加上动态代理的存在使得客户端完全感知不到自己使用的代理还是目标类,而装饰者是为了修饰目标对象而存在的,客户端可以自己选择需要的装饰行为。
基本实现
抽象组件 Component
Component
是定义一个对象接口,可以给这些对象动态地添加职责。
java
public interface Component {
void operation();
}
具体组件 ConcreteComponent
ConcreteComponent
是定义了一个具体的对象,也可以给这个对象添加一些职责。
java
public class ConcreteComponent implements Component{
@Override
public void operation() {
System.out.println("Concrete Component do something");
}
}
抽象装饰者 Decorator
Decorator
,装饰抽象类,继承了 Component
,从外类来扩展 Component
类的功能,但对于 Component
来说,是无需知道 Decorator
的存在的。
java
public abstract class Decorator implements Component{
private Component component;
public Decorator(Component component) {
this.component = component;
}
// 可以将具体装饰者公共的代码提取出来,不同的代码留给子类实现。
public abstract void before();
public abstract void after();
// 将执行动作转发给组件本身执行,可以在转发前后做装饰
@Override
public void operation() {
before();
component.operation();
after();
}
}
具体装饰者 ConcreteDecorator
具体的装饰对象,起到给 Component
添加职责的功能。每个Decorator
也只关心自己的装饰功能即可。
▪ 具体装饰者A
java
public class ConcreteDecoratorA extends Decorator{
public ConcreteDecoratorA(Component component){
super(component);
}
// 具体组件动作执行前的装饰
@Override
public void before() {
System.out.println("Concrete Decorator A do something ahead ");
}
// 具体组件动作执行后的装饰
@Override
public void after() {
System.out.println("Concrete Decorator A do something after ");
}
}
▪ 具体装饰者A
java
public class ConcreteDecoratorB extends Decorator{
public ConcreteDecoratorA(Component component){
super(component);
}
// 具体组件动作执行前的装饰
@Override
public void before() {
System.out.println("Concrete Decorator B do something ahead ");
}
// 具体组件动作执行后的装饰
@Override
public void after() {
System.out.println("Concrete Decorator B do something after ");
}
}
客户端 Client
java
//创建需要被装饰的组件
Component component = new ConcreteComponent();
//装饰者增强组件的代码
Decorator decorator = new ConcreteDecoratorA(component);
decorator.operation();
System.out.println("--------------");
//装饰者也可以装饰装饰者
decorator = new ConcreteDecoratorB(decorator);
decorator.operation();
输入结果如下:
js
Concrete Decorator A do something ahead
Concrete Component do something
Concrete Decorator A do something after
--------------
Concrete Decorator B do something ahead
Concrete Decorator A do something ahead
Concrete Component do something
Concrete Decorator A do something after
Concrete Decorator B do something after
源码赏析
JDK 之 IO 流
Java IO
类库有几十个的负责 IO 数据的读取和写入类。如果对 Java IO
类做一下分类,我们可以从字节/字符、输入/输出将它们划分为四类:
⨳ 字节输入流(InputStream
):如 ByteArrayInputStream
,FileInputStream
、ObjectInputStream
...
⨳ 字节输出流(OutputStream
):如 ByteArrayOutputStream
,FileOutputStream
、ObjectOutputStream
...
⨳ 字符输入流(Reader
):如 CharArrayReader
、FileReader
...
⨳ 字符输出流(Writer
):如 CharArrayWriter
、FileWriter
,PrintWriter
...
以字符输入流 (InputStream
) 为例,看看谁是装饰者,谁是被装饰者。
通过 UML
类图看,还是挺明显的:
⨳ Component
是 InputStream
,定义了 read
方法。
⨳ ConcreteComponent
是 FileInputStream
和 ByteArrayInputStream
是 ,其中 FileInputStream
可以读取文件,ByteArrayInputStream
可以读取字节数组。
⨳ Decorator
是 FilterInputStream
,内有具体装饰者的重复代码的抽象整合。
⨳ ConcreteDecorator
是 BufferedInputStream
和 BufferedInputStream
,其中 BufferedInputStream
支持带缓存功能的数据读取,DataInputStream
支持按照基本数据类型来读取数据。
下面通过客户端代码使用,来更好的理解装饰者对被装饰者的增强。假设是从文件中读基本数据类型:
java
FileInputStream in = new FileInputStream("/test.txt");
// 使用BufferedInputStream 包装
BufferedInputStream bin = new BufferedInputStream(in);
// 使用DataInputStream 包装 DataInputStream
din = new DataInputStream(bin);
// 取出int数据
int data = din.readInt();
BufferedInputStream
和 FileInputStream
都对 FileInputStream
完成了功能上的增强,而且这种增强可以叠加下去。
MyBatis 之 Cache
MyBatis
对缓存 Cache
的装饰者非常多,如上 UML
类图只节选了几个装饰者。
这里的具体组件就是 PerpetualCache
表示持久缓存,本质就是一个 HashMap
。其余几个 Cache
都是装饰者:
⨳ FifoCache:先进先出(FIFO)缓存装饰者,用于控制缓存的大小。当缓存满时,根据先进先出的原则淘汰最早放入的缓存项。
⨳ LruCache:最近最少使用(LRU)缓存装饰者,根据缓存项的访问顺序来进行淘汰,保留最近使用的缓存项,以提高缓存命中率。
⨳ LoggingCache:用于记录缓存操作日志的装饰者,可以在查询、插入、更新、删除等操作前后记录日志,用于调试和监控缓存的使用情况。
⨳ ...
前文建造者模式讲解了用于构建缓存 Cache
的建造者 CacheBuilder
,有一步建造过程是添加装饰者:
java
// CacheBuilder 节选
private final List<Class<? extends Cache>> decorators;
public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
if (decorator != null) {
this.decorators.add(decorator);
}
return this;
}
最后的建造,就会使用这些装饰者创建被包装的 Cache
:
java
/**
* 构建缓存实例的方法
* 使用装饰者模式动态地添加缓存的功能
* @return 构建好的缓存实例
*/
public Cache build() {
// 设置缓存的默认实现
setDefaultImplementations();
// 根据指定的实现和缓存 ID 创建基础的缓存实例
Cache cache = newBaseCacheInstance(implementation, id);
// 设置缓存实例的属性
setCacheProperties(cache);
// 如果缓存类型是 PerpetualCache,则应用装饰者模式增强缓存功能
if (PerpetualCache.class.equals(cache.getClass())) {
// 遍历预定义的装饰器列表
for (Class<? extends Cache> decorator : decorators) {
// 创建装饰器实例并将当前缓存实例作为参数传入,动态扩展缓存功能
cache = newCacheDecoratorInstance(decorator, cache);
// 为新装饰的缓存实例设置属性
setCacheProperties(cache);
}
// 执行标准的装饰器设置操作
cache = setStandardDecorators(cache);
}
// 如果缓存不是 LoggingCache 类型,则添加日志记录功能
else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 创建 LoggingCache 装饰器,包装当前缓存实例,以添加日志记录功能
cache = new LoggingCache(cache);
}
// 返回构建好的缓存实例
return cache;
}
可以看到 Mybatis
会使用遍历建造过程中添加的 Decorator
装饰 Cache
,而且 LoggingCache
是必须要加上的装饰者。
总结
装饰者模式允许你在运行时动态地将新功能添加到对象中,而无需修改其代码。
装饰者模式优点如下:
⨳ 符合单一职责原则:装饰者模式使得每个类都只负责一件事情,即原始对象的功能,而装饰器类负责添加额外的功能。
⨳ 符合开闭原则:装饰者模式遵循开放-封闭原则,即对扩展是开放的,对修改是封闭的。这意味着你可以在不修改现有代码的情况下扩展对象的行为。
⨳ 避免类爆炸:通过装饰者模式,你可以通过组合少量的装饰器类来创建大量的组合,而不是创建大量的子类。
这些优点代理模式也有。
缺点如下:
⨳ 增加复杂性:装饰者模式可能会导致许多小对象的产生,从而增加了系统的复杂性。
⨳ 可能引入性能问题:如果装饰器的嵌套层次很深,可能会导致性能问题,因为每一层装饰都需要执行额外的逻辑。
⨳ 调试困难:由于装饰者模式的灵活性,当系统中存在大量的装饰器类时,可能会增加调试的难度,特别是在追踪对象的行为时。