文章目录
- 一、我对装饰器模式的理解
- 二、根据实际需求将模式和业务对应
- 三、实现实际需求
- 四、效果演示
-
- 观察者模式调用的顺序和逻辑
-
- [1. 构造顺序:从内到外,逐层包装](#1. 构造顺序:从内到外,逐层包装)
- [2. 执行顺序:从外到内,逐层深入,再原路返回](#2. 执行顺序:从外到内,逐层深入,再原路返回)
- 五、作者的踩坑记录
- 六、写在最后

一、我对装饰器模式的理解
在完整实现完装饰器模式的代码后,我对这个设计模式有了更具象的理解:装饰器模式是一种结构型设计模式,它严格遵循开闭原则(对扩展开放,对修改关闭),在不修改原有核心组件代码的前提下,通过组合与继承的方式,为对象动态、灵活地添加新的功能或职责。
通俗来说,装饰器模式就像我们买奶茶:核心茶底(基础核心类)是固定的,珍珠、椰果、奶盖这些配料(装饰器)可以自由添加、自由组合、随时增减,全程不需要修改茶底本身的配方,就能得到不同口味的奶茶。它的核心不是单纯靠继承加功能,而是通过「继承保证接口统一」+「组合委托实现功能扩展」,实现动态、无侵入的能力增强.。
它的核心逻辑,是把核心功能和附加装饰功能解耦。我们可以为同一个核心对象,通过不同的装饰器组合,实现不同的功能扩展,无需改动核心类的任何代码,也不会影响其他使用该核心类的逻辑。
当然它也存在一定的局限性:多层装饰会产生较深的调用链路,排查问题时需要逐层梳理,增加了调试难度;同时过度使用会产生大量细粒度的装饰器类,一定程度上提升了系统的理解成本。
那装饰器模式到底是如何落地实现的?我们结合课本中标准的装饰器模式类图,来拆解它的核心实现逻辑:

核心角色拆解
装饰器模式的实现,离不开这4个核心角色,每个角色各司其职,共同构成了完整的装饰器体系:
- 抽象组件(Component):定义了核心对象的统一行为接口,是具体组件和抽象装饰器的共同父类,确保了被装饰的核心对象和装饰器具备一致的行为规范,能够实现嵌套组合。
- 具体组件(ConcreteComponent):也就是我们的核心基础类,它实现了抽象组件的接口,定义了最基础、最核心的功能,是后续被装饰器包装、扩展的主体。
- 抽象装饰器(Decorator):所有装饰器的抽象父类,它同样继承自抽象组件,同时内部持有一个抽象组件类型的对象指针。这个设计是装饰器模式的核心------它既保证了装饰器和核心组件具备相同的接口,又通过组合的方式,将核心功能的执行委托给持有的组件对象,为后续的功能扩展留出了空间。
- 具体装饰器(ConcreteDecoratorA/ConcreteDecoratorB):继承自抽象装饰器,通过重写接口方法,在调用原有组件的核心功能前后,添加自定义的扩展逻辑,实现对核心对象的功能增强。
以上就是装饰器模式的核心设计思想,单看类图和定义难免有些抽象,接下来我们就结合「超市小票打印」这个真实的业务需求,把装饰器模式落地到代码中,彻底搞懂它的使用方式。
二、根据实际需求将模式和业务对应
实验需求背景
本次我们需要基于装饰器模式,实现XX大学超市的小票打印程序,并编写客户端测试代码。
核心需求:小票中的商品明细信息为必填核心项,其余抬头、合计金额、售后提示、温馨公告、广告招商、停车福利等内容,均可自由选择是否添加、自由调整打印顺序,同时要保证打印内容的格式规范。
小票基础样式参考如下:

需求与装饰器模式的角色拆解
结合需求,我们可以把小票打印的业务逻辑,和装饰器模式的4个核心角色一一对应起来,让每个角色承担对应的业务职责:
- 抽象组件(抽象接口) :我们将小票的核心行为抽象为
Receipt抽象类,它定义了小票打印的统一规范,包含纯虚函数Print()(打印小票内容)、getTotalPrice()(获取商品总金额),同时声明虚析构函数,保证继承类的正确析构。 - 具体组件(核心基础类) :对应
ProductDetails类,它是小票的核心主体,继承并实现了Receipt抽象类的接口。它的核心职责是管理商品明细数据,内部通过std::vector<std::pair<std::string, float>> goodsList存储商品名称和价格,提供商品的添加、删除方法,同时重写打印函数实现商品明细的遍历输出,重写金额计算函数实现总金额的统计。 - 抽象装饰器 :对应
Decorator抽象类,它是所有小票装饰器的统一父类,同样继承自Receipt抽象类。它的核心设计是内部持有一个Receipt*类型的指针,指向被装饰的小票对象;同时通过构造函数接收外部传入的Receipt对象,完成内部指针的初始化------这一步是装饰器模式能够实现嵌套组合的核心基础。它默认实现的Print()和getTotalPrice(),会直接委托给内部持有的组件对象执行,为子类的扩展留出了空间。 - 具体装饰器 :对应小票抬头、合计金额、售后提示、公告、广告、停车福利等不同的装饰类,它们均继承自
Decorator抽象装饰器。通过构造函数接收小票对象并传递给父类完成初始化,同时重写Print()方法,在调用父类的打印方法前后,添加当前装饰器对应的打印内容,实现对小票内容的扩展。
下面是具体的类图:

三、实现实际需求
抽象组件(Receipt抽象接口)
cpp
#pragma once
class Receipt {
public:
virtual void Print() = 0;
virtual float getTotalPrice() = 0;
virtual ~Receipt() = default;
};
具体组件(商品明细核心类)
cpp
#pragma once
#include <iostream>
#include <vector>
#include <utility>
#include <string>
#include "Receipt.hpp"
class ProductDetails : public Receipt{
public:
void Print() override
{
for (auto& [name, price] : goodsList)
{
printf("%-20s %.1f\n", name.c_str(), price);
}
}
void Push(std::string name, float price)
{
goodsList.push_back({ name, price });
}
void Erase(std::string name)
{
for (auto it = goodsList.begin(); it != goodsList.end(); )
{
if (it->first == name)
{
it = goodsList.erase(it); // 删除后自动返回下一个迭代器
}
else
{
++it;
}
};
}
float getTotalPrice() override
{
float sum = 0.0;
for (auto& [name, price] : goodsList)
{
sum += price;
}
return sum;
}
private:
std::vector<std::pair<std::string, float>> goodsList;
};
抽象装饰器(Decorator)
cpp
#pragma once
#include "Receipt.hpp"
class Decorator : public Receipt{
public:
Decorator(Receipt* component)
:component(component)
{ }
void Print() override
{
component->Print();
}
float getTotalPrice() override
{
return component->getTotalPrice();
}
protected:
Receipt* component;
};
具体装饰器实现
小票抬头装饰器
cpp
#pragma once
#include <iostream>
#include <string>
#include "Decorator.hpp"
class ReceiptHeader : public Decorator{
public:
ReceiptHeader(Receipt* component)
: Decorator(component)
{}
// 重写打印:先打印抬头,再执行内层对象的打印逻辑
void Print() override {
// 居中打印超市名称
std::string title = "XX大学超市";
int totalWidth = 26; // 小票总宽度(和分割线对齐)
int leftPadding = (totalWidth - title.size()) / 2;
// 输出空格+标题实现居中效果
std::cout << std::string(leftPadding, ' ') << title << std::endl;
std::cout << "----------------------------" << std::endl;
// 调用内层对象的打印方法
component->Print();
}
};
售后提示装饰器
cpp
#pragma once
#include <iostream>
#include <string>
#include "Decorator.hpp"
class ServiceTips : public Decorator {
public:
ServiceTips(Receipt* component)
:Decorator(component)
{ }
// 重写打印:先执行内层打印,再打印售后提示内容
void Print() override
{
component->Print();
// 固定小票宽度
const int WIDTH = 26;
std::cout << "**************************\n";
// 居中打印售后提示内容
std::string line1 = "14天购物保证。货真价实";
std::string line2 = "XX超市电话83688888";
int pad1 = (WIDTH - line1.size()) / 2;
int pad2 = (WIDTH - line2.size()) / 2;
std::cout << std::string(pad1, ' ') << line1 << '\n';
std::cout << std::string(pad2, ' ') << line2 << '\n';
}
};
公共公告装饰器
cpp
#pragma once
#include <iostream>
#include <string>
#include "Decorator.hpp"
class PublicNotice : public Decorator {
public:
PublicNotice(Receipt* component)
: Decorator(component)
{ }
void Print() override
{
component->Print();
const int WIDTH = 26;
std::cout << "**************************\n";
std::string line1 = "货物售出概不退款";
std::string line2 = "保护环境,请勿随意丢弃";
int pad1 = (WIDTH - line1.size()) / 2;
int pad2 = (WIDTH - line2.size()) / 2;
std::cout << std::string(pad1, ' ') << line1 << '\n';
std::cout << std::string(pad2, ' ') << line2 << '\n';
}
};
合计金额装饰器
cpp
#pragma once
#include <stdio.h>
#include "Decorator.hpp"
class ReceiptTotal : public Decorator {
public:
ReceiptTotal(Receipt* component)
:Decorator(component)
{ }
void Print() override
{
// 先打印内层内容,再输出合计金额
component->Print();
printf("%-20s %.1f\n", "合计(人民币/元)", this->getTotalPrice());
}
};
广告招商装饰器
cpp
#pragma once
#include <iostream>
#include <string>
#include "Decorator.hpp"
class Slogan : public Decorator {
public:
Slogan(Receipt* component)
:Decorator(component)
{ }
void Print() override
{
component->Print();
const int WIDTH = 26;
std::cout << "**************************\n";
std::string line1 = "广告位招商!";
std::string line2 = "广告位招商!";
int pad1 = (WIDTH - line1.size()) / 2;
int pad2 = (WIDTH - line2.size()) / 2;
std::cout << std::string(pad1, ' ') << line1 << '\n';
std::cout << std::string(pad2, ' ') << line2 << '\n';
}
};
免费停车装饰器
cpp
#pragma once
#include <iostream>
#include <string>
#include "Decorator.hpp"
class FreePark : public Decorator {
public:
FreePark(Receipt* component)
:Decorator(component)
{ }
void Print() override
{
component->Print();
const int WIDTH = 26;
std::cout << "**************************\n";
std::string line1 = "凭此小票,免费停车";
int pad1 = (WIDTH - line1.size()) / 2;
std::cout << std::string(pad1, ' ') << line1 << '\n';
}
};
四、效果演示
装饰器模式的代码实现本身并不复杂,真正的核心难点,是理解装饰器构造顺序 和执行顺序的对应关系。同时装饰器模式的使用非常灵活,我们可以自由选择装饰器的组合,实现小票内容的自定义。
我们先来看完整的装饰器组合调用示例:
cpp
#include "Receipt.hpp"
#include "ProductDetails.hpp"
#include "Decorator.hpp"
#include "Composing.hpp"
#include "Advent.hpp"
#include "Bullentin.hpp"
#include "ReceiptTotal.hpp"
#include "Slogan.hpp"
#include "FreePark.hpp"
int main()
{
// 1. 创建核心的商品明细对象(最内层的具体组件)
ProductDetails* goods = new ProductDetails();
goods->Push("可口可乐500ml", 2.8);
goods->Push("桃李主食面包400g", 7.9);
// 2. 从内到外,逐层用装饰器包装核心对象
Receipt* r1 = new ReceiptHeader(goods); // 包装小票抬头
Receipt* r2 = new ReceiptTotal(r1); // 包装合计金额
Receipt* r3 = new ServiceTips(r2); // 包装售后提示
Receipt* r4 = new PublicNotice(r3); // 包装公共公告
Receipt* r5 = new Slogan(r4); // 包装招商广告
Receipt* over = new FreePark(r5); // 包装停车福利
// 3. 调用装饰器的打印方法
over->Print();
// 内存释放(实际项目中建议使用智能指针避免内存泄漏)
delete over;
delete r5;
delete r4;
delete r3;
delete r2;
delete r1;
delete core;
return 0;
}
执行上述代码,就能得到完整的小票打印效果,如下所示:

装饰器模式的灵活性就体现在这里:如果我们想要去掉广告招商的内容,只需要注释掉对应的装饰器包装代码,不把它加入到嵌套链路中即可,无需修改任何其他类的代码,示例如下:
cpp
#include "Receipt.hpp"
#include "ProductDetails.hpp"
#include "Decorator.hpp"
#include "Composing.hpp"
#include "Advent.hpp"
#include "Bullentin.hpp"
#include "ReceiptTotal.hpp"
#include "Slogan.hpp"
#include "FreePark.hpp"
int main()
{
// 创建核心商品对象
ProductDetails* goods = new ProductDetails();
goods->Push("可口可乐500ml", 2.8);
goods->Push("桃李主食面包400g", 7.9);
// 逐层包装,注释掉招商广告的装饰器,不加入链路
Receipt* r1 = new ReceiptHeader(goods);
Receipt* r2 = new ReceiptTotal(r1);
Receipt* r3 = new ServiceTips(r2);
Receipt* r4 = new PublicNotice(r3);
//Receipt* r5 = new Slogan(r4); // 去掉广告招商,直接注释该行即可
Receipt* over = new FreePark(r4); // 直接包装公告层的对象
// 调用最外层的打印方法
over->Print();
// 内存释放
delete over;
delete r4;
delete r3;
delete r2;
delete r1;
delete core;
return 0;
}

观察者模式调用的顺序和逻辑
我在写代码时候,弄不懂的就是多层嵌套下的执行逻辑和在函数中写的component->Print();到底是谁在调用,这里我就结合上面的示例,给大家讲清楚「构造顺序」和「Print执行顺序」的对应关系,彻底搞懂嵌套的底层逻辑。
1. 构造顺序:从内到外,逐层包装
我们的对象构造顺序,是先创建最核心的具体组件,再用装饰器一层一层从内到外包装 。
以上面的完整示例为例,构造链路是:
ProductDetails核心对象 → 被ReceiptHeader包装 → 被ReceiptTotal包装 → 被ServiceTips包装 → 被PublicNotice包装 → 被Slogan包装 → 被FreePark包装
每一层装饰器,都只关心自己持有的内层对象,不关心这个对象是核心组件,还是已经被其他装饰器包装过的对象,这也是它能实现无限嵌套的核心。
2. 执行顺序:从外到内,逐层深入,再原路返回
当我们调用最外层装饰器的Print()方法时,执行顺序和构造顺序完全相反,是从外到内执行 ,核心逻辑是:
每个装饰器重写的Print()方法,都会分为「前置逻辑」→ component->Print()(调用内层对象的打印方法)→「后置逻辑」三个部分。
我们以小票抬头ReceiptHeader为例,它的Print()逻辑是:
- 前置逻辑:打印超市抬头和分割线
- 调用
component->Print():执行内层对象的打印方法 - 无后置逻辑
而售后提示ServiceTips的Print()逻辑是:
- 无前置逻辑
- 调用
component->Print():执行内层对象的打印方法 - 后置逻辑:打印售后提示和联系电话
结合完整的示例,当我们调用over->Print()时,完整的执行链路是:
- 执行
FreePark的Print():先调用component->Print()(也就是Slogan的Print()),执行完内层后,再打印停车福利内容 - 执行
Slogan的Print():先调用component->Print()(也就是PublicNotice的Print()),执行完内层后,再打印招商广告内容 - 执行
PublicNotice的Print():先调用component->Print()(也就是ServiceTips的Print()),执行完内层后,再打印公告内容 - 执行
ServiceTips的Print():先调用component->Print()(也就是ReceiptTotal的Print()),执行完内层后,再打印售后提示内容 - 执行
ReceiptTotal的Print():先调用component->Print()(也就是ReceiptHeader的Print()),执行完内层后,再打印合计金额 - 执行
ReceiptHeader的Print():先打印抬头和分割线(前置逻辑),再调用component->Print()(也就是核心ProductDetails的Print()) - 执行
ProductDetails的Print():遍历打印所有商品明细,这是整个链路的最内层 - 执行完成后,再按照调用的原路,逐层返回执行每个装饰器的后置逻辑,最终完成整个小票的打印
简单总结就是:构造从内到外,执行从外到内;前置逻辑先于内层执行,后置逻辑后于内层执行。理解了这个核心逻辑,你就彻底搞懂了装饰器模式的嵌套执行原理。
五、作者的踩坑记录
在从零实现这个Demo的过程中,我也踩了不少装饰器模式的经典坑,在这里整理出来,帮大家避坑:
- 对装饰器模式的核心设计理解偏差:最开始我误以为装饰器模式就是简单的类嵌套,没有在抽象装饰器中定义抽象组件的指针,完全偏离了装饰器的核心设计。这里一定要记住:装饰器的核心是「组合优于继承」,是通过持有对象指针实现委托调用,而非单纯的类嵌套继承。
- 忽略了装饰器构造函数的传参设计:装饰器模式能够实现多层嵌套的核心,就是每个装饰器的构造函数,必须接收一个抽象组件类型的对象,并传递给父类完成内部指针的初始化。如果这一步缺失,装饰器就无法关联到被装饰的对象,整个模式的链路就会断裂。
- 头文件重复包含导致的类重定义报错 :一定要在每个头文件的开头加上
#pragma once(或者使用头文件保护宏),否则在多个文件交叉引用时,会出现大量的类重定义编译错误,排查起来非常耗时。
六、写在最后
写完整个Demo再回头看,装饰器模式的核心精髓,其实就是「用组合替代继承,实现动态的功能扩展」。它把核心功能和附加装饰功能完全解耦,让我们可以像搭积木一样,通过不同装饰器的组合,为同一个核心对象赋予不同的能力,同时全程不修改原有核心代码,完美符合开闭原则。
那到底什么场景下适合使用装饰器模式?
当我们需要为一个类动态添加可撤销的功能、或者需要排列组合大量的扩展功能时,装饰器模式就是绝佳的选择。就像我们这次的小票打印,核心的商品明细是固定的,而其他的附加内容可以自由组合、增删,用装饰器模式就能非常优雅地实现,而不是写大量的子类去覆盖各种组合情况。
如果这篇博客对你有帮助的话能够我点一个赞吗,这是对我对我最大的鼓励,如果想日和再拿出来看看复习下收藏一下,如果对我写的设计模式感兴趣的话我日后会持续更新,点个关注,下一篇更精彩,这个专栏的下一篇文章大概率是状态模式哦!
