设计模式实战解读(六):装饰器模式——功能增强,不动原代码

🔔 本文 5000+ 字深度原创,含完整代码示例和生产级落地方案。创作不易,如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔥 三连支持,你的认可是我持续输出的最大动力!
本文是「设计模式实战解读」系列第六篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。


一句话定义

装饰器模式(Decorator):在不修改原始对象、不使用子类继承的前提下,通过"包装"的方式动态地给对象添加新功能。

归属:结构型模式。


一、没有装饰器时的痛点

假设你有一个 MessageSender 接口,基础实现是 HttpMessageSender。现在要给它加三个功能:日志记录、失败重试、执行耗时统计。

方案一:直接修改原类

java 复制代码
public class HttpMessageSender implements MessageSender {
    public void send(Message msg) {
        long start = System.currentTimeMillis(); // 加了耗时统计
        int retryCount = 0;
        while (retryCount < 3) { // 加了重试
            try {
                log.info("发送消息: {}", msg.getId()); // 加了日志
                // 原来的逻辑...
                httpClient.post(url, msg);
                log.info("发送成功: {}", msg.getId());
                return;
            } catch (Exception e) {
                retryCount++;
                log.warn("第{}次重试", retryCount);
            }
        }
        long cost = System.currentTimeMillis() - start; // 耗时
        metricsService.record("message.send.cost", cost);
    }
}

问题:原来只有 5 行的核心逻辑,被日志/重试/监控的代码"淹没"了。而且这三个能力是耦合在一起的 ------如果另一个 MqMessageSender 也需要重试,要重复一遍。

方案二:继承扩展

java 复制代码
class LoggableMessageSender extends HttpMessageSender { ... }
class RetryableMessageSender extends HttpMessageSender { ... }
class MonitoredMessageSender extends HttpMessageSender { ... }
// 需要同时具备三个能力?
class LoggableRetryableMonitoredMessageSender extends ??? // 爆炸...

4 种增强组合,理论上需要 2³ = 8 个子类。这就是继承爆炸------用继承做功能叠加,类的数量以指数级增长。

核心诉求:把增强能力(日志/重试/监控/缓存/鉴权)和核心能力分离,按需组合,不修改原始类。


二、模式结构

复制代码
┌─────────────────────────────┐
│     Component(组件接口)     │
│  + send(Message): void      │ ← 统一接口
└─────────────┬───────────────┘
              │
   ┌──────────┴──────────┐
   │                     │
   ↓                     ↓
┌──────────────┐  ┌──────────────────────────┐
│ Concrete     │  │ Decorator(抽象装饰器)    │
│ Component    │  ├──────────────────────────┤
│ (真实实现)   │  │ - wrapped: Component     │ ← 持有被装饰对象
│              │  ├──────────────────────────┤
│HttpSender    │  │ + send(Message)          │ ← 调用 wrapped.send()
└──────────────┘  └────────────┬─────────────┘
                               │ 继承
                  ┌────────────┼──────────────┐
                  ↓            ↓              ↓
           LogDecorator  RetryDecorator  MonitorDecorator

装饰器和被装饰对象实现同一个接口------这是关键,使得装饰器可以嵌套:

java 复制代码
// 装饰器可以像俄罗斯套娃一样叠加
MessageSender sender = new HttpMessageSender();                    // 原始对象
sender = new LogDecorator(sender);                                 // 加日志
sender = new RetryDecorator(sender, 3);                           // 再加重试
sender = new MonitorDecorator(sender, metricsService);            // 再加监控
sender.send(msg); // 触发时:监控 → 重试 → 日志 → 实际发送

三、核心实现

3.1 接口 + 抽象装饰器

java 复制代码
// 组件接口
public interface MessageSender {
    void send(Message message);
}

// 具体组件(核心逻辑,保持干净)
public class HttpMessageSender implements MessageSender {
    private final String url;

    public HttpMessageSender(String url) {
        this.url = url;
    }

    @Override
    public void send(Message message) {
        // 只做一件事:发送 HTTP 请求
        httpClient.post(url, message.toJson());
    }
}

// 抽象装饰器(持有被装饰对象,转发调用)
public abstract class MessageSenderDecorator implements MessageSender {
    protected final MessageSender wrapped; // 被装饰的对象

    public MessageSenderDecorator(MessageSender wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public void send(Message message) {
        wrapped.send(message); // 默认直接转发
    }
}

3.2 具体装饰器

java 复制代码
// 装饰器1:日志装饰
public class LogDecorator extends MessageSenderDecorator {

    public LogDecorator(MessageSender wrapped) {
        super(wrapped);
    }

    @Override
    public void send(Message message) {
        log.info("[MessageSender] 开始发送, messageId={}", message.getId());
        try {
            wrapped.send(message);  // 调用被装饰对象
            log.info("[MessageSender] 发送成功, messageId={}", message.getId());
        } catch (Exception e) {
            log.error("[MessageSender] 发送失败, messageId={}", message.getId(), e);
            throw e;
        }
    }
}

// 装饰器2:重试装饰
public class RetryDecorator extends MessageSenderDecorator {
    private final int maxRetry;

    public RetryDecorator(MessageSender wrapped, int maxRetry) {
        super(wrapped);
        this.maxRetry = maxRetry;
    }

    @Override
    public void send(Message message) {
        int attempt = 0;
        Exception lastException = null;
        while (attempt <= maxRetry) {
            try {
                wrapped.send(message);
                return; // 成功即返回
            } catch (Exception e) {
                lastException = e;
                attempt++;
                if (attempt <= maxRetry) {
                    log.warn("第 {} 次重试, messageId={}", attempt, message.getId());
                    sleepQuietly(attempt * 500L); // 退避等待
                }
            }
        }
        throw new RuntimeException("重试 " + maxRetry + " 次后仍失败", lastException);
    }
}

// 装饰器3:监控装饰
public class MonitorDecorator extends MessageSenderDecorator {
    private final MetricsService metricsService;

    public MonitorDecorator(MessageSender wrapped, MetricsService metricsService) {
        super(wrapped);
        this.metricsService = metricsService;
    }

    @Override
    public void send(Message message) {
        long start = System.currentTimeMillis();
        try {
            wrapped.send(message);
            metricsService.increment("message.send.success");
        } catch (Exception e) {
            metricsService.increment("message.send.failure");
            throw e;
        } finally {
            long cost = System.currentTimeMillis() - start;
            metricsService.recordTime("message.send.cost", cost);
        }
    }
}

3.3 装饰器组合使用

java 复制代码
// 按需自由组合,顺序可以调整
MessageSender sender = new HttpMessageSender("https://api.example.com/send");
sender = new RetryDecorator(sender, 3);    // 先重试(内层)
sender = new LogDecorator(sender);         // 再记日志(中层)
sender = new MonitorDecorator(sender, metricsService); // 最外层监控

sender.send(message);
// 执行顺序:监控.before → 日志.before → 重试.before → HttpSender → 重试.after → 日志.after → 监控.after

3.4 Spring 中的 Builder 风格

java 复制代码
// 提供流式 Builder,让组合更直观
public class MessageSenderBuilder {
    private MessageSender sender;

    public static MessageSenderBuilder wrap(MessageSender sender) {
        MessageSenderBuilder builder = new MessageSenderBuilder();
        builder.sender = sender;
        return builder;
    }

    public MessageSenderBuilder withLog() {
        sender = new LogDecorator(sender);
        return this;
    }

    public MessageSenderBuilder withRetry(int maxRetry) {
        sender = new RetryDecorator(sender, maxRetry);
        return this;
    }

    public MessageSenderBuilder withMonitor(MetricsService metrics) {
        sender = new MonitorDecorator(sender, metrics);
        return this;
    }

    public MessageSender build() {
        return sender;
    }
}

// 使用
MessageSender sender = MessageSenderBuilder
    .wrap(new HttpMessageSender(url))
    .withRetry(3)
    .withLog()
    .withMonitor(metricsService)
    .build();

四、真实应用场景

4.1 框架级应用------Java I/O 流(最经典的装饰器)

java 复制代码
// Java IO 就是装饰器模式的教科书示例
InputStream raw = new FileInputStream("data.csv");         // 基础文件流
InputStream buffered = new BufferedInputStream(raw);       // 装饰:加缓冲
InputStream unzipped = new GZIPInputStream(buffered);      // 装饰:加解压
Reader reader = new InputStreamReader(unzipped, "UTF-8"); // 装饰:字节转字符

// 每层只做自己的增强,不关心其他层
组件角色 对应类
组件接口 InputStream
具体组件 FileInputStream / ByteArrayInputStream
抽象装饰器 FilterInputStream
具体装饰器 BufferedInputStream / GZIPInputStream / CipherInputStream

4.2 框架级应用------Spring 的装饰器

场景 被装饰对象 装饰器 增强内容
Spring Security HttpServletRequest SecurityContextHolderAwareRequestWrapper 注入安全上下文
Spring Cache Repository 方法 CacheInterceptor(AOP 实现装饰器思想) 加缓存
Spring Transaction Service 方法 TransactionInterceptor 加事务
HttpClient RestTemplate 各种 Interceptor 加日志/加鉴权/加重试
MyBatis Executor CachingExecutor 加二级缓存

4.3 业务场景

场景 核心能力 常见装饰
接口调用 HTTP Client 日志 + 重试 + 超时 + 熔断
数据访问 Repository 缓存 + 读写分离 + 数据脱敏
文件处理 FileProcessor 加密 + 压缩 + 格式校验
消息发送 MessageSender 日志 + 重试 + 频控
权限校验 ResourceHandler 鉴权 + 数据权限过滤

4.4 iPaaS 连接器的装饰器链

在 iPaaS 平台中,连接器调用需要多个横切关注点------这是装饰器的理想场景:

复制代码
ConnectorHandler(核心:构建请求 + 发送 + 解析响应)
    ↑ 被装饰
    
AuthDecorator       → 在请求头注入鉴权信息(OAuth / APIKey / Basic)
    ↑ 被装饰
    
RetryDecorator      → 失败自动重试(最多3次,指数退避)
    ↑ 被装饰
    
TimeoutDecorator    → 超时控制(默认30秒,可按连接器配置)
    ↑ 被装饰
    
LogDecorator        → 记录请求/响应完整日志到执行流水
    ↑ 被装饰
    
MetricsDecorator    → 统计调用次数/耗时/成功率

每个连接器共用同一套装饰链。新增一个全局能力(如请求签名验证),只需加一层装饰器,所有连接器自动获得,不需要改任何连接器代码。

java 复制代码
// 装饰链构建(按连接器配置按需启用)
ConnectorHandler handler = new DingTalkConnectorHandler(config);

if (config.isAuthEnabled()) {
    handler = new AuthDecorator(handler, authStrategyFactory.create(config));
}
handler = new TimeoutDecorator(handler, config.getTimeoutMs());
handler = new RetryDecorator(handler, config.getMaxRetry());
handler = new LogDecorator(handler, executionLogger);
handler = new MetricsDecorator(handler, metricsService);

handler.execute(request); // 完整的装饰链执行

五、常见变种

5.1 函数式装饰(Java 8+)

Function.andThen() / Function.compose() 实现函数级装饰:

java 复制代码
// 原始数据处理函数
Function<String, String> process = text -> text.trim().toLowerCase();

// 装饰:加日志
Function<String, String> withLog = process
    .andThen(result -> {
        log.info("处理结果: {}", result);
        return result;
    });

// 装饰:加脱敏
Function<String, String> withDesensitize = withLog
    .andThen(result -> result.replaceAll("\\d{4}(?=\\d{4})", "****"));

// 使用
String output = withDesensitize.apply("  138 1234 5678  ");
// 输出:1381****5678

5.2 AOP 与装饰器的关系

Spring AOP 和装饰器都能实现"不修改原代码添加横切功能",区别在于:

维度 装饰器模式 Spring AOP
时机 运行时手动组合 编译/运行时自动代理
控制粒度 对象级别(哪个实例) 切点(哪些方法)
透明度 调用方感知(知道包了几层) 调用方无感(透明代理)
适用场景 需要灵活组合不同装饰 全局横切、声明式(@Cacheable/@Transactional)
性能 无代理开销 有动态代理开销

使用建议

  • 少量固定增强(全局日志、全局事务)→ 用 AOP
  • 需要动态组合、对象级控制 → 用装饰器

5.3 可配置装饰器链

通过配置决定加哪些装饰器:

java 复制代码
// 读取连接器配置,动态构建装饰链
ConnectorHandler buildChain(ConnectorConfig config, ConnectorHandler base) {
    ConnectorHandler handler = base;
    // 按配置顺序从内到外加装饰
    for (String feature : config.getEnabledFeatures()) {
        handler = switch (feature) {
            case "auth"    -> new AuthDecorator(handler, ...);
            case "retry"   -> new RetryDecorator(handler, config.getMaxRetry());
            case "timeout" -> new TimeoutDecorator(handler, config.getTimeoutMs());
            case "log"     -> new LogDecorator(handler, logger);
            default -> handler; // 未知特性忽略
        };
    }
    return handler;
}

六、优缺点

优点 缺点
不修改原类,符合开闭原则 多层装饰后调试困难(堆栈深)
装饰可以自由组合 装饰顺序有影响,需要明确文档化
单个装饰器职责单一,易测试 类数量增加(每个能力一个装饰类)
运行时动态添加/去除装饰 使用者需要理解装饰链的构建方式
比继承更灵活(避免子类爆炸) 装饰器之间共享状态难以处理

七、避坑指南

坑 1:装饰器持有的 wrapped 被修改

装饰器一旦构建,不要在运行时替换 wrapped------这会导致并发问题。如果需要动态切换,应该重新构建整个装饰链。

坑 2:装饰顺序搞错

装饰顺序不同,行为不同。以 RetryDecorator + LogDecorator 为例:

复制代码
情况A:Log(Retry(HttpSender))
→ 记录 "开始发送" → [重试机制] → HTTP发送 → 记录 "成功/失败"
→ 日志只记录最终结果(重试细节不记)

情况B:Retry(Log(HttpSender))
→ [重试机制] → 记录 "开始发送" → HTTP发送 → 记录 "成功/失败" → [重试]
→ 每次重试都会记录日志(重试几次记几条)

原则:越靠近内层的装饰器越先执行业务逻辑,越靠近外层的装饰器越先拦截。建议用注释或文档明确装饰顺序和原因。

坑 3:装饰器不应该改变接口行为

装饰器只能增强 ,不能改变原来的语义。比如:

java 复制代码
// ❌ 错误装饰器:改变了返回值语义
public class UpperCaseDecorator extends MessageSenderDecorator {
    @Override
    public void send(Message message) {
        // 把消息内容改成大写再发送------这不是"增强",是"篡改"
        message.setContent(message.getContent().toUpperCase());
        wrapped.send(message);
    }
}

数据转换这类操作不是"装饰",应该在业务层做,或者用职责链/管道模式处理。

坑 4:装饰层太深,异常堆栈看不懂

复制代码
Exception in thread "main" ...
    at MonitorDecorator.send(MonitorDecorator.java:28)
    at LogDecorator.send(LogDecorator.java:15)
    at RetryDecorator.send(RetryDecorator.java:22)
    at AuthDecorator.send(AuthDecorator.java:19)
    at HttpMessageSender.send(HttpMessageSender.java:45)

缓解方案

  • 控制装饰层数(≤5层)
  • 在最外层装饰器统一打印完整上下文信息
  • 每个装饰器的异常消息加上自身标识([RetryDecorator] 重试耗尽

坑 5:用装饰器实现业务分支

java 复制代码
// ❌ 用装饰器实现 if-else(应该用策略模式)
public class VipDiscountDecorator extends PriceCalculatorDecorator {
    @Override
    public BigDecimal calculate(Order order) {
        BigDecimal price = wrapped.calculate(order);
        if (order.isVip()) {
            return price.multiply(new BigDecimal("0.9")); // VIP 打九折
        }
        return price; // 非 VIP 不打折
    }
}

装饰器内有业务分支 → 应该用策略模式,而不是装饰器。装饰器的职责是无条件增强 ,不是有条件执行不同逻辑


八、常见问题(FAQ)

Q:装饰器模式和代理模式有什么区别?

A:两者结构几乎相同(都是包装同一个接口),区别在于意图

  • 装饰器:增强对象的功能,调用方明确知道在使用装饰后的对象,关注"功能扩展"
  • 代理:控制对象的访问,调用方以为在直接使用原对象,关注"访问控制"(懒加载、权限校验、远程代理)

实际使用中边界模糊,Spring AOP 的 Proxy 就兼具两者的特点。

Q:什么时候用装饰器,什么时候用继承?

A:继承是编译时固定 的,装饰器是运行时动态 的。如果增强能力的组合 是固定的(永远都要有日志和监控),可以用继承;如果增强能力需要按需组合(有时要重试有时不要,有时要加密有时不要),用装饰器。

Q:多个装饰器叠加,性能有影响吗?

A:每层装饰增加一次方法调用,JVM 的 JIT 编译器在大多数情况下会内联优化(尤其是调用链固定时),实际性能损耗可以忽略不计。只有在极高频率调用(每秒百万级)且装饰器本身逻辑非常简单时,才需要考虑。

Q:Java I/O 的装饰器为什么被批评?

A:Java I/O 的装饰器实现确实不够友好------需要手动层层包装(new BufferedInputStream(new GZIPInputStream(new FileInputStream(...)))),创建时语法繁琐,且装饰顺序容易出错。现代 API 设计(如 Builder 模式 + 流式 API)可以改善这个问题。

Q:Spring 的 @Cacheable 是装饰器模式吗?

A:从思想上是的------在不修改原方法的前提下给它加上缓存能力。但实现上 Spring 用的是 AOP(动态代理),不是手工编写的装饰类。可以理解为"用 AOP 实现的自动装饰器"。


九、小结

装饰器模式的核心价值:把"需要什么能力"和"核心逻辑是什么"分离,通过包装实现按需组合。

三个实践要点:

  1. 装饰器和被装饰对象必须实现同一个接口------这是装饰器可以嵌套的前提
  2. 装饰器只做增强,不改变语义------有业务分支的增强逻辑用策略模式
  3. 控制装饰层数(≤5层)并文档化顺序------装饰链过深时调试是噩梦

设计模式系列前六篇(单例/工厂/模板方法/观察者/策略/装饰器)已形成完整的"创建型 + 行为型 + 结构型"覆盖,后续可继续扩展责任链、代理、组合模式。


标签:#设计模式 #装饰器模式 #Decorator #结构型模式 #Java #JavaIO #Spring #AOP #连接器增强 #函数式编程 #代理模式 #面向对象 #软件工程

相关推荐
likerhood13 小时前
Java ArrayList 详解:从动态数组到扩容机制与常见陷阱
java·开发语言·windows
Chloeis Syntax13 小时前
JavaEE初阶学习日记(3)---网络初认识
java·网络·笔记·学习
AI人工智能+电脑小能手13 小时前
【大白话说Java面试题 第80题】【Mysql篇】第10题:MySQL 在什么条件下索引失效?
java·开发语言·mysql·adb·面试
还在忙碌的吴小二13 小时前
Spring Boot Examples 学习示例集新手入门指南
java·spring boot·后端·学习·spring
霸道流氓气质13 小时前
Spring AI 工作流引擎扩展 Human-in-the-Loop 人工审批功能完整实战
java·人工智能·spring
better_liang13 小时前
每日Java面试场景题知识点之-分布式秒杀系统的设计
java·redis·分布式·消息队列·高并发·秒杀系统·限流降级
Python+9913 小时前
C++ 注解(注释)完整讲解
java·开发语言·c++
Reisentyan13 小时前
[Review]GoLang Learn Data Day 3
java·开发语言·golang
H_老邪13 小时前
Java基础-Java 核心语法与面向对象(底层原理级)篇
java·开发语言