作者:CQ莹儿_259 | 来源:互联网 | 2023-07-05 20:09
前言
上周末在家翻看之前写的部分文章,发现在设计模式方面甚少涉猎。在阅读开源项目源码的过程中,我们经常会接触到各种设计模式,深入理解它们无疑大有裨益,能够帮助我们快速get到那些masterminds背后的思想。今天就来谈一谈应用较为广泛的装饰模式。
装饰模式与四要素
所谓装饰模式(decorator pattern),就是在不改变原有类,也不影响其他继承自该类的子类的行为的基础上,为原有类在运行期动态地添加新行为的模式。
我们知道,类继承是为类扩充功能的一般方案。而装饰模式作为类继承的替代方案存在,其意义在于:
类继承扩充的功能在编译期就被确定,而装饰模式扩充的功能可以在运行时由调用方确定。如果要为类同时扩充多个相互独立而又可以组合的功能,采用类继承方案就意味着为每种组合创建新的类,容易造成子类泛滥。装饰模式就可以灵活地按需组合(就像现实中的小装饰品可以随意摆放一样),更加简洁且易于修改。
下面的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
明天高考,各位小盆友加油加油。
民那晚安晚安。