装饰器模式:给代码穿上“可拆卸”的外衣

说实话,我第一次接触装饰器模式,是在看 Java I/O 源码的时候。当时我盯着 new BufferedInputStream(new FileInputStream("test.txt")) 这行代码,心里想的是:这嵌套得也太深了吧?后来我才明白,这恰恰是装饰器模式的精髓——一层一层地给核心功能“加料”

今天我们就来聊聊这个模式。我会结合一个真实的业务场景:短信发送功能。你想想看,一个基础的短信发送,可能只需要把文本发出去。但实际项目中,我们往往需要给它加上签名、加密、日志记录……这些附加功能。如果用继承来做,那得有多少子类?签名短信、加密短信、签名+加密短信……光想想就头大。

装饰器模式就是来解决这个问题的。它允许你动态地给一个对象添加额外的职责。说白了,就是你可以像搭积木一样,自由组合各种功能。

模式核心:三个角色

要理解装饰器,先记住这三个角色:

  • 抽象组件(Component):定义核心接口。比如短信发送的接口。
  • 具体组件(ConcreteComponent):实现核心功能。比如最基础的短信发送。
  • 装饰器(Decorator):持有组件引用,并实现相同接口。它负责“包装”核心组件。

嗯,这里要注意:装饰器本身也是组件。这意味着你可以用装饰器去包装另一个装饰器。这就是嵌套调用的由来。

业务场景:短信发送的“豪华套餐”

假设我们有一个基础的短信发送功能:

// 抽象组件
public interface SmsSender {
    void send(String message);
}

// 具体组件:基础短信发送
public class BasicSmsSender implements SmsSender {
    @Override
    public void send(String message) {
        System.out.println("发送短信:" + message);
    }
}

现在,我们需要给短信加上签名。我个人的习惯是,先写一个抽象的装饰器基类:

// 抽象装饰器
public abstract class SmsDecorator implements SmsSender {
    protected SmsSender wrapped;

    public SmsDecorator(SmsSender wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public void send(String message) {
        wrapped.send(message);
    }
}

然后,具体的装饰器就可以继承这个基类:

// 签名装饰器
public class SignatureDecorator extends SmsDecorator {
    private String signature;

    public SignatureDecorator(SmsSender wrapped, String signature) {
        super(wrapped);
        this.signature = signature;
    }

    @Override
    public void send(String message) {
        // 在发送前追加签名
        String signedMessage = message + " 【" + signature + "】";
        super.send(signedMessage);
    }
}

// 加密装饰器
public class EncryptDecorator extends SmsDecorator {
    public EncryptDecorator(SmsSender wrapped) {
        super(wrapped);
    }

    @Override
    public void send(String message) {
        // 模拟加密
        String encrypted = "ENC(" + message + ")";
        super.send(encrypted);
    }
}

// 日志装饰器
public class LogDecorator extends SmsDecorator {
    public LogDecorator(SmsSender wrapped) {
        super(wrapped);
    }

    @Override
    public void send(String message) {
        System.out.println("[日志] 开始发送短信...");
        super.send(message);
        System.out.println("[日志] 短信发送完成。");
    }
}

你看,每个装饰器只负责一件事。它们可以自由组合:

public class Client {
    public static void main(String[] args) {
        // 基础短信
        SmsSender sender = new BasicSmsSender();

        // 加上签名
        sender = new SignatureDecorator(sender, "蓝海资料掘金营");

        // 再加上加密
        sender = new EncryptDecorator(sender);

        // 最后加上日志
        sender = new LogDecorator(sender);

        // 发送
        sender.send("今晚8点有直播课");
    }
}

运行结果:

[日志] 开始发送短信...
发送短信:ENC(今晚8点有直播课 【蓝海资料掘金营】)
[日志] 短信发送完成。

是不是很灵活?想加什么功能,就包一层。想去掉,直接拆掉那层就行。

Java I/O 中的经典应用

其实 Java 的 I/O 流就是装饰器模式最经典的例子。我记得刚学的时候,被 BufferedInputStreamDataInputStreamObjectInputStream 这些类搞得晕头转向。后来理解了装饰器模式,一下子就通了。

你看:

// 基础组件:FileInputStream 从文件读取字节
InputStream in = new FileInputStream("data.bin");

// 装饰器1:BufferedInputStream 提供缓冲功能
in = new BufferedInputStream(in);

// 装饰器2:DataInputStream 提供读取基本类型的功能
in = new DataInputStream(in);

// 现在你可以用 DataInputStream 的方法了
DataInputStream dis = (DataInputStream) in;
int value = dis.readInt();

每个装饰器都只增加一种能力。缓冲、数据类型读取、对象序列化……它们可以任意组合。这就是为什么 Java I/O 类库看起来那么庞大,但核心逻辑其实很简洁。

避坑指南:我曾经踩过的坑

我曾经在一个项目中,用装饰器模式给 HTTP 请求添加了重试、超时、日志等功能。看起来很美,但后来发现了一个问题:装饰器链的顺序很重要

比如,如果你把日志装饰器放在最外层,它记录的是整个请求的耗时。但如果放在内层,它记录的只是核心请求的耗时,不包括重试的时间。这两种结果完全不同。

所以,我的建议是:在设计装饰器时,一定要明确每个装饰器的职责边界,并且通过文档或命名来约定顺序。比如,我习惯把日志装饰器放在最外层,因为它应该记录整个调用链的完整信息。

什么时候用装饰器?

我总结了几条经验:

  • 当你需要给一个对象动态添加功能,而且这些功能可以自由组合时
  • 当使用继承会导致子类爆炸时(比如短信的签名、加密、日志组合有 2^3=8 种,用继承你得写 8 个类)
  • 当功能需要运行时动态决定时(比如根据配置决定是否加密)

但也要注意,装饰器模式不适合所有场景。如果功能组合是固定的,或者只有一两种变体,直接用继承或策略模式可能更简单。

知识体系图

下面这张图展示了装饰器模式的核心结构。我特意用短信发送的例子来画,方便你对照理解。

装饰器模式结构图(短信发送示例) «接口» SmsSender + send(message) 实现 BasicSmsSender 发送原始短信 实现 SmsDecorator 持有 wrapped 引用 wrapped 继承 继承 继承 SignatureDecorator 追加签名 EncryptDecorator 加密内容 LogDecorator 记录日志 💡 使用方式: new LogDecorator(new EncryptDecorator(new SignatureDecorator(new BasicSmsSender(), "签名"), "密钥"))

与 Java I/O 的对应关系

为了让你更清楚,我把短信示例和 Java I/O 做个对照:

角色 短信示例 Java I/O 示例
抽象组件 SmsSender InputStream
具体组件 BasicSmsSender FileInputStream
抽象装饰器 SmsDecorator FilterInputStream
具体装饰器 SignatureDecorator BufferedInputStream
具体装饰器 EncryptDecorator DataInputStream
具体装饰器 LogDecorator PushbackInputStream

核心要点:装饰器模式的核心是“组合优于继承”。它通过对象组合的方式,实现了功能的动态扩展。每个装饰器只关注自己的职责,通过链式调用来叠加效果。

个人小技巧:我在设计装饰器时,会先画一个“功能组合矩阵”。比如短信有签名、加密、日志三个维度,矩阵的每个单元格就是一种组合。然后检查这些组合是否都能通过装饰器链实现。如果发现某个组合需要特殊处理,那可能就是设计有问题。

注意:装饰器模式虽然灵活,但也会带来一些问题。比如调试时,调用栈会变得很深,难以追踪。另外,如果装饰器链过长,性能也会受影响。我建议装饰器数量控制在 3-5 层以内,超过这个数,就要考虑是否该用其他模式了。

好了,关于装饰器模式,今天就聊到这里。记住它的核心思想:动态地给对象添加职责,而且这些职责可以自由组合。下次你看到 Java I/O 的嵌套调用,或者要给某个功能加“料”时,不妨想想装饰器模式。


公众号:蓝海资料掘金营,微信deep3321