news 2026/10/3 2:01:15

java-design-patterns 装饰器(Decorator)模式实战指南:用组合替代继承动态扩展对象职责

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
java-design-patterns 装饰器(Decorator)模式实战指南:用组合替代继承动态扩展对象职责
  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

装饰器(Decorator)模式是 GoF 提出的经典结构性设计模式,其核心思想是动态地为对象附加额外职责,用对象组合替代子类继承来扩展功能。本文以 java-design-patterns 仓库中的decorator模块为骨架,通过完整的巨魔(Troll)实战示例、源码调用链与单元测试,讲解装饰器模式的定义、四类角色、实现要点、适用场景及 JDK 中的真实应用。读完本文,你将能够独立实现一个符合单一职责原则、支持运行时动态叠加职责的装饰器组件。

或称:包装器(Wrapper)

装饰器模式在业界常被称为包装器,因为它本质上就是用一个装饰类对象把目标对象"包装"起来:装饰类与目标实现同一接口,内部持有目标对象的引用,对外透明地转发调用,并在转发前后插入额外的行为。

目的

动态地为对象附加额外的职责。装饰器为子类提供了一种灵活的替代方案,以扩展功能。

  • 动态:职责的叠加发生在运行时,而非编译期;
  • 透明:对调用方来说,装饰后的对象与原始对象使用方式完全一致(同一接口);
  • 替代继承:避免为了每种功能组合创建大量子类。

真实世界例子:愤怒的巨魔

附近的山丘上住着一个愤怒的巨魔。通常它是徒手的,但有时它有武器。为了武装巨魔不必创建新的巨魔,而是用合适的武器动态地装饰它。

通俗地说:装饰器模式让你可以在运行时通过把对象包装进一个装饰类对象中,来动态改变一个对象的行为。

维基百科给出的定义更加严谨:

在面向对象的编程中,装饰器模式是一种设计模式,它允许将行为静态或动态地添加到单个对象中,而不会影响同一类中其他对象的行为。装饰器模式通常对于遵守单一责任原则很有用,因为它允许将功能划分到具有唯一关注领域的类之间。

模式角色与类图

从 decorator/etc/decorator.urm.puml 中的 PlantUML 定义可以看出,本模块共有四个参与者,正好对应装饰器模式的四个标准角色:

角色本模块实现类职责
抽象构件(Component)Troll接口定义业务方法attack()、getAttackPower()、fleeBattle()
具体构件(ConcreteComponent)SimpleTroll实现接口的原始对象,被装饰的目标
装饰(Decorator)ClubbedTroll持有Troll decorated引用,实现同一接口,转发调用并叠加行为
客户端(Client)App运行时组装具体构件与装饰器

装饰器模式类图:Troll 接口由 SimpleTroll 与 ClubbedTroll 实现,ClubbedTroll 通过组合持有被装饰的 Troll 引用

类图中的关键关系:

  • SimpleTroll ..|> Troll:具体构件实现接口;
  • ClubbedTroll ..|> Troll:装饰器也实现同一接口,保证类型透明;
  • ClubbedTroll --> "-decorated" Troll:装饰器组合(而非继承)目标对象,这是整个模式能够动态叠加职责的根基。

程序示例:从徒手到持棒

下面以巨魔为例,从源码层面完整走一遍装饰器模式的实现。

第一步:抽象构件——巨魔接口

首先定义一个简单的巨魔接口,它声明了巨魔的全部行为。对应源码 decorator/src/main/java/com/iluwatar/decorator/Troll.java:

public interface Troll { void attack(); int getAttackPower(); void fleeBattle(); }

第二步:具体构件——徒手巨魔

然后创建一个简单的巨魔,直接实现Troll接口,初始攻击力为 10。对应源码 decorator/src/main/java/com/iluwatar/decorator/SimpleTroll.java:

@Slf4j public class SimpleTroll implements Troll { @Override public void attack() { LOGGER.info("The troll tries to grab you!"); } @Override public int getAttackPower() { return 10; } @Override public void fleeBattle() { LOGGER.info("The troll shrieks in horror and runs away!"); } }

注意这里使用了 Lombok 的@Slf4j注解生成日志对象,日志框架为 SLF4J + Logback,依赖声明可见 decorator/pom.xml。

第三步:装饰器——持棒巨魔

接下来我们想为巨魔添加一根球棒。按照装饰器模式,不修改SimpleTroll,也不为它创建子类,而是新建一个ClubbedTroll装饰器:它实现Troll接口,通过构造器注入被装饰的巨魔,并在转发调用时叠加行为。对应源码 decorator/src/main/java/com/iluwatar/decorator/ClubbedTroll.java:

@Slf4j @RequiredArgsConstructor public class ClubbedTroll implements Troll { private final Troll decorated; @Override public void attack() { decorated.attack(); LOGGER.info("The troll swings at you with a club!"); } @Override public int getAttackPower() { return decorated.getAttackPower() + 10; } @Override public void fleeBattle() { decorated.fleeBattle(); } }

这里@RequiredArgsConstructor为final字段decorated自动生成构造器。逐方法分析装饰器的行为:

  • attack():先调用decorated.attack()转发原始攻击,再追加"挥棒"日志——这是典型的前置/后置增强;
  • getAttackPower():返回decorated.getAttackPower() + 10——把攻击力从 10 增强到 20;
  • fleeBattle():原样转发,不做任何增强——装饰器可以选择性增强部分方法。

第四步:客户端组装——实战演示

程序入口 decorator/src/main/java/com/iluwatar/decorator/App.java 演示了"先徒手作战、再动态装备球棒"的完整过程:

// simple troll var troll = new SimpleTroll(); troll.attack(); // The troll tries to grab you! troll.fleeBattle(); // The troll shrieks in horror and runs away! LOGGER.info("Simple troll power: {}.\n", troll.getAttackPower()); // 10 // change the behavior of the simple troll by adding a decorator var clubbedTroll = new ClubbedTroll(troll); clubbedTroll.attack(); // The troll tries to grab you! The troll swings at you with a club! clubbedTroll.fleeBattle(); // The troll shrieks in horror and runs away! LOGGER.info("Clubbed troll power: {}.\n", clubbedTroll.getAttackPower()); // 20

两个关键观察点:

  1. 运行时动态装饰:troll对象本身从未被修改,ClubbedTroll只是把它"包"了一层,包装前后troll的行为完全不变,而clubbedTroll的行为被增强了;
  2. 原始对象可复用:同一个SimpleTroll实例既能独自作战,也能被任意多个装饰器反复包装,不影响同一类中其他对象的行为。

运行与测试验证

编译运行

本模块是一个独立的 Maven 工程,可通过 Maven 直接运行主类查看日志输出(主类com.iluwatar.decorator.App已在 decorator/pom.xml 的maven-assembly-plugin中配置):

mvn -pl decorator compile exec:java -Dexec.mainClass=com.iluwatar.decorator.App

或在模块目录内直接执行:

mvn package java -cp target/decorator-*.jar com.iluwatar.decorator.App

单元测试验证

仓库为装饰器模块提供了三个测试,从不同角度验证了模式行为:

SimpleTrollTest.java:验证具体构件的行为。断言getAttackPower()返回 10,attack()与fleeBattle()分别输出对应日志(通过自定义InMemoryAppender捕获 Logback 日志),并断言共产生 2 条日志。

ClubbedTrollTest.java:这是最有说服力的测试,使用 Mockito 的spy验证委托关系与增强逻辑:

final var simpleTroll = spy(new SimpleTroll()); final var clubbed = new ClubbedTroll(simpleTroll); assertEquals(20, clubbed.getAttackPower()); // 增强:10 + 10 verify(simpleTroll, times(1)).getAttackPower(); // 委托:增强基于原对象 clubbed.attack(); // 转发原始攻击 verify(simpleTroll, times(1)).attack(); clubbed.fleeBattle(); // 原样转发逃跑 verify(simpleTroll, times(1)).fleeBattle(); verifyNoMoreInteractions(simpleTroll);

测试明确印证了装饰器的两个核心语义:增强逻辑确实发生在原对象结果之上(20 = 10 + 10),且所有调用都被委托给被装饰对象,装饰器自身没有引入额外状态。

AppTest.java:验证主方法App.main可无异常执行,保证示例可运行。

适用性:什么时候使用装饰器

根据原文档并结合源码结构,在以下场景中应当考虑使用装饰器模式:

  1. 动态透明地向单个对象添加职责,即不影响其他对象——如上例,给一个巨魔加球棒不影响其他徒手巨魔;
  2. 对于可以撤销的职责——装饰可以随时解除(去掉包装即恢复原对象),职责的叠加顺序也可控;
  3. 当通过子类化进行扩展不切实际时——有时可能存在大量独立的扩展,若为每种组合都建立子类,会产生组合爆炸。例如功能 A、B、C 的任意组合需要 2³ 种子类;而用装饰器,只需要 3 个装饰类即可在运行时任意叠加。此外,当类定义被隐藏或无法用于子类化时,装饰器是唯一可行的扩展手段。

装饰器在 Java 世界中的真实案例

原文档列出了装饰器模式在标准 JDK 中的经典应用,这些都是可以随时打开源码验证的实例:

  • java.io体系:java.io.InputStream、java.io.OutputStream、java.io.Reader、java.io.Writer 及其子类体系——例如BufferedInputStream、DataInputStream都是装饰器,层层包装同一抽象输入流,逐层叠加缓冲、类型化读取等能力;
  • java.util.Collections的视图方法:
    • Collections#synchronizedXXX()——为任意集合动态附加线程安全能力;
    • Collections#unmodifiableXXX()——为任意集合动态附加只读保护;
    • Collections#checkedXXX()——为任意集合动态附加类型检查。

这三个系列的共同特征是:传入一个普通集合,返回一个"行为被改变但接口相同"的包装集合,且原集合本身不受影响——与巨魔示例的机制完全一致。

实现要点与设计权衡

结合源码 ClubbedTroll.java 可以总结出四条可复用的实现准则:

  1. 装饰器与构件必须实现同一接口(Troll),这是"透明"的前提,客户端无需感知对象是否被装饰;
  2. 用组合而非继承:装饰器持有final Troll decorated引用,在构造器中注入,保证职责可在运行时动态组合;
  3. 转发与增强分离:需要增强的方法先decorated.xxx()再追加逻辑(如attack()),需要原样传递的方法直接转发(如fleeBattle()),增强逻辑保持单一职责、互不干扰;
  4. 注意顺序敏感性与"洋葱"结构:多个装饰器叠加时呈层层包裹的洋葱结构,调用顺序即包装顺序,叠加职责过多时调试成本会上升,因此装饰器应保持小而专一。

总结

装饰器模式用一个"包装器"类解决了子类组合爆炸与运行时扩展两大痛点:SimpleTroll负责核心行为,ClubbedTroll只负责"加球棒"这一单一职责,两者通过Troll接口松耦合,客户端在运行时自由组装。从 java-design-patterns 的 decorator 模块源码 及其 测试用例 可以确认:增强基于委托结果、调用透明转发、原对象零侵入——这正是装饰器模式在 JDK IO 流与 Collections 工具类中被广泛采用的底层原因。

  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

相关推荐

上一篇:5个简单步骤:如何用LangFlow可视化界面快速构建AI应用
下一篇:如何 3 分钟搞定 scrcpy 安卓投屏:一份面向新手的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 2:00:24

如何搭建自己的开源法律助手:3 款中文法律大模型选型与部署指南

如何搭建自己的开源法律助手:3 款中文法律大模型选型与部署指南 【免费下载链接】Awesome-Chinese-LLM 整理开源的中文大语言模型,以规模较小、可私有化部署、训练成本较低的模型为主,包括底座模型,垂直领域微调及应用&#xff0c…

作者头像 李华
网站建设 2026/10/3 1:58:39

STM32F1自定义HID实战:寄存器级USB协议栈开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华