Decorator 模式
# Decorator 模式
Decorator 模式,也叫装饰器模式,是为了解决子类数量爆炸的一种设计模式,也叫 Fileter 模式。
# 使用装饰器模式之前
Java 的 IO 标准库提供的 InputStream
根据来源可以包括:
FileInputStream
:从文件读取数据,是最终数据源;ServletInputStream
:从 HTTP 请求读取数据,是最终数据源;Socket.getInputStream()
:从 TCP 连接读取数据,是最终数据源;- ...
如果我们想给这些类添加一些功能,如果使用继承是这样做的:
- 如果我们要给
FileInputStream
添加缓冲功能,则可以从FileInputStream
派生一个类:BufferedFileInputStream extends FileInputStream
- 如果要给
FileInputStream
添加计算签名的功能,也可以从FileInputStream
派生一个类:DigestFileInputStream extendsFileInputStream
- 如果要给
FileInputStream
添加加密/解密功能,还是可以从FileInputStream
派生一个类:CipherFileInputStream extends FileInputStream
这还只是添加了一个功能的情况,如果我们要给 FileInputStream
添加两个功能,还要再派生几个类;添加 3 个功能,还要派生类。这 3 种功能的组合,又需要更多的子类:
┌─────────────────┐
│ FileInputStream │
└─────────────────┘
▲
┌───────────┬─────────┼─────────┬───────────┐
│ │ │ │ │
┌───────────────────────┐│┌─────────────────┐│┌─────────────────────┐
│BufferedFileInputStream│││DigestInputStream│││CipherFileInputStream│
└───────────────────────┘│└─────────────────┘│└─────────────────────┘
│ │
┌─────────────────────────────┐ ┌─────────────────────────────┐
│BufferedDigestFileInputStream│ │BufferedCipherFileInputStream│
└─────────────────────────────┘ └─────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
这还只是针对 FileInputStream
设计,如果针对另一种 InputStream
设计,很快会出现子类爆炸的情况。
因此,直接使用继承,为各种 InputStream
附加更多的功能,根本无法控制代码的复杂度,很快就会失控。
# 装饰器模式
为了解决子类数量失控的问题,JDK 使用了一种设计模式:装饰器模式。如果不知道这个设计模式的读者可参考下这牌博客:漫画设计模式:什么是 “装饰器模式” ? (opens new window)
关键在于,如果我们想要给一个类加一些功能,不一定要使用继承,也可以用组合的方式:例如我想给 InputStream
加个功能,我可以定义一个类 A,里面有个成员变量 InputStream
,然后我在这个类里加上功能相关的代码!这样就不用派生类,也可以实现这个效果。因为所有输入流都是继承自 InputStream
的,所以类 A 也可以继承自 InputStream
,这样它就可以被当成一个输入流。
如果想给类 A 加个功能,一样的道理,定义一个类 B,里面有个成员变量类 InputStream(因为都是继承自 InputStream
,所以可以用 InputStream
来接受变量),然后类 B 里面加上功能代码。
举个贴近生活的例子:
上班族大多都有睡懒觉的习惯,每天早上上班时间都很紧张,于是很多人为了多睡一会,就会用方便的方式解决早餐问题。有些人早餐可能会吃煎饼,煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么“加码”,都还是一个煎饼。在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等,都是装饰器模式。
在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰器模式来实现。
以 JDK 为例,JDK 是这样设计的:
首先将 InputStream
分为两大类:
一类是直接提供数据的基础 InputStream
,例如:
- FileInputStream
- ByteArrayInputStream
- ServletInputStream
- ...
一类是提供额外附加功能的 InputStream
,例如:
- BufferedInputStream
- DigestInputStream
- CipherInputStream
- ...
当我们需要给一个“基础”InputStream
附加各种功能时,我们先确定这个能提供数据源的 InputStream
,因为我们需要的数据总得来自某个地方,例如,FileInputStream
,数据来源自文件:
InputStream file = new FileInputStream("test.gz");
紧接着,我们希望 FileInputStream
能提供缓冲的功能来提高读取的效率,因此我们用 BufferedInputStream
包装这个 InputStream
,得到的包装类型是 BufferedInputStream
,但它仍然被视为一个 InputStream
:
InputStream buffered = new BufferedInputStream(file);
最后,假设该文件已经用 gzip 压缩了,我们希望直接读取解压缩的内容,就可以再包装一个 GZIPInputStream
:
InputStream gzip = new GZIPInputStream(buffered);
无论我们包装多少次,得到的对象始终是 InputStream
,我们直接用 InputStream
来引用它,就可以正常读取:
┌─────────────────────────┐
│GZIPInputStream │
│┌───────────────────────┐│
││BufferedFileInputStream││
││┌─────────────────────┐││
│││ FileInputStream │││
││└─────────────────────┘││
│└───────────────────────┘│
└─────────────────────────┘
2
3
4
5
6
7
8
9
上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为 Filter 模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合:
┌─────────────┐
│ InputStream │
└─────────────┘
▲ ▲
┌────────────────────┐ │ │ ┌─────────────────┐
│ FileInputStream │─┤ └─│FilterInputStream│
└────────────────────┘ │ └─────────────────┘
┌────────────────────┐ │ ▲ ┌───────────────────┐
│ByteArrayInputStream│─┤ ├─│BufferedInputStream│
└────────────────────┘ │ │ └───────────────────┘
┌────────────────────┐ │ │ ┌───────────────────┐
│ ServletInputStream │─┘ ├─│ DataInputStream │
└────────────────────┘ │ └───────────────────┘
│ ┌───────────────────┐
└─│CheckedInputStream │
└───────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
类似的,OutputStream
也是以这种模式来提供各种功能。
注意到在叠加多个 FilterInputStream
,我们只需要持有最外层的 InputStream
,并且,当最外层的 InputStream
关闭时(在 try(resource)
块的结束处自动关闭),内层的 InputStream
的 close()
方法也会被自动调用,并最终调用到最核心的“基础”InputStream
,因此不存在资源泄露。
# 小结
Decorator 模式可以在运行期动态增加功能,Java 的 IO 标准库使用装饰器模式为 InputStream
和 OutputStream
增加功能:
- 可以把一个
InputStream
和任意个FilterInputStream
组合; - 可以把一个
OutputStream
和任意个FilterOutputStream
组合。 - 当最外层的
InputStream
关闭时,内层的InputStream
也会被关闭,不用担心资源泄漏的问题。
推荐阅读
- 漫画设计模式:什么是 “装饰器模式” ? (opens new window):小灰的博客,对于装饰器模式讲解的非常好
- 装饰器模式 | 菜鸟教程 (opens new window)
- 01
- 中国网络防火长城简史 转载10-12
- 03
- 公告:博客近期 RSS 相关问题10-02