装饰器设计模式介绍
装饰器设计模式是一种结构型设计模式,允许在不改变原有对象的基础上动态地为其增加新的职责和功能。这种模式的主要目的是在不修改原始类的情况下扩展其功能或行为。通过将新功能封装成装饰者组件,你可以根据需要为任意数量的实例应用该功能。
装饰器模式有以下几个主要组成部分:
- 抽象构件(Component):这是一个接口或者抽象类,定义了被装饰的对象所应该具备的基本行为和特性。
- 具体构件(Concrete Component):这是实现了抽象构件接口的具体类,它们可以被装饰。
- 装饰者(Decorator):这也是一个接口或者抽象类,它继承了抽象构件的接口。装饰者的作用是为具体构件添加额外的行为和功能。
- 具体装饰者(Concrete Decorator):这个类实现了装饰者接口,并为具体的构件添加了特定的功能和行为。每个具体装饰者都持有一个指向相同类型的具体构件对象的引用,并通过调用该对象的方法来完成其自身的功能。
下面是一个简单的 Java 示例,演示了如何使用装饰器模式为一个房间添加不同的装饰品:
// 抽象构件 - 房间
interface Room {
void addDecoration(Decoration decoration);
void display();
}
// 具体构件 - 房间A
class RoomA implements Room {
private List<Decoration> decorations = new ArrayList<>();
@Override
public void addDecoration(Decoration decoration) {
decorations.add(decoration);
}
@Override
public void display() {
System.out.println("Room A");
for (Decoration decoration : decorations) {
decoration.decorate(this);
}
}
}
// 抽象装饰者 - 装饰品
interface Decoration {
void decorate(Room room);
}
// 具体装饰者 - 地毯
class Carpet implements Decoration {
@Override
public void decorate(Room room) {
System.out.println("Adding carpet to the room.");
}
}
// 具体装饰者 - 画作
class Painting implements Decoration {
@Override
public void decorate(Room room) {
System.out.println("Adding painting to the room.");
}
}
public class Main {
public static void main(String[] args) {
Room roomA = new RoomA();
roomA.addDecoration(new Carpet());
roomA.addDecoration(new Painting());
roomA.display();
}
}
在这个例子中,Room
是一个抽象构件,表示具有可装饰性的房间;RoomA
是一个具体构件,表示一个具体的房间;Decoration
是一个抽象装饰者,表示可以为房间添加装饰品的通用接口;Carpet
和 Painting
是两个具体装饰者,分别表示地毯和画作这两种装饰品。
通过使用装饰器模式,我们可以轻松地地为房间添加各种装饰品,而不必直接修改房间类的代码。这使得系统的扩展性和灵活性得到了很大的提高。
装饰器模式的具体实现
- 装饰器模式就是对具体的实现进行代理和对代理方法的增强,我们需要先定义一个共同的需要实现的接口
package xyz.xiaolinz.demo.decorator.demo1;
/**
* 成分
*
* @author huangmuhong
* @date 2023/08/09
*/
public interface Component {
/**
* 操作
*
* @author huangmuhong
* @date 2023/08/09
*/
void operation();
}
- 定义实例具体实现
package xyz.xiaolinz.demo.decorator.demo1;
/**
* @author huangmuhong
* @date 2023/8/9
*/
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("未被装饰的操作");
}
}
- 定义一个抽象的装饰类,同样实现被装饰接口,并代理目标装饰类
package xyz.xiaolinz.demo.decorator.demo1;
/**
* 装饰器设计模式
* 1. 使用组合的方式,实现对被装饰对象的功能增强
* 2. 装饰器和被装饰对象实现同一个接口,方便扩展
* 3. 定义通用的抽象装饰器,方便扩展
*
* @author huangmuhong
* @date 2023/8/9
*/
public abstract class Decorator implements Component {
protected final Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
- 定义具体的装饰类实现
package xyz.xiaolinz.demo.decorator.demo1;
/**
*
* 具体的装饰器
* 通过重写父类的方法,实现对父类的功能增强
*
* @author huangmuhong
* @date 2023/8/9
*/
public class DecoratorOne extends Decorator {
public DecoratorOne(Component component) {
super(component);
}
@Override
public void operation() {
System.out.println("装饰器1增强的行为");
super.operation();
System.out.println("装饰器1增强的行为");
}
}
使用场景
在 Java 中,装饰器模式的应用非常广泛,特别是在 I/O 操作中。Java 中的 I/O 类库就是使用装饰器模式来实现不同的数据流之间的转换和增强的。
从 IO 库的设计理解装饰器
在初学 Java 的时候,曾经对 Java IO 的一些用法产生过很大疑惑,比如下面这样一段代码。我们打开文件 test. txt,从中读取数据。其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率,具体的代码如下:
InputStream in = new FileInputStream("D:/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
初看上面的代码,我们会觉得 Java IO 的用法比较麻烦,需要先创建一个 FileInputStream 对象,然后再传递给 BufferedInputStream 对象来使用。我在想,Java IO 为什么不设计一个继承 FileInputStream 并且支持缓存的 BufferedFileInputStream 类呢?这样我们就可以像下面的代码中这样,直接创建一个 BufferedFileInputStream 类对象,打开文件读取数据,用起来岂不是更加简单?
InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
InputStream 基于装饰器模式的设计方案
“组合优于继承”,可以“使用组合来替代继承”。针对刚刚的继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。下面的代码展示了 Java IO 的这种设计思路。不过,我对代码做了简化,只抽象出了必要的代码结构,如果你感兴趣的话,可以直接去查看 JDK 源码。
public abstract class InputStream {
//...
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
//...
}
public long skip(long n) throws IOException {
//...
}
public int available() throws IOException {
return 0;
}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
}
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) {
this.in = in;
}
//...实现基于缓存的读数据接口...
}
public class DataInputStream extends InputStream {
protected volatile InputStream in;
protected DataInputStream(InputStream in) {
this.in = in;
}
//...实现读取基本类型数据的接口
}
看了上面的代码,你可能会问,那装饰器模式就是简单的“用组合替代继承”吗?当然不是。从 Java IO 的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方。
第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
比如,下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
第二个比较特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。实际上,符合“组合关系”这种代码结构的设计模式有很多,比如之前讲过的代理模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。
就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A impelements IA {
public void f() { //... }
}
public class AProxy impements IA {
private IA a;
public AProxy(IA a) {
this.a = a;
}
public void f() {
// 新添加的代理逻辑
a.f();
// 新添加的代理逻辑
}
}
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A impelements IA {
public void f() { //... }
}
public class ADecorator impements IA {
private IA a;
public ADecorator(IA a) {
this.a = a;
}
public void f() {
// 功能增强代码
a.f();
// 功能增强代码
}
}
评论区