一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情。
模式动机
最近几天气温大幅下降,那就添点衣服吧,噢天哪,开始下雨了💧,穿上我的小雨衣再出门。
Parden? 是不是跑题了?其实并没有,刚刚所说的正是使用「装饰模式」的一个例子:
- 觉得冷时,你可以添一件衣服保暖,如果还觉得冷,可以再套上一件夹克;
- 遇到雨天,你还可以再穿一件雨衣以防淋湿。
所有这些衣物都 “扩展” 了你的基本行为(如增强了 “保暖”、“防淋湿” 功能),但它们不是你自身的一部分,所以它们并不影响你的原有行为,如果你不再需要某件衣物,可以随时脱掉。
有点抽象❓不要紧,我再举一个更贴切生活的案例:房屋装修🏡
你买了一间毛坯房,你住进去之前肯定会精心装修一番吧,刷墙、铺地、通水电、安装家具、安装电器等等,这让房屋有了更多的额外功能,住起来就会更舒适。不过装修这个动作并不会影响房屋的主体结构,也不会影响房屋是用来住的这个基本功能。
🤣抱歉,也不是所有人都买得起房子的(譬如我),并不是很贴切生活 (doge)
如此一来,我们明白了「装饰模式」就是在不改变原有功能的基础上动态扩展新的功能,那在软件系统层面我们如何实现给一个类或对象增加行为功能呢?
一般有两种:
- 继承机制:使用继承机制是给现有类添加功能的一种有效途径,但这种方法是静态的,用户不能控制增加行为的方式和时机,而且根据 “合成复用原则” 的指导,更倡导少使用继承。
- 关联机制:即将一个类的对象嵌入到另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,在「装饰模式」中,称这个嵌入的对象为装饰器(Decorator)。
🚀这样一来,装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展,这就是装饰模式的强大之处。
以上两种机制其实在适配器模式一文中就提到过,也就是所谓的类结构模式 & 对象结构模式。
定义
装饰模式是一种对象结构型模式。
装饰模式可以在不更改原有功能的基础上动态地给一个对象增加一些额外的职责,就从增加功能的角度来看,使用装饰模式比用继承更加灵活,它能有效地把对象的核心功能和装饰功能区分开。
UML 类图
模式结构
装饰模式包含如下角色:
Component:抽象构件声明被装饰对象和装饰器的公用接口ConcreteComponent:具体构件类(被装饰类)是被装饰对象所属的类,它定义了基础行为,装饰类可以增强这些行为。Decorator:抽象装饰类拥有一个指向被装饰对象的引用成员变量,该类应当被声明为通用接口,这样它就可以引用具体的装饰类了。ConcreteDecorator:具体装饰类定义了可动态添加到构件的额外功能,它会重写装饰基类的方法,并在调用父类方法之前或之后进行额外的行为。Client:客户端可以使用多层装饰来封装构件。
更多实例
多重加密系统
再来点真实的代码:
/* Cipher.java */
public interface Cipher {
String encrypt(String plainText);
}
/* BaseCipher.java */
public class BaseCipher implements Cipher {
@Override
public String encrypt(String plainText) {
// 移位
String str1 = plainText.substring(1);
String str2 = plainText.substring(0, 1);
return str1 + str2;
}
}
/* CipherDecorator.java */
public class CipherDecorator implements Cipher {
private Cipher cipher;
public CipherDecorator(Cipher cipher) {
this.cipher = cipher;
}
@Override
public String encrypt(String plainText) {
return cipher.encrypt(plainText);
}
}
/* ComplexCipher.java */
public class ComplexCipher extends CipherDecorator {
public ComplexCipher(Cipher cipher) {
super(cipher);
}
@Override
public String encrypt(String plainText) {
// 调用原有功能 encrypt
String text = super.encrypt(plainText);
// 添加增强功能 reverse
return reverse(text);
}
private String reverse(String text) {
// 反转字符串
return new StringBuilder(text).reverse().toString();
}
}
/* AdvanceCipher.java */
public class AdvanceCipher extends CipherDecorator {
public AdvanceCipher(Cipher cipher) {
super(cipher);
}
@Override
public String encrypt(String plainText) {
// 原有功能 encrypt
String text = super.encrypt(plainText);
// 增强功能 mod
return mod(text);
}
private String mod(String text) {
// 字符串求模
StringBuilder builder = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char modChar = (char) (text.charAt(i) % 128);
builder.append(modChar);
}
return builder.toString();
}
}
/* Client */
public class Client {
public static void main(String[] args) {
Cipher baseCipher, complexCipher, advanceCipher;
baseCipher = new BaseCipher();
complexCipher = new ComplexCipher(baseCipher);
advanceCipher = new AdvanceCipher(complexCipher);
String plainText = advanceCipher.encrypt("PlainText");
System.out.println(plainText);
}
}
执行结果:
示例代码
Component.java
public interface Component {
void execute();
}
ConcreteComponent.java
public class ConcreteComponent implements Component {
@Override
public void execute() {
// TODO: execute some basic behavior
}
}
Decorator.java
public class Decorator implements Component {
private Component wrappee;
public Decorator(Component wrappee) {
this.wrappee = wrappee;
}
@Override
public void execute() {
wrappee.execute();
}
}
ConcreteDecoratorA.java
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component wrappee) {
super(wrappee);
}
@Override
public void execute() {
// Original feature
super.execute();
// Additional feature
extraBehaviorA();
}
public void extraBehaviorA() {
// TODO: Add extra behavior to enhance base-behavior
}
}
ConcreteDecoratorB.java
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component wrappee) {
super(wrappee);
}
@Override
public void execute() {
// Original feature
super.execute();
// Additional feature
extraBehaviorB();
}
public void extraBehaviorB() {
// TODO: Add extra behavior to enhance base-behavior
}
}
Client.java
public class Client {
public static void main(String[] args) {
Component concreteComponent, concreteDecoratorA, concreteDecoratorB;
concreteComponent = new ConcreteComponent();
concreteDecoratorA = new ConcreteDecoratorA(concreteComponent);
concreteDecoratorB = new ConcreteDecoratorB(concreteDecoratorA);
// decoratorB --> decoratorA --> concreteComponent
concreteDecoratorB.execute();
}
}
优缺点
✔装饰模式无须创造新子类即可动态扩展对象的行为/功能,相对继承方式灵活许多。
✔可以对多个具体装饰类进行排列组合,创造出很多不同功能的组合,采用继承关系难以实现,而采用装饰模式却十分简单。
✔具体装饰类和具体构件类可以独立变化,符合 “开闭原则”。
❌使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同。
❌这种比继承更加灵活机动的特性,同时也意味着比继承更加容易出错,排错也较困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
适用场景
在以下情况推荐使用装饰模式:
(1)在不影响其他对象的情况下,动态地为对象新增额外的行为/功能,这些功能也可以动态地被撤销。
(2)当不能采用继承方式对系统进行扩展或者采用继承不利于系统扩展和维护时;前者比如类被定义为 final 类从而无法被继承,后者则是系统中存在大量独立的扩展,为支持每一种组合将产生爆炸的子类组合数量。
子类组合数量爆炸的实例很好地论证了:在某些情况下,继承方式不利于系统扩展和维护,可以采用装饰模式对这些功能进行排列组合,并增强到原有对象中,提高系统的可维护性。
「装饰模式」落地
java.swing
在 java.swing 包中,可以通过装饰模式动态给一些构件增加新的行为或改善其外观显示。如:JList 构件本身并不支持直接滚动(没有滚动条),要创建可以滚动的列表,可以使用如下代码实现:
// JList 充当具体构件
JList list = new JList();
// JScrollPane 充当装饰器, 对 JList 进行功能扩展
JScrollPane sp = new JScrollPane(list);
Java I/O
装饰模式在 JDK 中最著名的应用莫过于 Java I/O 标准库的设计了,以 InputStream 为例:
其中 InputStream 充当抽象构件类,它定义了一系列 read() 用于读取数据;而 FilterInputStream则充当抽象装饰类,可对 SequenceInputStream、ByteArrayInputStream、ObjectInputStream、FileInputStream、PipedInputStream 这些类进行增强。
⭐介绍下充当具体构件类的几种流:
| 流名称 | 应用场景 |
|---|---|
SequenceInputStream | 把多个 InputStream 合并为一个 InputStream,“序列输入流” 类允许应用程序把几个输入流连续地合并起来 |
ByteArrayInputStream | 访问数组,把内存中的一个缓冲区作为 InputStream 使用,CPU 从缓存区读取数据比从存储介质的速率快 10 倍以上 |
ObjectInputStream | 对象流,具有读取对象的功能 |
FileInputStream | 访问文件,把一个文件作为 InputStream,实现对文件的读取操作 |
PipedInputStream | 访问管道,主要在线程中使用,一个线程通过管道输出流发送数据,而另一个线程通过管道输入流读取数据,这样可实现两个线程间的通讯 |
⭐介绍下充当具体装饰类的几种流:
| 流名称 | 应用场景 |
|---|---|
DataInputStream | 特殊流,读取各种基本类型数据,如 byte、int、String... |
PushBackInputStream | 推回输入流,可以把读取进来的某些数据重新回退到输入流的缓冲区之中 |
BufferedInputStream | 缓冲流,增加了缓冲功能 |
🥇如下代码演示通过 BufferedInputStream & DataInputStream 为 FileInputStream 增加缓冲功能与读取基本数据类型的功能:
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("D://test.txt")));
while (in.available() != 0) {
System.out.print((char) in.readByte());
}
in.close();
🚀看完还是不过瘾,不妨自己动手写一个具体装饰类实现「将输入流中的所有小写字母全部变成大写字母」的功能,并将其装饰到输入流中!
import java.io.*;
public class UpperCaseInputStream extends FilterInputStream {
/**
* Creates a <code>FilterInputStream</code>
* by assigning the argument <code>in</code>
* to the field <code>this.in</code> so as
* to remember it for later use.
*
* @param in the underlying input stream, or <code>null</code> if
* this instance is to be created without an underlying stream.
*/
protected UpperCaseInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int read = super.read();
return (read == -1 ? read : Character.toUpperCase(read));
}
@Override
public int read(byte[] b) throws IOException {
int read = super.read(b);
for (int i = 0; i < read; i++) {
b[i] = (byte) Character.toUpperCase((char) b[i]);
}
return read;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
for (int i = off; i < off + read; i++) {
b[i] = (byte) Character.toUpperCase((char) b[i]);
}
return read;
}
public static void main(String[] args) throws IOException {
int c;
InputStream in = new UpperCaseInputStream(new FileInputStream("D://xxx.txt"));
try {
while ((c = in.read()) >= 0) {
System.out.print((char) c);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
in.close();
}
}
}
"Hello World!" —> "HELLO WORLD!"
🙄除了 InputStream 的子类 FilterInputStream,还有 OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。
模式扩展
装饰模式的简化
装饰模式所包含的 4 个角色并非任何时候都需要存在的,在有些应用场景下是可以简化的,如以下两种情况。
(1)如果只有一个具体构件类而没有抽象构件时,可以让抽象装饰类继承具体构件
(2)如果只有一个具体装饰类时,可以将抽象装饰类和具体装饰类合并
⭐装饰模式简化需要注意的问题:
- 一个装饰类的接口必须与被装饰类的接口保持相同
- 不要将过多的逻辑和状态放在具体构件类中
透明装饰模式
在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型。
客户端代码
// 全部声明为抽象构件类型
Cipher bc, cc, ac;
bc = new BaseCipher();
cc = new ComplexCipher(bc);
ac = new AdvanceCipher(cc);
ac.encrypt("..");
半透明装饰模式
大多数装饰模式都是半透明装饰模式,而非完全透明的。允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。因为有时我们需要单独调用新增的业务方法,为了能够调用到新增方法,我们不得不明确定义具体装饰类,而具体构件类还是可以使用抽象构件类来定义。
⭐半透明装饰模式可以给系统带来更多的灵活性,设计相对简单,使用起来也非常方便,但最大的缺点在于不能实现对同一个对象的多次装饰,而且需要客户端有区别地对待装饰前后的对象。
以一个 “变形金刚” 的例子来演示说明
客户端代码
// 具体构件类还是可以使用抽象构件类来定义
Transform camaro = new Car();
camaro.move();
// 声明具体装饰类型的对象
Robot bumblebee = new Robot(camaro);
bumblebee.move();
bumblebee.say();
代理模式 VS 装饰模式
⭐千万别将「代理模式」和「装饰模式」混淆,它们的 UML 有着非常相似的结构!上一篇代理模式中的静态代理没有对 Proxy 进行更深一层的抽象,且关联的是具体的被代理类 Service,而非其接口 ServiceInterface,一旦稍微做些变化,它们的结构可以说是完全一致。
不过一般情况下,代理模式的 UML 图会比较具体,而装饰模式的 UML 图会相对抽象一点。
「代理模式 & 装饰模式」之间的演化!
🌖那么我们如何区分这两者呢?
🚀尽管装饰模式和代理模式有着极为相似的结构,且这两个模式都是将一个对象的部分工作委派给另一个对象,但是其意图/功能却非常不同。
- 代理模式主要是控制客户端的访问(不一定需要调用原有目标对象的功能)
- 装饰模式主要是在原有功能上进行增强/扩展,而且不会影响到原有功能的使用(一定会调用原有功能,并在此基础上进行扩展)
适配器模式 & 代理模式 & 装饰模式
- 「适配器模式」可以对已有对象的接口进行修改,提供不同的接口
- 「装饰模式」能在不改变已有对象接口的前提下强化对象功能,而且还支持递归组合
- 「代理模式」能为对象提供相同的接口
最后
👇下一篇:「设计模式」🏰外观模式(Facade)
❤️ 好的代码无需解释,关注「手撕设计模式」专栏,跟我一起学习设计模式,你的代码也能像诗一样优雅!
❤️ / END / 如果本文对你有帮助,点个「赞」支持下吧,你的支持就是我最大的动力!