设计模式里如果只能让我挑一个,落实到日常业务中最出效果的,我大概率会选装饰者模式。它解决的不是那种百年一遇的架构难题,而是每天都在发生的需求——给一个对象动态加功能,但又不想动它原来的代码。第一次用继承去堆功能,你会觉得很爽,等子类多到连自己都认不全的时候,就该回头看看装饰者了。这篇文章我打算从继承失控的真实痛点讲起,把装饰者模式的结构、代码、业务场景和踩坑经验一次说透,适合刚接触设计模式的读者,也适合写了很多业务代码、想理清封装思路的开发者。
1. 装饰者模式到底是什么——从一个真实痛点说起
1.1 继承为什么会让代码走向失控
很多项目是从一个简单的类开始的。比如消息通知,最开始只有邮件通知,一个类搞定。后来产品说要加短信通知,你心想简单,抽个接口,写两个实现类。再后来需求开始升级,有人要发带附件的邮件,有人要发加密短信,还有人要既带附件又带签名。如果继续用继承,组合的类别数量就会呈爆炸式增长。
以通知功能为例算算账:2种基础渠道(邮件、短信),3种附加能力(附件、加密、签名)。用继承来做,你需要创建6个组合类。如果基础渠道变成4种,附加能力变成5种,组合类的数量就是4乘5等于20个。这还只是单层扩展,如果附件和加密能叠加,类数量会直接变成指数级。我见过最夸张的代码里有十几个类,命名全是 EmailWithAttachmentEncrypted、SmsWithSignatureEncrypted 这种组合,改一个基础逻辑,所有相关子类全要重新编译。
这还只是类爆炸的问题。真正让人头大的是继承引入了强耦合,附加能力被固化在类的层级里,运行时想换一套组合根本做不到。产品经理说“今天先发加密邮件,明天只发带附件的短信”,代码层面就要增加新类,改动范围根本无法控制。用继承扩展功能这条路,越往后走越是给自己埋雷。
1.2 装饰者模式的破局思路
装饰者模式给出的解法非常朴素:不通过继承来扩展,而是用组合层层包装。核心思想是把附加功能拆成独立的小类,每个小类只做一件事,然后在运行时动态组装。
你可以把这种组装方式想象成俄罗斯套娃。最里面是一个基础对象,外层一层套一层,每一层负责一种附加能力。无论套了多少层,客户端拿到的仍然是同一个接口类型,调用方法时内层外层会依次执行,最终得到一个完整的结果。
这个思路有两个关键点。第一,基础类和装饰类实现的是同一个接口,所以客户端根本不需要关心自己拿到的是原始对象还是被包装过的对象。第二,装饰类里持有被装饰对象的引用,调用时先做自己那部分事情,再把请求传递下去。这样功能可以无限叠加,而且互不干扰。
这套设计完美契合了开闭原则——对扩展开放,对修改关闭。要增加一个新功能时,你只需要新增一个装饰类,不用改动任何已有的类。对比继承那一长串类,改动量完全不在一个量级。我第一次把项目的价格计算从继承重构为装饰者时,最大的感受是终于不用再为每一个组合场景新建类了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 装饰者模式的结构与代码实现
2.1 四个角色,先看清楚谁是谁
装饰者模式涉及四个角色,理解他们是掌握整个模式的前提。
| 角色 | 名称 | 职责 |
|---|---|---|
| Component | 抽象组件 | 定义业务接口,规定核心行为 |
| ConcreteComponent | 具体组件 | 实现接口,是被装饰的基础对象 |
| Decorator | 抽象装饰类 | 持有Component引用,维持接口一致性 |
| ConcreteDecorator | 具体装饰类 | 实现具体的附加功能 |
实际代码里,抽象装饰类是一个让很多初学者困惑的存在。既然装饰器本质上也是一个Component,为什么不直接让具体装饰类去实现接口,而要中间夹一个抽象装饰类?这背后是一个很实在的考量:所有具体装饰器都需要持有被装饰对象的引用,而且这个引用必须通过构造函数注入。如果把这些公共逻辑抽到抽象装饰类里,每个具体装饰器只需要写自己那部分增强逻辑,代码会清爽很多。
抽象装饰类的另一个作用是把装饰器的类型收敛起来。你可以在代码里看到某个类是 CoffeeDecorator 的子类,一眼就知道它是用来装饰咖啡的,而不是去实现一个宽泛的接口,语义更明确。
