设计模式之装饰器模式 理论总结 C++代码实战

文章目录

一、我对装饰器模式的理解

在完整实现完装饰器模式的代码后,我对这个设计模式有了更具象的理解:装饰器模式是一种结构型设计模式,它严格遵循开闭原则(对扩展开放,对修改关闭),在不修改原有核心组件代码的前提下,通过组合与继承的方式,为对象动态、灵活地添加新的功能或职责

通俗来说,装饰器模式就像我们买奶茶:核心茶底(基础核心类)是固定的,珍珠、椰果、奶盖这些配料(装饰器)可以自由添加、自由组合、随时增减,全程不需要修改茶底本身的配方,就能得到不同口味的奶茶。它的核心不是单纯靠继承加功能,而是通过「继承保证接口统一」+「组合委托实现功能扩展」,实现动态、无侵入的能力增强.。

它的核心逻辑,是把核心功能和附加装饰功能解耦。我们可以为同一个核心对象,通过不同的装饰器组合,实现不同的功能扩展,无需改动核心类的任何代码,也不会影响其他使用该核心类的逻辑。

当然它也存在一定的局限性:多层装饰会产生较深的调用链路,排查问题时需要逐层梳理,增加了调试难度;同时过度使用会产生大量细粒度的装饰器类,一定程度上提升了系统的理解成本。

那装饰器模式到底是如何落地实现的?我们结合课本中标准的装饰器模式类图,来拆解它的核心实现逻辑:

核心角色拆解

装饰器模式的实现,离不开这4个核心角色,每个角色各司其职,共同构成了完整的装饰器体系:

  1. 抽象组件(Component):定义了核心对象的统一行为接口,是具体组件和抽象装饰器的共同父类,确保了被装饰的核心对象和装饰器具备一致的行为规范,能够实现嵌套组合。
  2. 具体组件(ConcreteComponent):也就是我们的核心基础类,它实现了抽象组件的接口,定义了最基础、最核心的功能,是后续被装饰器包装、扩展的主体。
  3. 抽象装饰器(Decorator):所有装饰器的抽象父类,它同样继承自抽象组件,同时内部持有一个抽象组件类型的对象指针。这个设计是装饰器模式的核心------它既保证了装饰器和核心组件具备相同的接口,又通过组合的方式,将核心功能的执行委托给持有的组件对象,为后续的功能扩展留出了空间。
  4. 具体装饰器(ConcreteDecoratorA/ConcreteDecoratorB):继承自抽象装饰器,通过重写接口方法,在调用原有组件的核心功能前后,添加自定义的扩展逻辑,实现对核心对象的功能增强。

以上就是装饰器模式的核心设计思想,单看类图和定义难免有些抽象,接下来我们就结合「超市小票打印」这个真实的业务需求,把装饰器模式落地到代码中,彻底搞懂它的使用方式。

二、根据实际需求将模式和业务对应

实验需求背景

本次我们需要基于装饰器模式,实现XX大学超市的小票打印程序,并编写客户端测试代码。

核心需求:小票中的商品明细信息为必填核心项,其余抬头、合计金额、售后提示、温馨公告、广告招商、停车福利等内容,均可自由选择是否添加、自由调整打印顺序,同时要保证打印内容的格式规范。

小票基础样式参考如下:

需求与装饰器模式的角色拆解

结合需求,我们可以把小票打印的业务逻辑,和装饰器模式的4个核心角色一一对应起来,让每个角色承担对应的业务职责:

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

而售后提示ServiceTipsPrint()逻辑是:

  • 无前置逻辑
  • 调用component->Print():执行内层对象的打印方法
  • 后置逻辑:打印售后提示和联系电话

结合完整的示例,当我们调用over->Print()时,完整的执行链路是:

  1. 执行FreeParkPrint():先调用component->Print()(也就是SloganPrint()),执行完内层后,再打印停车福利内容
  2. 执行SloganPrint():先调用component->Print()(也就是PublicNoticePrint()),执行完内层后,再打印招商广告内容
  3. 执行PublicNoticePrint():先调用component->Print()(也就是ServiceTipsPrint()),执行完内层后,再打印公告内容
  4. 执行ServiceTipsPrint():先调用component->Print()(也就是ReceiptTotalPrint()),执行完内层后,再打印售后提示内容
  5. 执行ReceiptTotalPrint():先调用component->Print()(也就是ReceiptHeaderPrint()),执行完内层后,再打印合计金额
  6. 执行ReceiptHeaderPrint():先打印抬头和分割线(前置逻辑),再调用component->Print()(也就是核心ProductDetailsPrint()
  7. 执行ProductDetailsPrint():遍历打印所有商品明细,这是整个链路的最内层
  8. 执行完成后,再按照调用的原路,逐层返回执行每个装饰器的后置逻辑,最终完成整个小票的打印

简单总结就是:构造从内到外,执行从外到内;前置逻辑先于内层执行,后置逻辑后于内层执行。理解了这个核心逻辑,你就彻底搞懂了装饰器模式的嵌套执行原理。

五、作者的踩坑记录

在从零实现这个Demo的过程中,我也踩了不少装饰器模式的经典坑,在这里整理出来,帮大家避坑:

  1. 对装饰器模式的核心设计理解偏差:最开始我误以为装饰器模式就是简单的类嵌套,没有在抽象装饰器中定义抽象组件的指针,完全偏离了装饰器的核心设计。这里一定要记住:装饰器的核心是「组合优于继承」,是通过持有对象指针实现委托调用,而非单纯的类嵌套继承。
  2. 忽略了装饰器构造函数的传参设计:装饰器模式能够实现多层嵌套的核心,就是每个装饰器的构造函数,必须接收一个抽象组件类型的对象,并传递给父类完成内部指针的初始化。如果这一步缺失,装饰器就无法关联到被装饰的对象,整个模式的链路就会断裂。
  3. 头文件重复包含导致的类重定义报错 :一定要在每个头文件的开头加上#pragma once(或者使用头文件保护宏),否则在多个文件交叉引用时,会出现大量的类重定义编译错误,排查起来非常耗时。

六、写在最后

写完整个Demo再回头看,装饰器模式的核心精髓,其实就是「用组合替代继承,实现动态的功能扩展」。它把核心功能和附加装饰功能完全解耦,让我们可以像搭积木一样,通过不同装饰器的组合,为同一个核心对象赋予不同的能力,同时全程不修改原有核心代码,完美符合开闭原则。

那到底什么场景下适合使用装饰器模式?

当我们需要为一个类动态添加可撤销的功能、或者需要排列组合大量的扩展功能时,装饰器模式就是绝佳的选择。就像我们这次的小票打印,核心的商品明细是固定的,而其他的附加内容可以自由组合、增删,用装饰器模式就能非常优雅地实现,而不是写大量的子类去覆盖各种组合情况。

如果这篇博客对你有帮助的话能够我点一个赞吗,这是对我对我最大的鼓励,如果想日和再拿出来看看复习下收藏一下,如果对我写的设计模式感兴趣的话我日后会持续更新,点个关注,下一篇更精彩,这个专栏的下一篇文章大概率是状态模式哦!

相关推荐
无籽西瓜a1 小时前
【西瓜带你学设计模式 | 第十八期 - 命令模式】命令模式 —— 请求封装与撤销实现、优缺点与适用场景
java·后端·设计模式·软件工程·命令模式
脱氧核糖核酸__2 小时前
LeetCode热题100——54.螺旋矩阵(题解+答案+要点)
c++·算法·leetcode·矩阵
!停2 小时前
C++入门STL容器string底层剖析
开发语言·c++
会编程的土豆2 小时前
【数据结构与算法】栈的应用
数据结构·c++·算法
神仙别闹2 小时前
基于C++实现的简单的SMTP服务器
服务器·开发语言·c++
程序设计基础课组2 小时前
codeblock找不到MINGW64编译器怎么办?
c++·codeblocks
xcjbqd02 小时前
Qt Quick中QML与C++交互详解及场景切换实现
c++·qt·交互
!停2 小时前
C++入门STL容器string使用基础
开发语言·c++
m0_716765232 小时前
数据结构--栈的插入、删除、查找详解
开发语言·数据结构·c++·经验分享·学习·青少年编程·visual studio