浅谈装饰模式及其在JDK、Flink中的应用

前言

上周末在家翻看之前写的部分文章,发现在设计模式方面甚少涉猎。在阅读开源项目源码的过程中,我们经常会接触到各种设计模式,深入理解它们无疑大有裨益,能够帮助我们快速get到那些masterminds背后的思想。今天就来谈一谈应用较为广泛的装饰模式

装饰模式与四要素

装饰模式属于GoF设计模式分类中的结构型模式。

所谓装饰模式(decorator pattern),就是在不改变原有类,也不影响其他继承自该类的子类的行为的基础上,为原有类在运行期动态地添加新行为的模式。(Attach additional responsibilities to an object dynamically)

我们知道,类继承是为类扩充功能的一般方案。而装饰模式作为类继承的替代方案存在(Decorators provide a flexible alternative to subclassing for extending functionality),其意义在于:

类继承扩充的功能在编译期就被确定,而装饰模式扩充的功能可以在运行时由调用方确定。如果要为类同时扩充多个相互独立而又可以组合的功能,采用类继承方案就意味着为每种组合创建新的类,容易造成子类泛滥。装饰模式就可以灵活地按需组合(就像现实中的小装饰品可以随意摆放一样),更加简洁且易于修改。

下面的UML类图示出实现装饰模式的四要素。

  • 构件(Component):接口,用于定义整个实体空间的最基础的行为规范;
  • 构件实体(ConcreteComponent):实现Component的实体类,本身具有一些功能,同时也是被装饰(被扩充)的类;
  • 装饰器(Decorator):实现Component的类,其中维护一个ConcreteComponent的实例,具体的装饰功能由其子类实现;
  • 装饰器实体(ConcreteDecorator):继承Decorator并实现具体的装饰功能。

通过下面两句话即可使用装饰器实体ConcreteDecorator实现的扩充功能:

Component component = new ConcreteDecorator(new ConcreteComponent());
component.operation();

可见,调用方只需要额外调用装饰器实体的构造函数,而不必关心Component/ConcreteComponent在装饰之后的变化。不过由上也可以看出,装饰模式会new出更多的对象,当装饰器实体的链比较长时会有性能问题,并且出现问题时也不利于debug。

上面所有内容讲的装饰模式叫做透明装饰模式,即用户总可以只用Component来调用所有功能。相对地,还有一种半透明装饰模式,即装饰器实体中允许存在Component中不存在的新方法(如someNewBehavior()),调用方式相应就变成:

ConcreteDecorator component = new ConcreteDecorator(new ConcreteComponent());
component.someNewBehavior();

由于扩充功能可以在新方法中定义,半透明装饰模式更加灵活,但是就无法对用户屏蔽ConcreteDecorator存在的现实了。更重要的是,半透明装饰模式下对实例进行多次(链式)装饰是没有意义的,因为只能调用最后一次装饰时装饰器实体的新增方法。

干说了这么多,举两个示例来帮助理解吧。

Java I/O中的装饰模式

装饰模式在java.io包中广泛使用,包括基于字节流的InputStream/OutputStream和基于字符的Reader/Writer体系。以下以InputStream为例。

InputStream是所有字节输入流的基类,其下有众多子类,如基于文件的FileInputStream、基于对象的ObjectInputStream、基于字节数组的ByteArrayInputStream等。有些时候,我们想为这些流加一些其他的小特性,如缓冲、压缩等,用装饰模式实现就非常方便。相关的部分类图如下所示。

这个类图很标准,其中:

  • 构件是InputStream;
  • 构件实体是FileInputStream、ObjectInputStream等等;
  • 装饰器是FilterInputStream;
  • 装饰器实体是FilterInputStream的所有子类。

观察一下装饰器FilterInputStream的开头,可以发现它持有InputStream的引用,并且实现了InputStream中的所有方法(实际上就是简单地代理了一下)。具体的装饰器实体就继承FilterInputStream,并实现对应的扩充功能。如下图所示。

以下就可以用BufferedInputStream和GZIPInputStream创建一个带缓冲、压缩的文件输入流。

InputStream is = new GZIPInputStream(new BufferedInputStream(new FileInputStream("test.txt")));

当然,如果我们想要自己实现一个InputStream的装饰器实例,创建一个FilterInputStream的子类即可,就不再举例了。

Flink State TTL中的装饰模式

笔者之前写过一篇文章《简析Flink状态生存时间(State TTL)机制的底层实现》,这里就用到了装饰模式,但不像Java I/O那样标准。

为状态增加TTL的特性可以直接在原始状态之上实现,因此符合装饰模式的场景。Flink引入了一个AbstractTtlDecorator抽象类作为装饰器,负责为状态类型T装饰上与TTL相关的基本逻辑。相关的部分类图如下所示。

可见,虽然AbstractTtlDecorator并未持有State的实例(只有State的类型参数),但是在其子类AbstractTtlState中,通过持有TTL状态上下文TTLStateContext间接地得到了State实例。例如,由AbstractTtlState派生出来的TtlMapState直接在原来的MapState上进行增删改查操作,只是附带上了AbstractTtlDecorator和AbstractTtlState提供的TTL逻辑而已。其他的TtlListState等也是同理。具体的代码可参见前面给的传送门,这里不再重复贴了。

虽然这种模式的类结构并不典型,但是也完全契合装饰模式的精神,Ttl*State对用户也是透明的。有很多开源框架都采用了这种相对松散的装饰模式,有时会被称为包装(Wrapper)模式。

The End

明天高考,各位小盆友加油加油。

民那晚安晚安。

版权声明:
作者:siwei
链接:https://www.techfm.club/p/15023.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>