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

🔔 本文 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 #连接器增强 #函数式编程 #代理模式 #面向对象 #软件工程

相关推荐
aibo8603082 分钟前
基于 Spring Boot + Vue3 的菜谱分享网站设计与实现
java·spring boot·后端
凤山老林3 分钟前
Spring Boot 集成 TOTP 双因素认证与恢复码机制
java·spring boot·后端
小冯不疯6 分钟前
物料主数据同步实战:从金蝶云星空到小满OKKICRM的单一策略深度拆解
java·大数据·数据库
vipxieliang9 分钟前
Spring Boot 测试策略:单元测试、切片测试与集成测试
java·spring boot·后端
估值探索者21 分钟前
【Python量化技术指标实战 04】布林带通道突破,怎么知道是真突破还是假突破?
java·开发语言·python·接口·股票数据api接口
未秃头的程序猿22 分钟前
同一个商户号,V2 免密代扣和 V3 分账的证书怎么共存?
java·后端·微信
captain37636 分钟前
Spring Boot的配置文件
java·spring boot·后端
captain37637 分钟前
Spring Boot日志
java·数据库·spring boot
Rabitebla1 小时前
【Linux】 权限篇(三):深入理解 umask 位运算与粘滞位的设计思想
java·linux·服务器·c++·笔记·学习
Pin_Qing1 小时前
缓存能活多久,现在是个要算的参数
java·大数据·开发语言·人工智能·程序人生