装饰器模式:给对象“穿衣服”的艺术

说实话,我第一次接触装饰器模式时,觉得它有点绕。但后来在项目中用顺手了,才发现它真是个好东西。你想想看,如果我想给一个对象加功能,最直接的办法是什么?继承呗。但继承有个问题——它是静态的,编译时就定死了。而且每加一个功能就得新建一个子类,类多了简直要命。

装饰器模式就不一样了。它像给对象“穿衣服”一样,一层一层往上套。每件“衣服”都增加一点新功能,而且穿脱自由。嗯,这就是它的核心思想:动态地给一个对象添加额外的职责,比生成子类更灵活

为什么需要装饰器模式?

我在项目中遇到过这样一个场景:一个消息推送系统,基础功能是发短信。后来产品经理说,要加邮件通知。再后来,又要加微信通知、加日志记录、加失败重试……如果每个组合都建一个子类,那得有多少?短信+邮件、短信+微信、短信+邮件+微信……这还不算完,还要考虑顺序。

用继承的话,类数量会爆炸。用装饰器模式呢?每个功能就是一个装饰器,想怎么组合就怎么组合。说白了,这就是“组合优于继承”的典型应用。

核心要点:装饰器模式让你在不修改原有类的情况下,动态地扩展对象的功能。它遵循开闭原则——对扩展开放,对修改关闭。

装饰器模式的结构

装饰器模式有四个角色,我习惯这样记:

  • 组件接口(Component):定义对象的公共接口,所有组件和装饰器都要实现它。
  • 具体组件(ConcreteComponent):被装饰的原始对象,实现基础功能。
  • 装饰器基类(Decorator):实现组件接口,并持有组件对象的引用。它的主要作用是“传递调用”。
  • 具体装饰器(ConcreteDecorator):在调用组件方法前后,添加自己的额外逻辑。

下面这张图能帮你理清关系:

«接口» Component + operation() ConcreteComponent + operation() Decorator - component: Component + operation() ConcreteDecoratorA + operation() ConcreteDecoratorB + operation() ConcreteDecoratorC + operation() 实现 实现 聚合 继承

代码示例:消息推送系统

咱们用代码说话。先定义组件接口:

// 组件接口
public interface Notifier {
    void send(String message);
}

然后实现基础组件——短信通知:

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

接下来是装饰器基类。它负责“传递调用”:

// 装饰器基类
public abstract class NotifierDecorator implements Notifier {
    protected Notifier wrapped;
    
    public NotifierDecorator(Notifier notifier) {
        this.wrapped = notifier;
    }
    
    @Override
    public void send(String message) {
        wrapped.send(message);  // 转发给被装饰的对象
    }
}

现在可以写具体装饰器了。比如加一个邮件通知:

// 具体装饰器A:邮件通知
public class EmailDecorator extends NotifierDecorator {
    public EmailDecorator(Notifier notifier) {
        super(notifier);
    }
    
    @Override
    public void send(String message) {
        super.send(message);  // 先执行原有逻辑
        System.out.println("发送邮件: " + message);  // 再添加新功能
    }
}

再加一个微信通知:

// 具体装饰器B:微信通知
public class WeChatDecorator extends NotifierDecorator {
    public WeChatDecorator(Notifier notifier) {
        super(notifier);
    }
    
    @Override
    public void send(String message) {
        super.send(message);
        System.out.println("发送微信: " + message);
    }
}

客户端怎么用呢?就像叠衣服一样,一层套一层:

public class Client {
    public static void main(String[] args) {
        // 基础功能:只发短信
        Notifier notifier = new SmsNotifier();
        
        // 加个邮件功能
        notifier = new EmailDecorator(notifier);
        
        // 再加个微信功能
        notifier = new WeChatDecorator(notifier);
        
        // 调用时,三个功能都会执行
        notifier.send("系统故障,请立即处理!");
        
        // 输出:
        // 发送短信: 系统故障,请立即处理!
        // 发送邮件: 系统故障,请立即处理!
        // 发送微信: 系统故障,请立即处理!
    }
}

小提示:装饰器的执行顺序取决于你包装的顺序。上面的例子是“短信→邮件→微信”,如果你想先发微信再发邮件,调换一下装饰器的包装顺序就行。

装饰器模式 vs 继承

我整理了一张对比表,方便你理解两者的区别:

对比维度 装饰器模式 继承
扩展方式 动态组合,运行时添加功能 静态绑定,编译时确定
灵活性 高,可以自由组合、任意顺序 低,每个组合都需要新类
类数量 少,n个功能只需n个装饰器 多,n个功能需要2ⁿ个组合类
对原有类的修改 不需要修改 可能需要修改父类
适用场景 功能组合多、变化频繁 功能稳定、层次清晰

实际项目中的避坑指南

我曾经犯过一个错误:在装饰器里修改了被装饰对象的状态。结果多个装饰器共享同一个对象时,状态互相干扰,查了半天才定位到问题。记住,装饰器应该只添加行为,不要修改内部状态。

还有一点要注意:装饰器模式不适合处理“装饰顺序敏感”的场景。比如加密后再压缩,和压缩后再加密,结果完全不同。这种情况下,装饰器模式虽然能用,但容易出错。我建议用策略模式或者管道模式来处理。

⚠️ 注意事项:

  • 装饰器不要修改被装饰对象的接口,否则会破坏透明性
  • 避免装饰器链过长,超过5层就会影响可读性
  • 如果装饰器需要访问被装饰对象的特定方法,说明设计可能有问题

Java I/O 中的装饰器模式

你想想看,Java 的 I/O 流是不是很眼熟?BufferedInputStream 装饰 FileInputStreamDataInputStream 再装饰 BufferedInputStream……这就是典型的装饰器模式。

// Java I/O 中的装饰器
InputStream in = new FileInputStream("data.txt");
in = new BufferedInputStream(in);   // 添加缓冲功能
in = new DataInputStream(in);       // 添加基本类型读取功能

我个人习惯用这个例子来向团队解释装饰器模式。因为大家都用过,一说就懂。

什么时候用装饰器模式?

我总结了三个适用场景:

  1. 功能组合多:比如消息通知、日志记录、权限校验,这些功能经常需要组合使用。
  2. 功能变化频繁:产品经理今天加一个功能,明天减一个功能,用装饰器模式改起来快。
  3. 不想影响其他对象:装饰器只影响被装饰的那个实例,不会污染整个类。

反过来,如果功能组合固定、变化很少,直接用继承反而更简单。别为了用模式而用模式,这是新手容易犯的毛病。

一句话总结:装饰器模式就是“组合的艺术”。它用“包装”代替“继承”,让你像搭积木一样灵活地扩展对象功能。记住四个字——动态组合


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