跳到主要内容

装饰模式(Decorator Pattern)

序言

在软件开发的实践中,我们常常需要为对象添加新的功能,但有时候并不希望直接修改对象的原始类。直接修改原始类可能会引入不必要的依赖和耦合,增加系统的复杂性,同时也可能影响到其他使用该类的模块。那么,有没有一种方式可以灵活地给对象添加功能,而不改变其接口和继承结构呢?

面对这一问题,装饰模式(Decorator Pattern)提供了一个优雅的解决方案。装饰模式是一种结构型设计模式,它允许用户向一个现有的对象添加新的功能,同时又不改变其结构。这种模式通过创建一个包装对象,即“装饰者”,来包裹真实对象,并在保持原有对象方法签名不变的前提下提供额外的功能。

就像咖啡店里你可以根据个人喜好添加不同的调料和配料来定制你的咖啡一样,装饰模式允许我们在运行时动态地为对象添加“调料”,让其味道更加丰富。这种模式特别适合于场景复杂、需求多变的系统开发,因为它提供了极大的灵活性和扩展性。

接下来,我们将深入探讨装饰模式的结构、实现以及在实际开发中的应用案例,从而更好地理解这一设计模式如何帮助我们实现功能的动态扩展,使得软件系统更加灵活、易于维护和扩展。

定义

Attach additional responsibilities to an object dynamically. Decorators provide a flexible
alternative to subclassing for extending functionality.

动态的为对象附加额外的职责。装饰器为子类提供了灵活的替代方案,以扩展功能。

结构

想象一下系统中有一个文件上传和文件读取的功能。系统通过第三方OSS来管理文件。而系统需要提供给不同的客户,因此需要支持多种OSS。

但是现在有一些问题需要解决,客户需要上传一些敏感文件,如身份证照片,营业执照照片,这些数据绝不允许泄露。即便泄露,别人拿到这些文件数据也不能无法直接打开。怎么解决?

没关系!我们继承对应的实现,在子类中添加新的功能即可。于是我们有了这些类

现在客户有提出了新的需求,客户需要上传音频和视频文件,还有一些大文本文件。客户希望在上传前能先对文件进行压缩。怎么办?要继续通过子类继承来处理吗?如果又有客户希望图片在上传前要进行剪切或者压缩呢?紧接着又有客户说希望在上传图片前给图片加水印。又有客户说不希望一些非法图片能够被上传,要通过第三方接口对敏感图片进行检查...。面对客户无穷的需求,如果只通过继承来实现,类文件将出现指数级增长,而且重复代码量很大,极不利于系统的维护。

那有没有什么好方法来解决这些需求呢?让我们回忆下开头装饰模式的作用:动态的为对象附加额外的职责。我们怎么来实现呢?

我们通过EncryptionOssFileServiceDecorator修改uploadread方法,在upload方法中先对文件数据进行加密,然后再调用ossFileService上传文件。然后再read方法中先读取文件,再进行解密。

CompressionOssFileServiceDecorator同理,修改uploadread方法。分别实现压缩和解压缩的功能。

现在哪怕我们需要添加更多的功能也不怕了。

借助装饰模式,我们设置可以先对图片进行剪裁,然后再加水印,然后再对图片质量进行压缩上传。使用方式如下:

public static void main(String[] args) {
OssFileService aliyunFileService = new AliyunOssFileServiceImpl();

OssFileService imageQualityCompressionOssFileService = new ImageQualityCompressionOssFileServiceDecorator(aliyunFileService);
OssFileService imageWaterMarkOssFileService = new ImageWaterMarkOssFileServiceDecorator(imageQualityCompressionOssFileService);
OssFileService imageCutOssFileService = new ImageCutOssFileServiceDecorator(imageWaterMarkOssFileService);

File file = getFile();
imageCutOssFileService.upload(file);
}
java

当我们调用imageCutOssFileService.upload(file)会依次对图片进行剪切、加水印、图片质量压缩、上传OSS。

如上图,装饰模式就像穿衣服,我们可以在秋衣外套毛衣,然后再在毛衣外套外套。在上面的例子中,我们在上传OSS外套了图片质量压缩,然后在图片质量压缩外有套了加水印。而因为装饰器也实现了基础接口,所以这就使得装饰器之间可以相互组合。这也是装饰器模式的特色之一。

代码实现

下面我们简单实现上传阿里云OSS,以及图片剪切装饰器文件加密装饰器这两个装饰器,并演示装饰器之间的相互组合。这几个类已经足以说明装饰器模式的使用,因此其他的类不再实现。大家了解其思想即可。

以下都是简单打印实现,不做对应功能的具体实现,仅仅为了展示装饰模式的运行原理。有兴趣的同学可以自己做具体实现。

public interface OssFileService {
void upload(File file);
InputStream read(String path);
}
java

阿里云上传实现

public interface OssFileService {
void upload(File file);
InputStream read(String path);
}
java

装饰器实现

public class ImageCutOssFileServiceDecorator implements OssFileService {

private final OssFileService ossFileService;

public ImageCutOssFileServiceDecorator(OssFileService ossFileService) {
this.ossFileService = ossFileService;
}

@Override
public void upload(File file) {
System.out.println("对图片进行剪裁");
this.ossFileService.upload(file);
}

@Override
public InputStream read(String path) {
return this.ossFileService.read(path);
}
}

public class EncryptionOssFileServiceDecorator implements OssFileService {

private final OssFileService ossFileService;

public EncryptionOssFileServiceDecorator(OssFileService ossFileService) {
this.ossFileService = ossFileService;
}

@Override
public void upload(File file) {
System.out.println("对文件进行加密");
}

@Override
public InputStream read(String path) {
InputStream ins = ossFileService.read(path);
System.out.println("对文件进行解密");
return ins;
}
}
java

使用


public class Main {

public static void main(String[] args) {

// 不使用装饰器
OssFileService ossFileService = new AliyunOssFileServiceImpl();
ossFileService.upload(new File("test.txt"));
ossFileService.read("test.txt");
System.out.println("-----------------------------------");

// 使用图片剪切装饰器
OssFileService imageCutOssFileService = new ImageCutOssFileServiceDecorator(ossFileService);
imageCutOssFileService.upload(new File("test.jpeg"));
imageCutOssFileService.read("test.jpeg");
System.out.println("-----------------------------------");

// 先对图片进行剪切,然后再加密上传
// 这里要注意顺序,因为文件加密之后会导致剪切操作无法执行,因此对文件的操作第一步需要剪切文件。
OssFileService encryptionOssFileService = new EncryptionOssFileServiceDecorator(ossFileService);
OssFileService imageCutOssFileService2 = new ImageCutOssFileServiceDecorator(encryptionOssFileService);
imageCutOssFileService2.upload(new File("test.txt"));
imageCutOssFileService2.read("test.txt");
}
}

java

输出

将文件上传至OSS
从OSS读取文件
-----------------------------------
对图片进行剪裁
将文件上传至OSS
从OSS读取文件
-----------------------------------
对图片进行剪裁
对文件进行加密
从OSS读取文件
对文件进行解密

这里大家可能会发现,如果要执行的顺序是

那类的创建顺序以及包装顺序就应该是

正好是相反的,这一点一定要注意。

包装类之间的兼容性也是一个需要注意的问题。如经过加密包装器后就不能再进行剪裁。

在开源框架中的应用

JDK标准IO框架中的应用

在Java的标准库java.io包中,FilterInputStream极其子类都是装饰模式应用。一个典型的应用是BufferedInputStream

BufferedInputStream是Java中的一个类,它提供了对输入流的缓冲功能,可以显著提高读取数据的性能。

具体来说,BufferedInputStream包装了另一个输入流,并为其提供了缓冲功能。BufferedInputStream 的原理是通过在内存中创建一个缓冲区(buffer),当你从BufferedInputStream中读取数据时,它会尽可能多地从底层输入流中读取数据到缓冲区,并逐步地从缓冲区中返回数据。这样可以减少对底层输入流的直接读取次数,从而提高读取性能。

public
class BufferedInputStream extends FilterInputStream {

...

private static int DEFAULT_BUFFER_SIZE = 8192;

/**
* The maximum size of array to allocate. Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
*/
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

/*
* We null this out with a CAS on close(), which is necessary since closes can be asynchronous. We use nullness of buf[] as primary
* indicator that this stream is closed. (The "in" field is also nulled out on close.)
*/
protected volatile byte[] buf;

/**
* Creates a <code>BufferedInputStream</code> and saves its argument, the input stream
* <code>in</code>, for later use. An internal buffer array is created and stored in <code>buf</code>.
*
* @param in the underlying input stream.
*/
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}

/**
* Creates a <code>BufferedInputStream</code> with the specified buffer size, and saves its argument, the input stream
* <code>in</code>, for later use. An internal buffer array of length <code>size</code> is created and stored in <code>buf</code>.
*
* @param in the underlying input stream.
* @param size the buffer size.
* @exception IllegalArgumentException if {@code size <= 0}.
*/
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}

...

}
java

当通过 BufferedInputStream 读取数据时,它会先检查缓冲区中是否有数据可用,如果有则直接从缓冲区中返回数据;如果缓冲区中没有数据可用,它会调用底层输入流的 read 方法将更多的数据读取到缓冲区中,然后再从缓冲区中返回数据给调用者。

public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}

private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0; /* no mark: throw away the buffer */
else if (pos >= buffer.length) /* no room left in buffer */
...// 省略部分代码,BufferedInputStream的原理不是本文的重点。
// 上面的代码是为了判断开始读取的位置,每次读取的量是设置的buffer大小,如果没有设置,就是默认大小。
count = pos;
// getInIfOpen()获取到的就是被包装的InputStream
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}

private byte[] getBufIfOpen() throws IOException {
byte[] buffer = buf;
if (buffer == null)
throw new IOException("Stream closed");
return buffer;
}
java
  • pos 是读指针当前位置。
  • count 是缓冲区结束位置。
  • fill() 方法的作用就是向缓冲区中填入数据。
  • getBufIfOpen() 方法的作用就是获取缓冲区对象。

当调用者请求的数据已经在缓冲区中时,BufferedInputStream 可以直接返回数据,而无需每次都调用底层输入流的 read 方法,这样可以减少对底层输入流的频繁读取,提高了读取性能。

Spring中的TransactionAwareCacheDecorator

TransactionAwareCacheDecorator:这个类是Spring中一个典型的装饰器模式的实现。为缓存提供事务性的语义,使得缓存操作能够在事务上下文中正确执行。

核心源码如下:

public class TransactionAwareCacheDecorator implements Cache {

private final Cache targetCache;

public TransactionAwareCacheDecorator(Cache targetCache) {
Assert.notNull(targetCache, "Target Cache must not be null");
this.targetCache = targetCache;
}

@Override
public void put(final Object key, @Nullable final Object value) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
}
});
}
else {
this.targetCache.put(key, value);
}
}
}
java

put方法中,先判断了是否存在事务上下文。如果存在,则将缓存的put动作延后到了事务提交之后,在事务提交之后调用被装饰Cacheput方法,将数据放入缓存中;如果不存在则直接调用被装饰Cacheput方法,将数据放入缓存中。

何时使用

  • 需要扩展一个类的功能,但不想通过继承来实现:因为有时候继承会导致类层次过于复杂,而装饰模式提供了一种避免这种情况的方法。
  • 需要为多个类似的对象重复添加功能:如果有许多相似对象需要添加相同的功能,使用装饰模式可以避免重复编写代码。
  • 需要动态地改变对象的功能:装饰模式可以在运行时根据需要添加或移除功能,这种灵活性在某些应用中是非常有用的

这3点在上面的例子中都有体现,不太理解的可以仔细琢磨下上文中的例子。

与其他设计模式的联系

  • 装饰模式:装饰模式(Decorator Pattern)主要用于动态地给对象添加新的职责或功能。它通过创建一个包装对象来包裹真实对象,并在保持原有对象方法签名不变的前提下提供额外的功能。这种模式在Java I/O库中的InputStream、OutputStream、Reader和Writer类及其子类中使用广泛。
  • 代理模式:代理模式(Proxy Pattern)侧重于控制对对象的访问。它可以在不改变原对象的情况下,提供一个代理对象来实现一些额外的操作,如访问控制、缓存数据等。在网络编程中,代理模式常用于远程代理,以减少网络通信的开销。
  • 桥接模式:桥接模式(Bridge Pattern)的目的是将抽象部分与实现部分分离,使得它们可以独立地变化。这种模式适用于类的功能有多个维度的变化时,通过桥接模式可以将不同维度的变化解耦,提高系统的灵活性和可扩展性。
  • 门面模式:门面模式(Facade Pattern)的主要目的是简化复杂的子系统。它通过提供一个统一的接口来访问子系统中的一群接口,从而使得子系统更容易使用。门面模式常用于构建多层系统结构,利用门面对象作为每层的入口,简化层间调用。

Netty中的CompositeByteBuf就是组合模式与迭代器模式一起使用的一个实例。

易混淆模式

装饰模式极易与代理模式混淆,但其二者有一些区别。

  1. 目的:
    • 装饰器模式的主要目的是动态地给一个对象添加一些额外的职责,而不改变其接口。它通过组合的方式来实现对对象的功能进行增强。
    • 代理模式的主要目的是控制对对象的访问,它允许在访问对象时进行一些附加操作,比如延迟加载、访问控制等。
  2. 关注点:
    • 装饰器模式关注于对对象行为的增强,而不改变其接口。
    • 代理模式关注于对对象的访问进行控制和管理。
  3. 实现方式:
    • 装饰器模式通过递归组合来动态地给对象添加职责。
    • 代理模式通过将访问委派给另一个对象来控制对对象的访问。

虽然装饰器模式和代理模式有一些相似之处,比如它们都涉及到对对象的包装和增强,但它们的主要目的和应用场景有所不同。

总结

装饰模式是一种极具灵活性和可扩展性的设计模式。它允许我们在不改变原始对象的基础上,通过创建一个包装类来扩展对象的行为。这种模式适用于需要动态地为对象添加功能的场景,特别是当继承可能导致类层次过于复杂时。

装饰模式之所以被广泛使用,是因为它提供了一种避免类层次爆炸(即避免通过继承来扩展功能导致的类数量急剧增加)的方法,还使得新增的功能可以灵活地应用于多个相似对象。通过运行时组合对象的方式,装饰模式允许开发者根据需要轻松地添加或删除对象的功能,这为软件开发提供了极大的灵活性。

总的来说,装饰模式是一种强大的设计工具,它通过组合而非继承来实现功能的动态扩展,这使得它在软件开发中具有很高的灵活性和可维护性。