【软考设计模式】装饰模式:“套娃“式增强与动态职责扩展精讲
2026/7/16 9:32:16 网站建设 项目流程

系列定位:软考软件设计师 / 系统架构设计师 — 结构型模式专题第 3 讲

考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现

难度等级:⭐⭐⭐☆☆(结构像套娃,层层包装,掌握递归组合后很稳)


一、考纲定位与模式定义

1.1 考纲要求

装饰模式在软考中属于结构型模式的基础内容。考察形式包括:

  • 上午选择题:判断场景描述所属模式;识别装饰模式与桥接模式、适配器模式的区别;判断类图中是否存在 "递归组合" 结构(Decorator 组合 Component)

  • 下午设计题:补全 Decorator 抽象类的构造器(注入 Component)和委托逻辑;补全 ConcreteDecorator 中新增的职责方法;识别类图中 Component、ConcreteComponent、Decorator、ConcreteDecorator 四个角色

1.2 模式定义

装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。

核心意图:当需要在不改变原有对象接口的前提下,给对象动态添加新功能时,不用继承(继承是静态的、编译时确定的),而是用包装(Wrapper)的方式,在运行时层层套娃。每一层装饰者都持有被装饰对象的引用,在调用被装饰对象方法的前后插入自己的增强逻辑。

通俗理解

你去奶茶店点一杯奶茶。基础款是 "珍珠奶茶"(ConcreteComponent),你可以加料:+ 椰果(ConcreteDecoratorA)、+ 布丁(ConcreteDecoratorB)、+ 奶盖(ConcreteDecoratorC)。每加一种料,都是在原有奶茶的基础上包装一层,最终你拿到的是一杯 "奶盖 + 布丁 + 椰果 + 珍珠奶茶"。每一层都不改变奶茶的 "喝" 这个接口,但增加了新的口感(职责)。


二、UML 类图与角色划分

┌────────────────────────────────────────────────────────────┐ │ Client │ │ + main() │ │ ├── Component c = new ConcreteComponent(); │ │ ├── c = new ConcreteDecoratorA(c); // 第一层包装 │ │ ├── c = new ConcreteDecoratorB(c); // 第二层包装 │ │ └── c.operation(); // 逐层调用,最终到 ConcreteComponent │ └─────────────────────────────┬──────────────────────────────┘ │ uses ▼ ┌────────────────────────────────────────────────────────────┐ │ <<interface>> │ │ Component │ │ + operation() │ └─────────────────────────────┬──────────────────────────────┘ ┌────────────┴────────────┐ ▼ ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ ConcreteComponent │ │ Decorator │ │ (被装饰的原始对象) │ │ - component: │ │ + operation() │ │ Component │ │ (原始业务逻辑) │ │ + operation() { │ │ │ │ // 可前置增强 │ │ │ │ component. │ │ │ │ operation(); │ │ │ │ // 可后置增强 │ │ │ │ } │ └────────────────────────┘ └──────────┬─────────────┘ │ ┌──────────────────┴──────────────────┐ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ │ ConcreteDecoratorA │ │ ConcreteDecoratorB │ │ + operation() { │ │ + operation() { │ │ // 增强A │ │ // 增强B │ │ super.op(); │ │ super.op(); │ │ // 增强A │ │ // 增强B │ │ } │ │ } │ │ + addedBehavior() │ │ + addedBehavior() │ └─────────────────────┘ └─────────────────────┘
角色职责软考填空关键词
Component(抽象组件)定义一个对象接口,规范所有对象(被装饰者和装饰者)的统一接口interface/abstract class+operation()
ConcreteComponent(具体组件)被装饰的原始对象,实现 Component 接口,完成核心功能原始业务类,如CoffeeFileReader
Decorator(抽象装饰者)实现 Component 接口,组合一个 Component 引用,将请求委托给被装饰对象abstract class+protected Component component+ 构造器注入
ConcreteDecorator(具体装饰者)继承 Decorator,在operation()中调用父类委托方法的前后添加自己的增强逻辑extends Decorator+super.operation()+ 新增职责方法
Client(客户端)创建 ConcreteComponent,并用 ConcreteDecorator 层层包装,最终调用operation()new DecoratorA(new DecoratorB(new Component()))

类图识别要点:软考类图中,装饰模式的核心特征是Decorator 类与 Component 是组合关系(Decorator 持有 Component 引用),且 Decorator 与 Component 实现同一个接口(或继承同一个抽象类)。如果类图里只有一个转换类连接两个不同接口 → 适配器;如果有两个独立继承树 → 桥接;如果是一个递归包装结构 → 装饰。


三、场景一:咖啡加料(最经典的装饰模式)

业务背景:奶茶店的基础饮品是 "珍珠奶茶"(ConcreteComponent),顾客可以加料:椰果、布丁、奶盖。每加一种料,价格增加,描述也变化。要求在不改变奶茶类的前提下,动态添加配料。

说明:这是 GoF 原书中装饰模式的经典例子,也是软考下午题最爱考的 "层层包装增强" 场景。

3.1 代码实现
// Component:饮品接口 public interface Beverage { String getDescription(); double getCost(); } // ConcreteComponent:珍珠奶茶(原始对象) public class PearlMilkTea implements Beverage { @Override public String getDescription() { return "珍珠奶茶"; } @Override public double getCost() { return 12.0; } } // Decorator:抽象装饰者(所有加料的父类) public abstract class CondimentDecorator implements Beverage { protected Beverage beverage; public CondimentDecorator(Beverage beverage) { this.beverage = beverage; } } // ConcreteDecorator:椰果加料 public class Coconut extends CondimentDecorator { public Coconut(Beverage beverage) { super(beverage); } @Override public String getDescription() { return beverage.getDescription() + " + 椰果"; } @Override public double getCost() { return beverage.getCost() + 2.0; } } // ConcreteDecorator:布丁加料 public class Pudding extends CondimentDecorator { public Pudding(Beverage beverage) { super(beverage); } @Override public String getDescription() { return beverage.getDescription() + " + 布丁"; } @Override public double getCost() { return beverage.getCost() + 3.0; } } // ConcreteDecorator:奶盖加料 public class CheeseFoam extends CondimentDecorator { public CheeseFoam(Beverage beverage) { super(beverage); } @Override public String getDescription() { return beverage.getDescription() + " + 奶盖"; } @Override public double getCost() { return beverage.getCost() + 5.0; } public String getTaste() { return "咸香芝士味"; } } // Client:客户端层层包装 public class Client { public static void main(String[] args) { Beverage beverage = new PearlMilkTea(); System.out.println(beverage.getDescription() + " ¥" + beverage.getCost()); beverage = new Coconut(beverage); System.out.println(beverage.getDescription() + " ¥" + beverage.getCost()); beverage = new Pudding(beverage); System.out.println(beverage.getDescription() + " ¥" + beverage.getCost()); beverage = new CheeseFoam(beverage); System.out.println(beverage.getDescription() + " ¥" + beverage.getCost()); Beverage combo = new CheeseFoam(new Pudding(new Coconut(new PearlMilkTea()))); System.out.println(combo.getDescription() + " ¥" + combo.getCost()); } }

关键体会:每一层装饰者都持有内层对象的引用(beverage),在getDescription()getCost()中先调用内层对象的方法,再追加自己的逻辑。最终调用像剥洋葱一样,从外层逐层向内传递到PearlMilkTea


四、场景二:日志装饰(实际开发场景,前置/后置增强)

业务背景:系统有一个基础的日志记录器SimpleLogger(ConcreteComponent),只负责写日志到文件。现在需要动态添加以下功能:① 给每条日志加时间戳 ② 给每条日志加 IP 地址 ③ 给每条日志加线程名。这些增强功能可以任意组合,且不能修改SimpleLogger的源码。

说明:这是实际开发中 AOP(面向切面编程)思想的简单实现,也是软考下午题中常见的 "在方法前后插入增强逻辑" 场景。

4.1 代码实现
// Component:日志接口 public interface Logger { void log(String message); } // ConcreteComponent:基础日志记录器 public class SimpleLogger implements Logger { @Override public void log(String message) { System.out.println("[LOG] " + message); } } // Decorator:抽象日志装饰者 public abstract class LoggerDecorator implements Logger { protected Logger logger; public LoggerDecorator(Logger logger) { this.logger = logger; } } // ConcreteDecorator:时间戳装饰 public class TimestampLogger extends LoggerDecorator { public TimestampLogger(Logger logger) { super(logger); } @Override public void log(String message) { String timestamp = java.time.LocalDateTime.now().toString(); logger.log("[" + timestamp + "] " + message); } } // ConcreteDecorator:IP 地址装饰 public class IpLogger extends LoggerDecorator { private String ip; public IpLogger(Logger logger, String ip) { super(logger); this.ip = ip; } @Override public void log(String message) { logger.log("[IP:" + ip + "] " + message); } } // ConcreteDecorator:线程名装饰 public class ThreadLogger extends LoggerDecorator { public ThreadLogger(Logger logger) { super(logger); } @Override public void log(String message) { String threadName = Thread.currentThread().getName(); logger.log("[Thread:" + threadName + "] " + message); } } // Client:客户端自由组合 public class Client { public static void main(String[] args) { Logger logger = new SimpleLogger(); logger.log("用户登录成功"); Logger logger2 = new TimestampLogger(new SimpleLogger()); logger2.log("用户登录成功"); Logger logger3 = new ThreadLogger( new IpLogger( new TimestampLogger(new SimpleLogger()), "192.168.1.100" ) ); logger3.log("用户登录成功"); } }

关键体会:装饰者可以在被装饰对象方法调用的前后插入增强逻辑(本例是前置增强)。客户端可以自由组合装饰顺序:A(B(C(component)))B(A(C(component)))的效果可能不同,取决于各装饰者的逻辑。


五、场景三:Java IO 风格(BufferedReader 包装 FileReader)

业务背景:Java 标准库中的 IO 流就是装饰模式的经典实现。FileReader(ConcreteComponent)提供基础的文件读取功能,BufferedReader(ConcreteDecorator)在FileReader的基础上增加了缓冲功能,LineNumberReader(ConcreteDecorator)又增加了行号追踪功能。使用装饰模式模拟这个设计。

说明:这是软考上午题中常考的 "识别 Java IO 中的设计模式" 场景。Reader是 Component,FileReader是 ConcreteComponent,BufferedReader是 Decorator。

5.1 代码实现
// Component:数据读取接口 public interface DataReader { String read(); } // ConcreteComponent:文件读取器(基础功能) public class FileReader implements DataReader { private String filename; public FileReader(String filename) { this.filename = filename; } @Override public String read() { return "文件 " + filename + " 的原始内容"; } } // Decorator:抽象读取装饰者 public abstract class ReaderDecorator implements DataReader { protected DataReader reader; public ReaderDecorator(DataReader reader) { this.reader = reader; } } // ConcreteDecorator:缓冲装饰(增加缓冲功能) public class BufferedReader extends ReaderDecorator { private String buffer; public BufferedReader(DataReader reader) { super(reader); } @Override public String read() { if (buffer == null) { buffer = reader.read(); System.out.println("[BufferedReader] 已缓冲数据"); } return buffer; } public String readLine() { return read() + "(按行读取)"; } } // ConcreteDecorator:行号追踪装饰 public class LineNumberReader extends ReaderDecorator { private int lineNumber = 0; public LineNumberReader(DataReader reader) { super(reader); } @Override public String read() { lineNumber++; String content = reader.read(); return "[" + lineNumber + "] " + content; } public int getLineNumber() { return lineNumber; } } // Client public class Client { public static void main(String[] args) { DataReader reader = new FileReader("report.txt"); System.out.println(reader.read()); DataReader buffered = new BufferedReader(new FileReader("report.txt")); System.out.println(buffered.read()); System.out.println(buffered.read()); DataReader lineReader = new LineNumberReader( new BufferedReader(new FileReader("report.txt")) ); System.out.println(lineReader.read()); System.out.println(lineReader.read()); } }

六、三种场景对比与演进思路

维度场景一:咖啡加料场景二:日志装饰场景三:Java IO
ComponentBeverageLoggerDataReader
ConcreteComponentPearlMilkTeaSimpleLoggerFileReader
DecoratorCondimentDecoratorLoggerDecoratorReaderDecorator
ConcreteDecoratorCoconutPuddingCheeseFoamTimestampLoggerIpLoggerThreadLoggerBufferedReaderLineNumberReader
增强方式累加描述和价格前置添加元信息前置缓冲/行号
新增方法getTaste()(奶盖特有)readLine()getLineNumber()
包装层数任意多层任意多层任意多层
软考考法下午大题(补全价格累加)下午大题(补全前置增强)上午选择题(识别 Java IO)

七、装饰模式 vs 桥接模式 vs 适配器模式:核心对比

对比项装饰模式桥接模式适配器模式
设计目的动态增强对象功能,不改变接口分离抽象与实现,独立变化转换接口,兼容不兼容的类
是否改变接口❌ 不改变(保持 Component 接口)❌ 不改变(各自定义接口)✅ 改变(Adapter 转接口)
是否新增功能✅ 新增功能❌ 不新增(只是分离)❌ 不新增(只是转换)
类图结构递归组合(Decorator 组合 Component)两个独立继承树一个 Adapter 转换类
关系类型组合(Decorator 持有 Component)组合(Abstraction 持有 Implementor)组合/继承(Adapter 持有/继承 Adaptee)
Client 调用new Decorator(new Component())new Abstraction(new Implementor())new Adapter(new Adaptee())
运行时特性可动态叠加多层创建时绑定实现层创建时绑定被适配者
软考判断递归包装 + 接口相同 + 增强功能两个继承树 + 组合一个转换类 + 接口不同

记忆口诀:装饰是 "套娃增强"(层层包装,功能叠加),桥接是 "拆桥分流"(两个维度独立),适配器是 "搭桥翻译"(接口转换兼容)。


八、软考高频考点与易混淆辨析

8.1 高频考点
考点内容
模式分类结构型模式(GoF 23 正式成员)
核心四角色Component(抽象组件)、ConcreteComponent(具体组件)、Decorator(抽象装饰者)、ConcreteDecorator(具体装饰者)
核心思想递归组合:Decorator 组合 Component,且两者实现同一接口
装饰者构造器必须接收一个 Component 参数(public Decorator(Component c)),用于注入被装饰对象
委托逻辑Decorator 的operation()中调用component.operation(),前后插入增强逻辑
与继承的区别装饰是运行时动态增强,继承是编译时静态绑定;装饰更灵活
适用场景① 需要在不影响其他对象的情况下,动态、透明地给单个对象添加职责 ② 需要撤销职责时 ③ 不能用子类扩展时
Java IO 例子BufferedReader装饰FileReaderDataInputStream装饰InputStream
8.2 易混淆辨析:装饰 vs 代理模式
对比项装饰模式代理模式
目的增强对象功能(添加新职责)控制对象访问(权限、延迟加载、远程调用)
对客户端透明性通常不透明(客户端知道自己在用装饰者)通常透明(客户端以为在调用真实对象)
是否新增接口可以新增方法(如readLine()一般不新增方法,保持与真实对象一致
典型例子咖啡加料、Java IO 流远程代理、虚拟代理、保护代理
软考判断看到 "添加职责"、"增强功能" → 装饰看到 "控制访问"、"延迟加载"、"权限检查" → 代理

关键区分:装饰是为了 "加功能",代理是为了 "控访问"。如果类图里 Decorator 新增了Component没有的方法 → 装饰;如果 Proxy 完全保持原接口,只是控制调用时机 → 代理。


九、真题风格模拟与代码填空

模拟题 1(上午选择题)

以下关于装饰模式的叙述中,正确的是()。

A. 装饰模式属于创建型模式,主要用于创建具有增强功能的对象

B. 在装饰模式中,Decorator 通过继承 Component 来获得被装饰对象的功能

C. 装饰模式可以在不改变对象接口的前提下,动态地给对象添加额外职责

D. 装饰模式与适配器模式的核心区别在于:装饰模式转换接口,适配器模式增强功能

答案:C

解析

  • A 错误:装饰属于结构型模式,不是创建型。

  • B 错误:Decorator 通过组合(持有 Component 引用)获得被装饰对象的功能,不是继承。如果用继承,就无法在运行时动态替换被装饰对象。

  • C 正确:这是装饰模式的核心定义。

  • D 错误:说反了。装饰模式增强功能(不改变接口),适配器模式转换接口(不改变功能)。


模拟题 2(下午代码填空 — 补全装饰者构造器和委托逻辑)

某系统使用装饰模式实现数据加密传输。基础组件DataSender负责发送明文,EncryptionDecorator在发送前对数据进行加密。请补全(1)~(4)。

// Component:数据发送接口 interface DataSender { void send(String data); } // ConcreteComponent:基础发送器(发送明文) class PlainSender implements DataSender { public void send(String data) { System.out.println("发送数据: " + data); } } // Decorator:抽象装饰者 abstract class SenderDecorator implements DataSender { protected (1)______ sender; public SenderDecorator((2)______ sender) { this.sender = sender; } } // ConcreteDecorator:加密装饰者 class EncryptionDecorator extends SenderDecorator { public EncryptionDecorator(DataSender sender) { (3)______; } @Override public void send(String data) { String encrypted = "ENC(" + data + ")"; System.out.println("[加密] 原文: " + data + " -> 密文: " + encrypted); (4)______; } } public class Client { public static void main(String[] args) { DataSender sender = new EncryptionDecorator(new PlainSender()); sender.send("敏感信息"); } }

答案

  • (1)DataSender

  • (2)DataSender

  • (3)super(sender)

  • (4)sender.send(encrypted)

阅卷要点

  • (1)(2) 必须是DataSender(Component 接口类型),体现面向接口编程。不能写PlainSender等具体类。

  • (3) 必须是super(sender),调用父类 Decorator 的构造器完成注入。如果写this.sender = sender虽然功能正确,但不符合装饰模式的规范写法(应该由父类统一持有引用),软考阅卷时可能扣分。

  • (4) 必须是sender.send(encrypted),这是将增强后的数据委托给被装饰者发送。如果写super.sender.send(encrypted)也算对,但sender.send(encrypted)更简洁。


模拟题 3(下午代码填空 — 多层装饰与新增方法)

某系统使用装饰模式实现图片处理。基础组件Image提供display()方法。BorderDecorator增加边框显示,FilterDecorator增加滤镜效果。FilterDecorator还新增了getFilterName()方法。请补全(1)~(3)。

// Component interface Image { void display(); } // ConcreteComponent class Photo implements Image { public void display() { System.out.println("显示照片"); } } // Decorator abstract class ImageDecorator implements Image { protected Image image; public ImageDecorator(Image image) { this.image = image; } } // ConcreteDecorator:边框装饰 class BorderDecorator extends ImageDecorator { public BorderDecorator(Image image) { super(image); } public void display() { System.out.println("绘制边框"); (1)______; } } // ConcreteDecorator:滤镜装饰 class FilterDecorator extends ImageDecorator { private String filterName; public FilterDecorator(Image image, String filterName) { super(image); this.filterName = filterName; } public void display() { System.out.println("应用滤镜: " + filterName); (2)______; } public String getFilterName() { return (3)______; } } public class Client { public static void main(String[] args) { Image image = new FilterDecorator( new BorderDecorator(new Photo()), "黑白" ); image.display(); } }

答案

  • (1)image.display()

  • (2)image.display()

  • (3)filterName

阅卷要点

  • (1)(2) 必须是image.display(),这是 Decorator 将请求委托给被装饰的 Component 的标准写法。如果写super.image.display()也算对,但通常直接写image.display()

  • 注意:如果 (1) 写成super.display()零分!因为ImageDecorator没有实现display()方法(抽象类),调用super.display()会编译错误。

  • (3) 必须是filterName,返回装饰者特有的属性。


十、常见陷阱与注意事项

陷阱 1:误认为装饰是创建型模式

装饰属于结构型模式,因为它解决的是对象之间的结构关系(递归组合),而不是对象的创建问题。软考上午题如果问"以下属于创建型模式的是",选项里出现"装饰"不能选

陷阱 2:Decorator 继承 Component 而不是组合

这是软考最高频的失分点。装饰模式的核心是组合代替继承

  • ❌ 错误:class Decorator extends Component(继承,静态绑定,无法动态替换)

  • ✅ 正确:class Decorator { protected Component component; }(组合,运行时注入)

软考代码填空里,Decorator 必须持有protected Component xxx字段,并通过构造器注入。如果 Decorator 直接继承 ConcreteComponent,那就是错误的继承滥用。

陷阱 3:Decorator 的构造器没有注入 Component

装饰者的构造器必须接收一个 Component 参数,用于注入被装饰对象。如果构造器是无参的(如new Decorator()),那就无法持有内层对象,失去了装饰的意义。软考代码填空里,如果 Decorator 构造器参数为空,通常直接判错。

陷阱 4:与适配器模式混淆
装饰模式适配器模式
接口不变(Component 接口)接口改变(Target → Adaptee 转换)
目的是增强功能目的是兼容接口
可以新增方法一般不新增方法

如果类图里 Component 和 Decorator 的方法名完全一致,只是 Decorator 在前后加了增强逻辑 → 装饰。如果 Adapter 的方法名与 Adaptee 不同 → 适配器。

陷阱 5:与代理模式混淆

装饰和代理的类图结构几乎一样(都实现了同一接口,都组合了目标对象)。区别在于意图

  • 装饰:为了添加新功能(如加料、加时间戳、加缓冲)。

  • 代理:为了控制访问(如延迟加载、权限检查、远程调用)。

软考中如果题干强调 "增加功能"、"增强"、"添加职责" → 装饰;强调 "控制访问"、"代理"、"延迟加载" → 代理。

陷阱 6:多层装饰时的顺序问题

多层装饰的顺序可能影响最终效果。例如new A(new B(c))new B(new A(c))的结果可能不同。软考代码填空通常不涉及顺序问题,但选择题可能问 "以下哪种包装顺序能得到 XXX 效果",需要仔细分析每一层装饰者的前后置逻辑。

陷阱 7:ConcreteDecorator 忘记调用super.operation()

如果 ConcreteDecorator 的operation()中只写了自己的增强逻辑,忘记了调用component.operation()(或super.operation()),那么被装饰对象的原始功能就丢失了。软考阅卷时,如果operation()方法里没有委托调用,通常会扣分。


十一、总结

要点内容
定义动态地给一个对象添加一些额外的职责,就增加功能来说,比生成子类更为灵活
分类结构型模式(GoF 23 正式成员)
核心四角色Component(抽象组件)、ConcreteComponent(具体组件)、Decorator(抽象装饰者)、ConcreteDecorator(具体装饰者)
核心思想递归组合:Decorator 组合 Component,且两者实现同一接口,运行时层层包装
装饰者构造器必须接收 Component 参数,注入被装饰对象
委托逻辑operation()中调用component.operation(),前后插入增强逻辑
与继承的区别装饰是运行时动态增强,继承是编译时静态绑定;装饰更灵活
与适配器区别装饰:增强功能,不改变接口;适配器:转换接口,不改变功能
与代理区别装饰:添加新功能;代理:控制对象访问
Java 典型例子BufferedReader装饰FileReaderDataInputStream装饰InputStream
软考重点代码填空(补全 Decorator 构造器 + 委托调用);类图识别(递归组合结构);与适配器/代理的辨析
答题技巧看到 "动态添加职责"、"层层包装"、"不改变接口" → 装饰;看到new Decorator(new Component())的套娃写法 → 确认装饰

系列预告:下一篇将讲外观模式—— 当子系统接口过于复杂、客户端调用成本过高时,如何用 "门面" 类将复杂子系统的接口统一封装,提供一个简洁的高层接口。咱们下回见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询