配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.decorator

系列导航:Java 25 设计模式学习指南

上一篇:组合 · 下一篇:外观

文本展示需要去除首尾空白,有些页面还需要外层方括号。若用继承表示“原文本”“去空白文本”“带框文本”“去空白且带框文本”,组合一多,子类就会迅速增长。

装饰器把每一种增强独立成一个对象,并允许它包装同一接口的另一个对象。调用者仍然只执行 read,却可以在装配处决定处理顺序。

从问题提炼设计意图

装饰器通过组合包装组件,保持相同接口并在调用前后增加行为,使增强能够在对象层面动态组合。

对象职责与协作关系

示例角色 职责
TextSource 组件接口
PlainText 提供原始文本的具体组件
TextDecorator 保存并委托内层组件的装饰基类
TrimmedText、FramedText 两种可组合增强
classDiagram
    TextSource <|.. PlainText
    TextSource <|.. TextDecorator
    TextDecorator --> TextSource : wraps
    TextDecorator <|-- TrimmedText
    TextDecorator <|-- FramedText

代码思路:变化应该落在哪个对象上

TextDecorator 自己实现 TextSource,同时又持有一个 TextSource。这是可递归包装的关键:被包装者可以是 PlainText,也可以是另一个装饰器。

TrimmedText 先调用 super.read 取得内层结果,再 strip;FramedText 则给内层结果加方括号。外层调用会向内逐层委托,结果再从内向外逐层加工。装配顺序定义了运算顺序。

以“ Java ”为输入,先去空白再加框得到“[Java]”;先加框再去空白得到“[ Java ]”,因为空格已经在方括号内部。这个反例说明装饰组合通常不满足交换律,测试应明确关心顺序。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.decorator,源码目录为 src/main/java/com/hanserwei/patterns/decorator/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

TextDecorator.java

package com.hanserwei.patterns.decorator;

import java.util.Objects;

/** 把调用委托给被包装对象的装饰基类. */
public abstract class TextDecorator implements TextSource {
  /** 被包装的文本源,可以是另一个装饰器. */
  private final TextSource source;

  /** 注入被包装组件. */
  protected TextDecorator(TextSource source) {
    this.source = Objects.requireNonNull(source, "source");
  }

  /** 读取内层文本,供具体装饰器增强. */
  @Override
  public String read() {
    return source.read();
  }
}

TrimmedText.java

package com.hanserwei.patterns.decorator;

/** 可组合的文本增强器. */
public final class TrimmedText extends TextDecorator {
  /** 包装文本源并保留同一读取接口. */
  public TrimmedText(TextSource source) {
    super(source);
  }

  /** 先读取内层结果,再应用本层转换. */
  @Override
  public String read() {
    return super.read().strip();
  }
}

FramedText.java

package com.hanserwei.patterns.decorator;

/** 可组合的文本增强器. */
public final class FramedText extends TextDecorator {
  /** 包装文本源并保留同一读取接口. */
  public FramedText(TextSource source) {
    super(source);
  }

  /** 先读取内层结果,再应用本层转换. */
  @Override
  public String read() {
    return "[" + super.read() + "]";
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.decorator;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    TextSource source = new FramedText(new TrimmedText(new PlainText(" Java ")));
    System.out.println(source.read());
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runDecorator
./gradlew test --tests 'com.hanserwei.patterns.decorator.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

[Java]

用测试确认模式的行为

交换装饰顺序得到不同结果,原文本保持不变;单层包装也符合 TextSource 契约。

对应测试位于 src/test/java/com/hanserwei/patterns/decorator/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 流、文本处理和消息处理中按需叠加转换能力。
  • 给组件加日志、计量、压缩等横切行为,同时维持调用接口。
  • 运行时根据配置选择增强组合,避免组合子类爆炸。

适用边界与容易踩的坑

增强不能破坏组件契约。如果内层承诺一次性流语义,装饰器反复读取它就可能出错;如果内层需要关闭资源,包装层必须清楚谁负责关闭、关闭顺序如何。本例使用纯字符串,故意避开资源生命周期,但真实实现不能忽略它。

包装层太多会让调试栈和行为顺序难以理解。装配代码应集中、可读,并测试关键组合;不是层数越多设计越先进。

本例的装饰器不改变 PlainText,因而多个组合可以共享同一个原始文本对象。如果包装的是可变组件,共享会产生不同生命周期下的状态关联,需要重新判断所有权。

与相近模式比较

代理也可能保持相同接口并持有目标对象,但重点是控制访问,例如缓存或权限;装饰器的重点是给调用方组合额外能力。外形相似时,应从意图和客户端的装配方式判断。

动手练习

增加 UppercaseText,并列举三种装配顺序的预期结果。再把增强换成“截断到固定长度”,寻找两个不满足交换律的组合,解释为什么测试必须覆盖顺序而非只单测每个装饰器。

系列导航:Java 25 设计模式学习指南

上一篇:组合 · 下一篇:外观