设计模式(C++)详解—装饰器模式(2)
<摘要>
本文是一场关于装饰器模式的盛大巡礼!我们将穿越到软件设计的“蛮荒”时代,亲眼目睹“类爆炸”灾难如何催生了装饰器这一优雅的解决方案。全文以“给核心对象穿衣服”这一核心比喻贯穿始终,通过精致UML图、生动故事、战场对比和深入的技术剖析,全方位解构装饰器模式。你将不仅理解其“组合优于继承”的哲学内核,还能通过咖啡店、超级英雄、游戏装备、IO流等大量有趣案例,透彻掌握其在C++中的实现精髓(包括智能指针、移动语义等现代C++特性)。本文还深入探讨了其在STL、Qt等框架中的现实应用,并与代理、适配器等模式进行犀利对比。最后,一份完整的、可编译运行的代码库(附带详尽注释和Makefile)将为你打通从理论到实践的最后一公里。这是一份旨在让你彻底弄懂、真心爱上装饰器模式的终极指南。
<解析>
1. 背景与核心概念:从“类爆炸”废墟中诞生的救世主
1.1 史诗的开篇:一场名为“继承”的噩梦
想象一下,你是一名软件建筑师,接到了设计一个“咖啡店订单系统”的任务。最初,一切都很简单。
class Coffee {
public:virtual double cost() const { return 5.0; }
};
客户说:“我要加奶的咖啡。” 简单!继承搞定。
class CoffeeWithMilk : public Coffee {
public:double cost() const override { return Coffee::cost() + 2.0; }
};
客户又说:“我还要加糖!” 没问题!
class CoffeeWithMilkAndSugar : public CoffeeWithMilk {
public:double cost() const override { return CoffeeWithMilk::cost() + 1.0; }
};
然而,客户的欲望是无限的:“有没有只加糖的?有没有加奶、加糖还加巧克力的?有没有加双份奶的?……” 你的代码库很快变成了下图所示的噩梦:
这就是臭名昭著的 “类爆炸” (Class Explosion)!你的项目被无数的子类淹没,维护起来就像在走一个巨大的迷宫。添加一种新配料?意味着你要创建所有可能组合的新子类。这简直是软件工程界的“指数级灾难”。
1.2 救世主的降临:装饰器模式的华丽转身
就在这绝望之际,GoF(四人帮,Gang of Four)带来了他们的圣经《设计模式》,并揭示了装饰器模式。
它的核心思想简单而深刻:不要再执着于“是什么”(is-a)的继承关系,而是转向“有什么”(has-a)的组合关系。
给对象“穿衣服”:一个贯穿全文的完美比喻
- 你的身体:就是核心对象(
BasicCoffee
)。 - 一件T恤:是一个装饰器(
MilkDecorator
),它包裹着你的身体,提供了“棉质T恤”的功能。 - 一件外套:是另一个装饰器(
SugarDecorator
),它包裹在“T恤”外面,提供了“保暖”和“甜蜜”的功能。 - 一条围巾:……如此往复。
你可以在运行时随心所欲地穿上或脱下这些“衣服”(装饰器)。最终,从外表看,你还是你(因为它们都实现了Person
接口),但你的功能(保暖程度、风格)已经发生了巨大变化。
装饰器模式的四大天王(关键角色):
-
Component (抽象组件) -
Beverage
- 身份:家族的族长。它定义了整个家族最基本、最核心的契约(接口)。
- 职责:宣布“所有这个家族的对象,都必须能计算价格(
cost()
)和说出自己的描述(getDescription()
)”。 - C++绝技:拥有一个纯虚函数
cost() const = 0;
,让子类去实现。
-
ConcreteComponent (具体组件) -
Espresso
,HouseBlend
- 身份:家族里的老实人,族长最朴实的儿子。他们实现了核心功能,是我们要被装饰的原始对象。
- 职责:“我就是一杯纯粹的咖啡,这是我的本来价格和味道。”
- C++绝技:重写纯虚函数,提供具体的实现。
-
Decorator (抽象装饰器) -
CondimentDecorator
- 身份:家族的“包装师”。它既是家族的一员(继承自
Component
),又手里抓着另一个家族成员(组合一个Component
指针)。 - 职责:“我是所有装饰器的老大,我知道怎么持有一个组件,并维护
Component
的接口。具体的装饰活儿交给我的手下们。” - C++心机:它通常不会自己实现
cost()
,而是留给子类。它持有一个std::unique_ptr<Component>
,准备随时进行“装饰”。
- 身份:家族的“包装师”。它既是家族的一员(继承自
-
ConcreteDecorator (具体装饰器) -
MochaDecorator
,WhipDecorator
- 身份:家族的“化妆师”。各怀绝技,负责给对象添加具体的额外职责。
- 职责:“我包裹一个组件,然后在它的基础上,加上我自己的价格和描述。”
- C++绝技:在重写的
cost()
方法中,先调用被包裹对象的cost()
,然后加上自己的费用。
让我们用UML图来直观地看看这个神奇的家族关系:
▲ 装饰器模式标准UML类图(使用Mermaid绘制)
这幅图揭示了装饰器模式的魔法源泉:
- 同一接口:
ConcreteComponent
和Decorator
都是Component
的子类。这意味着Decorator
可以完美地冒充Component
,这就是“透明性”的基石。 - 组合关系:
Decorator
手中牢牢抓着一个Component
的指针。这个指针可以指向一个ConcreteComponent
,也可以指向另一个Decorator
。这就允许了装饰器的层层嵌套,形成一条“装饰链”。 - 递归思想:当你在最外层的装饰器上调用
cost()
时,它会将调用委托给内部的Component
,内部的Component
可能又是一个装饰器,它会继续向内委托,直到调用到达最核心的ConcreteComponent
。然后,计算结果会带着每一层装饰器添加的“附加值”,一层层地返回。这个过程就像一颗石子投入水中,涟漪一层层荡开,最后又一层层返回。
1.3 现状与趋势:无处不在的装饰之魂
装饰器模式早已超越了简单的咖啡店例子,其思想深深植根于现代软件开发的方方面面:
- Java I/O Streams: 这是最经典的案例。
FileInputStream
(具体组件)被BufferedInputStream
(装饰器)装饰,再被DataInputStream
(另一个装饰器)装饰,从而动态地添加了缓冲、读取基本数据类型等功能。 - C++ Standard Template Library (STL):
std::stack
和std::queue
被称为容器适配器(Container Adapters)。它们本质上是对底层容器(如std::deque
,std::list
)的装饰。它们限制了容器的接口,改变了其行为(如LIFO, FIFO),完美体现了“组合优于继承”。 - 图形用户界面 (GUI): Qt、Java SWING等框架中,为视觉组件添加边框、滚动条、阴影等,无一不是装饰器模式的用武之地。
- Web开发: React中的高阶组件 (Higher-Order Components, HOC) 和Python/Java中的装饰器语法(@decorator),是装饰器模式在函数式和注解编程范式下的现代化身。它们在不修改原有组件的情况下,为其注入新的props或行为。
装饰器模式的生命力在于它精准地抓住了“弹性扩展”这一永恒需求,是“开放-封闭原则”(对扩展开放,对修改关闭)最优雅的实践之一。
2. 设计意图与考量:在优雅与复杂间走钢丝
2.1 灵魂拷问:我们为何而战?
GoF对装饰器模式的意图定义是:“动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。”
这句看似平淡的话,背后是无数前辈在“类爆炸”的火海中总结出的智慧结晶。我们来一场深度灵魂拷问:
核心目标 | 意味着什么? | 如何实现? |
---|---|---|
动态地 (Dynamically) | 功能的添加是在运行时决定的,而不是在编译时写死的。 | 通过组合和对象指针,在程序运行时动态地组装装饰链。 |
额外的职责 (Additional Responsibilities) | 指的是那些可选的、非核心的功能。 | 每个具体装饰器封装一个单一的、明确的职责。 |
比生成子类更灵活 (More Flexible than Subclassing) | 彻底击败“类爆炸”,用线性增长的类数应对指数增长的功能组合。 | 利用组合的灵活性,将功能分解为多个小类,再递归地组合它们。 |
2.2 设计理念:权衡的艺术与智慧的结晶
任何设计都是权衡的产物,装饰器模式也不例外。它用一时的“复杂性”换来了长久的“灵活性”。
设计决策 | 带来的巨大好处 (The Upside) | 潜在的代价与考量 (The Downside) |
---|---|---|
1. 装饰器与组件共享同一接口 | 透明性 (Transparency):客户端完全不知道它面对的是一个被层层包裹的“套娃”,它依然快乐地调用着Component 的接口。这符合“最少知识原则”。 | 不纯粹的身份:装饰器在“是”一个组件的同时,又“有”一个组件。这有时会让基于类型的操作变得复杂。 |
2. 采用组合而非继承 | 极致灵活 (Ultimate Flexibility):运行时动态组合,功能无限叠加。完全符合“组合优于继承”的原则。 | 小对象瘟疫:会在系统中引入大量细粒度的装饰器对象,如果过度使用,会使系统变得复杂,难以理解和调试。 |
3. 装饰器可在被装饰对象行为前后添加新行为 | 强大扩展 (Powerful Extension):可以在调用原始操作前(如权限检查)、后(如日志记录)甚至中间(如缓冲)插入新行为。 | 顺序依赖症:装饰器的顺序有时至关重要!先加密再压缩,和先压缩再加密,结果是完全不同的。 |
4. 省略抽象的Decorator类 | 代码更简洁。如果装饰器种类很少,可以直接让具体装饰器继承Component 并组合一个Component 。 | 失去了一个有用的中间层,可能会使未来添加所有装饰器的共通功能变得困难。 |
为了更生动地展示这个动态组合的过程,我们来看一段“对象时装秀”的时序图:
▲ 装饰器模式调用时序图:一场价格的递归计算(使用Mermaid绘制)
2.3 战场对决:装饰器 vs. 它的“亲戚们”
在设计模式的世界里,分清“谁是谁”至关重要。装饰器有几个长相相似的“亲戚”,但它们的“职业”截然不同。
模式 | “职业” (目的) | “招牌动作” (关键区别) | 一句话人话 |
---|---|---|---|
装饰器 (Decorator) | 增强功能:不改变接口,增加新的职责。像给手机加外壳、贴膜。 | 透明扩展、递归组合。关注于动态、无限地添加功能。 | “我还是我,只是功能更强了。” |
代理 (Proxy) | 控制访问:控制对对象的访问,可能延迟创建、加权限控制、做网络代理等。 | 对象关系通常较静态,代理通常不或很少增强功能,而是做访问控制。 | “我替本体出面,控制谁可以见它,或者什么时候见。” |
适配器 (Adapter) | 转换接口:将一个接口转换成另一个客户端期望的接口。像电源转接头。 | 接口转换是核心目的,而不是增强功能。 | “我不是你想要的,但我能变成你想要的。” |
策略 (Strategy) | 改变行为:封装一系列算法,使它们可以相互替换。改变对象的内核。 | 改变对象的内核算法,而装饰器是在外围增加新的、辅助性的行为。 | “我换的是它的‘心脏’,而你只是给它穿‘衣服’。” |
组合 (Composite) | 统一处理:将对象组合成树形结构以表示“部分-整体”的层次结构。 | 旨在统一处理单个对象和对象组合,装饰器旨在增强单个对象的功能。 | “我和装饰器是好朋友,但我是管理‘集团’的,他是包装‘个人’的。” |
3. 实例与应用场景:模式的力量,源于解决真实的问题
让我们告别枯燥的咖啡店,看看装饰器模式在更多元、更酷的场景中是如何大显神通的。
实例一:超级英雄装备系统
场景:设计一个游戏中的超级英雄。英雄本身有基础能力,但可以通过装备各种高科技装备(蝙蝠侠战衣、钢铁侠盔甲、美国队长的盾牌)来动态获得新能力。
// Component: 英雄
class Hero {
public:virtual ~Hero() = default;virtual std::string getAbilities() const {return "Fist Fight"; // 基础能力:徒手格斗}virtual int getPower() const {return 10; // 基础战斗力}
};// ConcreteComponent: 布鲁斯·韦恩
class BruceWayne : public Hero {
public:std::string getAbilities() const override {return "Master Martial Artist, Genius-Level Intellect, " + Hero::getAbilities();}// ... 可能还有其他布鲁斯特有的属性
};// Decorator: 装备装饰器
class GearDecorator : public Hero {
protected:std::unique_ptr<Hero> hero;
public:explicit GearDecorator(std::unique_ptr<Hero> h) : hero(std::move(h)) {}// getAbilities 和 getPower 仍然是纯虚的?不,我们可以提供默认实现:直接委托。virtual std::string getAbilities() const override { return hero->getAbilities(); }virtual int getPower() const override { return hero->getPower(); }
};// ConcreteDecorator: 蝙蝠战衣
class BatsuitDecorator : public GearDecorator {
public:using GearDecorator::GearDecorator;std::string getAbilities() const override {return GearDecorator::getAbilities() + ", Advanced Armor, Grappling Hook, Stealth Tech";}int getPower() const override {return GearDecorator::getPower() + 50;}
};// ConcreteDecorator: 便携式飞行器
class JetpackDecorator : public GearDecorator {
public:using GearDecorator::GearDecorator;std::string getAbilities() const override {return GearDecorator::getAbilities() + ", Flight";}int getPower() const override {return GearDecorator::getPower() + 25;}
};// 客户端使用
void createBatman() {std::unique_ptr<Hero> bruce = std::make_unique<BruceWayne>();std::cout << "Bruce: " << bruce->getAbilities() << " | Power: " << bruce->getPower() << std::endl;// 动态装备!bruce = std::make_unique<BatsuitDecorator>(std::move(bruce));bruce = std::make_unique<JetpackDecorator>(std::move(bruce)); // 蝙蝠侠+战衣+飞行器!std::cout << "Batman with Jetpack: " << bruce->getAbilities() << " | Power: " << bruce->getPower() << std::endl;
}
输出:
Bruce: Master Martial Artist, Genius-Level Intellect, Fist Fight | Power: 10
Batman with Jetpack: Master Martial Artist, Genius-Level Intellect, Fist Fight, Advanced Armor, Grappling Hook, Stealth Tech, Flight | Power: 85
这个例子展示了如何动态地“组装”出一个强大的英雄,完美体现了装饰器模式的灵活性。
实例二:数据流处理管道 (I/O流的精髓)
场景:处理网络数据,需要经过解密、解压、校验等步骤。这些步骤的顺序和组合可能需要动态变化。
// Component: 数据处理器
class DataProcessor {
public:virtual ~DataProcessor() = default;virtual std::string process(const std::string& data) = 0;
};// ConcreteComponent: 基础处理器 (可能就是简单读写)
class BasicProcessor : public DataProcessor {
public:std::string process(const std::string& data) override {return data; // 原样返回}
};// Decorator: 处理过滤器
class ProcessingFilter : public DataProcessor {
protected:std::unique_ptr<DataProcessor> processor;
public:explicit ProcessingFilter(std::unique_ptr<DataProcessor> p) : processor(std::move(p)) {}
};// ConcreteDecorator: 解密过滤器
class DecryptionFilter : public ProcessingFilter {std::string key;
public:DecryptionFilter(std::unique_ptr<DataProcessor> p, std::string k) : ProcessingFilter(std::move(p)), key(std::move(k)) {}std::string process(const std::string& data) override {std::string encryptedData = processor->process(data); // 先让内部的处理器处理(如果有的话)return decrypt(encryptedData, key); // 然后自己解密}
private:std::string decrypt(const std::string& data, const std::string& key) { /*...*/ return "Decrypted(" + data + ")"; }
};// ConcreteDecorator: 解压过滤器
class DecompressionFilter : public ProcessingFilter {
public:using ProcessingFilter::ProcessingFilter;std::string process(const std::string& data) override {std::string compressedData = processor->process(data);return decompress(compressedData);}
private:std::string decompress(const std::string& data) { /*...*/ return "Decompressed(" + data + ")"; }
};// 客户端使用:构建处理管道
void processData() {// 构建一个管道:数据 -> 解密 -> 解压 -> 基础处理std::unique_ptr<DataProcessor> processor = std::make_unique<BasicProcessor>();processor = std::make_unique<DecompressionFilter>(std::move(processor));processor = std::make_unique<DecryptionFilter>(std::move(processor), "my_secret_key");std::string rawData = "Xyz...encrypted and compressed data...";std::string result = processor->process(rawData);std::cout << "Final Result: " << result << std::endl;// 输出可能是: Final Result: Decompressed(Decrypted(Xyz...encrypted and compressed data...))
}
这个例子深刻揭示了装饰器模式在构建灵活、可配置的处理管道方面的巨大优势。顺序至关重要!
实例三:GUI组件装饰 (给界面“美颜”)
场景:为UI组件动态添加视觉特效,如边框、滚动条、阴影、背景色等。
// Component: 视觉组件
class VisualComponent {
public:virtual ~VisualComponent() = default;virtual void draw() = 0;
};// ConcreteComponent: 文本框
class TextBox : public VisualComponent {
public:void draw() override {std::cout << "Drawing TextBox content" << std::endl;}
};// Decorator: 视觉装饰器
class VisualDecorator : public VisualComponent {
protected:std::unique_ptr<VisualComponent> component;
public:explicit VisualDecorator(std::unique_ptr<VisualComponent> comp) : component(std::move(comp)) {}void draw() override {component->draw(); // 默认委托}
};// ConcreteDecorator: 边框装饰
class BorderDecorator : public VisualDecorator {int width;std::string color;
public:BorderDecorator(std::unique_ptr<VisualComponent> comp, int w, std::string c): VisualDecorator(std::move(comp)), width(w), color(std::move(c)) {}void draw() override {VisualDecorator::draw(); // 1. 先画内部的组件drawBorder(); // 2. 再画边框(新增功能)}
private:void drawBorder() {std::cout << "Drawing Border: width=" << width << ", color=" << color << std::endl;}
};// ConcreteDecorator: 滚动条装饰
class ScrollDecorator : public VisualDecorator {
public:using VisualDecorator::VisualDecorator;void draw() override {VisualDecorator::draw();drawScrollBar();}
private:void drawScrollBar() {std::cout << "Drawing ScrollBar" << std::endl;}
};// 客户端使用
void createFancyTextBox() {std::unique_ptr<VisualComponent> textBox = std::make_unique<TextBox>();textBox = std::make_unique<BorderDecorator>(std::move(textBox), 2, "red");textBox = std::make_unique<ScrollDecorator>(std::move(textBox));textBox->draw();
}
输出:
Drawing TextBox content
Drawing Border: width=2, color=red
Drawing ScrollBar
GUI装饰是装饰器模式的另一个绝佳应用场景,它让我们可以像搭积木一样构建出复杂的UI效果。
4. C++代码实现:现代C++的优雅实践
让我们用现代C++(C++11/17)的最佳实践,完整地实现最经典的咖啡店例子。
4.1 带完整注释的代码实现
Beverage.h
/*** @file Beverage.h* @brief 装饰器模式核心头文件:定义抽象组件、具体组件、抽象装饰器和具体装饰器。* @details* 使用现代C++特性:智能指针(std::unique_ptr)管理资源,移动语义转移所有权,override明确重写。*/#pragma once // 跨平台防止重复包含#include <string>
#include <memory> // For std::unique_ptr
#include <utility> // For std::move/*** @brief 抽象饮料类(Component)* * 它是所有饮料和调料装饰器的共同基类,定义了整个模式的抽象接口。* 使用虚析构函数确保通过基类指针删除派生类对象时行为正确。*/
class Beverage {
public:virtual ~Beverage() = default; ///< 虚析构函数是多态基类的必需品/*** @brief 获取饮料的描述信息* * 非纯虚函数,提供默认实现。具体组件和装饰器可以重写它。* * @return std::string 描述字符串*/virtual std::string getDescription() const {return description_;}/*** @brief 计算饮料的价格* * 这是一个纯虚函数,强制所有具体子类都必须提供自己的实现。* * @return double 计算出的价格*/virtual double cost() const = 0;protected:std::string description_ = "Unknown Beverage"; ///< 饮料描述, protected 允许子类访问和修改
};/*** @brief 调料装饰器抽象类(Decorator)* * 继承自Beverage,因此它本身也是一种Beverage,可以替代Beverage对象。* 核心在于它组合(持有)了一个Beverage对象的智能指针。* 此类声明了getDescription()为纯虚函数,强制具体装饰器必须重写。*/
class CondimentDecorator : public Beverage {
public:virtual ~CondimentDecorator() override = default; // 虽然不必须,但显式声明是好习惯/*** @brief 获取描述(纯虚函数)* * 在装饰器模式中,装饰器必须修改描述,所以这里声明为纯虚,* 强制每个具体装饰器实现自己的描述追加逻辑。*/virtual std::string getDescription() const override = 0;// cost() 不在这个抽象层实现,留给具体装饰器protected:/*** @brief 被装饰的饮料对象* * 使用std::unique_ptr明确表示所有权:CondimentDecorator独占其持有的Beverage。* 通过构造函数注入,并使用std::move转移所有权,避免不必要的拷贝。*/std::unique_ptr<Beverage> beverage_;
};// --- 具体组件 (Concrete Components) ---/*** @brief 浓缩咖啡*/
class Espresso : public Beverage {
public:Espresso();double cost() const override;
};/*** @brief 黑咖啡*/
class HouseBlend : public Beverage {
public:HouseBlend();double cost() const override;
};// --- 具体装饰器 (Concrete Decorators) ---/*** @brief 摩卡调料装饰器*/
class Mocha : public CondimentDecorator {
public:/*** @brief 构造函数,接收一个需要被装饰的Beverage对象* * @param beverage 被装饰的饮料。注意:一旦传入,其所有权将转移给Mocha对象。*/explicit Mocha(std::unique_ptr<Beverage> beverage);std::string getDescription() const override;double cost() const override;
};/*** @brief 奶泡调料装饰器*/
class Whip : public CondimentDecorator {
public:explicit Whip(std::unique_ptr<Beverage> beverage);std::string getDescription() const override;double cost() const override;
};
Beverage.cpp
/*** @file Beverage.cpp* @brief 实现Beverage.h中声明的所有具体类。*/#include "Beverage.h"// ------------------------- 具体组件实现 -------------------------Espresso::Espresso() {description_ = "Espresso";
}
double Espresso::cost() const {return 1.99;
}HouseBlend::HouseBlend() {description_ = "House Blend Coffee";
}
double HouseBlend::cost() const {return 0.89;
}// ------------------------- 具体装饰器实现 -------------------------Mocha::Mocha(std::unique_ptr<Beverage> beverage) {beverage_ = std::move(beverage); // 接管传入对象的所有权
}
std::string Mocha::getDescription() const {// 在原有描述基础上,追加当前调料的描述。return beverage_->getDescription() + ", Mocha";
}
double Mocha::cost() const {// 计算价格:被装饰对象的价格 + 当前调料的价格。return beverage_->cost() + 0.20;
}Whip::Whip(std::unique_ptr<Beverage> beverage) {beverage_ = std::move(beverage);
}
std::string Whip::getDescription() const {return beverage_->getDescription() + ", Whip";
}
double Whip::cost() const {return beverage_->cost() + 0.10;
}
main.cpp
/*** @file main.cpp* @brief 装饰器模式客户端演示代码* * 展示如何动态地组合各种饮料和调料,并计算最终价格和描述。*/#include <iostream>
#include <memory>
#include "Beverage.h"/*** @brief 打印订单信息* * 辅助函数,用于统一格式化输出饮料信息。* * @param beverage 要打印的饮料对象*/
void printOrder(const Beverage& beverage) {std::cout << " " << beverage.getDescription() << " -- $" << beverage.cost() << std::endl;
}/*** @brief 主函数* * 程序入口,创建不同的饮料和调料组合,演示装饰器模式的强大功能。* * @return int 程序退出码*/
int main() {std::cout << "\n=== 装饰器模式咖啡店开业啦! ===\n" << std::endl;std::cout << "订单 1: 一杯浓缩咖啡" << std::endl;std::unique_ptr<Beverage> beverage1 = std::make_unique<Espresso>();printOrder(*beverage1);std::cout << "\n订单 2: 一杯双倍摩卡加奶泡的黑咖啡" << std::endl;// 1. 创建基础饮料:黑咖啡std::unique_ptr<Beverage> beverage2 = std::make_unique<HouseBlend>();// 2. 用装饰器进行第一次装饰:加一份摩卡beverage2 = std::make_unique<Mocha>(std::move(beverage2));// 3. 第二次装饰:再加一份摩卡beverage2 = std::make_unique<Mocha>(std::move(beverage2));// 4. 第三次装饰:加奶泡beverage2 = std::make_unique<Whip>(std::move(beverage2));printOrder(*beverage2);std::cout << "\n订单 3: 一杯只加奶泡的浓缩咖啡" << std::endl;// 另一种简洁的写法,但注意:一旦move,之前的指针就无效了。auto beverage3 = std::make_unique<Whip>(std::make_unique<Espresso>());printOrder(*beverage3);std::cout << "\n=== 感谢光临! ===\n" << std::endl;return 0;
}
4.2 编译与运行:Makefile一站式搞定
Makefile
# Decorator Pattern Demo Makefile
# 使用现代C++标准(C++17)并开启常用警告# 编译器配置
CXX := g++
CXXFLAGS := -std=c++17 -Wall -Wextra -pedantic -O2# 最终目标
TARGET := decorator_demo# 需要编译的源文件
SRCS := Beverage.cpp main.cpp
# 由源文件生成的目标文件
OBJS := $(SRCS:.cpp=.o)# 默认目标:构建可执行文件
all: $(TARGET)# 链接规则:将所有的.o文件链接成可执行文件
$(TARGET): $(OBJS)$(CXX) $(CXXFLAGS) -o $@ $^# 编译规则:每个.cpp文件生成一个.o文件
%.o: %.cpp Beverage.h$(CXX) $(CXXFLAGS) -c $< -o $@# 伪目标:运行程序
run: $(TARGET)@echo "运行程序..."@./$(TARGET)# 伪目标:清理生成的文件
clean:rm -f $(OBJS) $(TARGET)# 伪目标:显示帮助信息
help:@echo "可用命令:"@echo " make all 编译程序(默认)"@echo " make run 编译并运行程序"@echo " make clean 清理编译生成的文件"@echo " make help 显示此帮助信息"# 声明这些目标是“伪目标”,不代表实际文件
.PHONY: all run clean help
编译与运行:
- 保存文件:将上述四个文件(
Beverage.h
,Beverage.cpp
,main.cpp
,Makefile
)放在同一个目录下。 - 打开终端:导航到这个目录。
- 执行编译:
或者make
你会看到g++编译器的输出,如果没有错误,将生成名为make all
decorator_demo
的可执行文件。 - 运行程序:
或者直接运行:make run
./decorator_demo
预期输出与解读:
=== 装饰器模式咖啡店开业啦! ===订单 1: 一杯浓缩咖啡Espresso -- $1.99订单 2: 一杯双倍摩卡加奶泡的黑咖啡House Blend Coffee, Mocha, Mocha, Whip -- $1.39订单 3: 一杯只加奶泡的浓缩咖啡Espresso, Whip -- $2.09=== 感谢光临! ===
- 订单1:验证了基础组件
Espresso
的正确性。 - 订单2:是装饰器模式的精髓展示。描述清晰地显示了装饰的层次(
House Blend Coffee
->Mocha
->Mocha
->Whip
),价格也是正确的:0.89 + 0.20 + 0.20 + 0.10 = $1.39
。 - 订单3:展示了另一种组合方式,证明装饰器可以装饰任何
Beverage
,包括另一个具体组件。价格:1.99 + 0.10 = $2.09
。
这个完整的示例不仅实现了模式,还展示了现代C++中资源管理(智能指针)、所有权转移(移动语义)和代码组织的最佳实践。
5. 总结
装饰器模式是一种极其强大且灵活的结构型设计模式,是“组合优于继承”这一设计原则的典范体现。它通过将功能分解为一系列装饰器类,并通过递归组合的方式动态地构建对象,完美地解决了使用继承带来的“类爆炸”和灵活性不足的问题。
它的核心价值在于:
- 动态扩展:允许在运行时为对象添加或撤销职责。
- 透明性:装饰后的对象与原始对象接口一致,客户端无需更改代码。
- 避免子类泛滥:用少量装饰器类组合出无限的功能可能性。
在C++中实现它,需要注意:
- **使用智能指针(如
std::unique_ptr
)**来明确管理被装饰对象的所有权,避免内存泄漏。 - 利用移动语义来高效地转移资源所有权,构建装饰链。
- 理解
override
和虚析构函数在多态中的重要性。
装饰器模式并非银弹,它会增加系统的复杂性,引入大量小对象。但在需要弹性、可扩展设计的场景下(如I/O流、GUI组件、中间件管道),它无疑是首选方案。从《设计模式》到现代C++和前端框架,它的思想历久弥新,深刻理解并掌握它,将极大地提升你作为软件工程师的设计和架构能力。