设计模式入门:3. 装饰器模式详解 C++实现
装饰器模式详解:动态给对象"穿衣服",C++完整实现
引言
想象一下你在咖啡店点咖啡:你可以点一杯基础的美式咖啡,也可以选择加奶、加糖、加摩卡、加焦糖… 每加一种配料,咖啡的价格和描述都会发生变化。如果用传统的继承方式来实现,你需要为每一种组合都创建一个类:CoffeeWithMilk、CoffeeWithSugar、CoffeeWithMilkAndSugar、CoffeeWithMochaAndMilk… 很快就会出现"类爆炸"问题。
装饰器模式(Decorator Pattern)正是为了解决这个问题而生的。它是一种结构型设计模式,允许你在运行时动态地给一个对象添加额外的职责,而不需要修改原有对象的代码,也不需要通过继承来扩展功能。
今天我们就用C++语言,从基础概念到完整实现,彻底搞懂装饰器模式。
一、装饰器模式的核心概念
1.1 解决的痛点
在软件开发中,我们经常需要给对象添加新的功能。传统的做法是使用继承:创建一个子类,在子类中添加新的方法或重写父类的方法。但这种方式有几个明显的缺点:
- 类爆炸:每添加一个新功能就需要创建一个新的子类,功能组合越多,类的数量就会呈指数级增长
- 静态继承:继承是静态的,在编译时就确定了,无法在运行时动态改变对象的行为
- 继承层次过深:多层继承会导致代码难以理解和维护
- 违反单一职责原则:一个子类可能包含多个不相关的功能
装饰器模式采用**“组合优于继承”**的设计原则,通过包装对象的方式来动态添加功能,完美解决了这些问题。
1.2 核心思想
装饰器模式的核心思想是:创建一个装饰器类,它包装了原始对象,并且与原始对象实现了相同的接口。这样,客户端可以透明地使用装饰后的对象,就像使用原始对象一样。装饰器可以在调用原始对象的方法前后添加自己的逻辑,从而实现功能的扩展。
你可以把装饰器想象成手机壳:它不会改变手机本身的功能,但可以给手机添加保护、美观、支架等额外功能。你可以给手机套上多个手机壳,每个手机壳都添加不同的功能,而且可以随时取下或更换。
1.3 四个核心角色
装饰器模式包含四个关键角色:
- 抽象组件(Component):定义了对象的通用接口,是具体组件和抽象装饰器共同的父类
- 具体组件(Concrete Component):被装饰的原始对象,实现了抽象组件接口
- 抽象装饰器(Decorator):继承自抽象组件,持有一个抽象组件的引用,用于包装具体组件或其他装饰器
- 具体装饰器(Concrete Decorator):实现了具体的扩展功能,在调用原始对象方法的前后添加自己的逻辑
二、标准装饰器模式实现
2.1 UML类图
+----------------+ | Component | <-- 抽象组件 +----------------+ | + operation() | +----------------+ ^ ^ / \ / \ +----------------+ +----------------+ | ConcreteComp | | Decorator | <-- 抽象装饰器 +----------------+ +----------------+ | + operation() | | - component: | +----------------+ | Component* | +----------------+ ^ | +----------------+ | ConcreteDecor | <-- 具体装饰器 +----------------+ | + operation() | +----------------+2.2 C++实现(咖啡例子)
我们就用开头提到的咖啡例子来实现装饰器模式。我们有基础的简单咖啡,可以动态添加奶、糖、摩卡等配料。
#include<iostream>#include<string>#include<memory>// 现代C++智能指针// 抽象组件:咖啡classCoffee{public:virtual~Coffee()=default;virtualstd::stringgetDescription()const=0;// 获取咖啡描述virtualdoublecost()const=0;// 获取咖啡价格};// 具体组件:简单咖啡(被装饰的原始对象)classSimpleCoffee:publicCoffee{public:std::stringgetDescription()constoverride{return"简单咖啡";}doublecost()constoverride{return10.0;// 基础价格10元}};// 抽象装饰器:咖啡装饰器classCoffeeDecorator:publicCoffee{protected:std::unique_ptr<Coffee>coffee_;// 持有被装饰的咖啡对象public:// 构造函数接收一个咖啡对象explicitCoffeeDecorator(std::unique_ptr<Coffee>coffee):coffee_(std::move(coffee)){}};// 具体装饰器:加奶classMilkDecorator:publicCoffeeDecorator{public:usingCoffeeDecorator::CoffeeDecorator;// 继承构造函数std::stringgetDescription()constoverride{returncoffee_->getDescription()+" + 牛奶";}doublecost()constoverride{returncoffee_->cost()+2.0;// 加奶加2元}};// 具体装饰器:加糖classSugarDecorator:publicCoffeeDecorator{public:usingCoffeeDecorator::CoffeeDecorator;std::stringgetDescription()constoverride{returncoffee_->getDescription()+" + 糖";}doublecost()constoverride{returncoffee_->cost()+1.0;// 加糖加1元}};// 具体装饰器:加摩卡classMochaDecorator:publicCoffeeDecorator{public:usingCoffeeDecorator::CoffeeDecorator;std::stringgetDescription()constoverride{returncoffee_->getDescription()+" + 摩卡";}doublecost()constoverride{returncoffee_->cost()+5.0;// 加摩卡加5元}};// 客户端代码intmain(){// 1. 简单咖啡std::unique_ptr<Coffee>coffee1=std::make_unique<SimpleCoffee>();std::cout<<"咖啡1: "<<coffee1->getDescription()<<",价格: "<<coffee1->cost()<<"元"<<std::endl;// 2. 加奶咖啡std::unique_ptr<Coffee>coffee2=std::make_unique<MilkDecorator>(std::make_unique<SimpleCoffee>());std::cout<<"咖啡2: "<<coffee2->getDescription()<<",价格: "<<coffee2->cost()<<"元"<<std::endl;// 3. 加奶加糖咖啡(嵌套装饰)std::unique_ptr<Coffee>coffee3=std::make_unique<SugarDecorator>(std::make_unique<MilkDecorator>(std::make_unique<SimpleCoffee>()));std::cout<<"咖啡3: "<<coffee3->getDescription()<<",价格: "<<coffee3->cost()<<"元"<<std::endl;// 4. 豪华咖啡:摩卡+奶+糖(任意组合)std::unique_ptr<Coffee>coffee4=std::make_unique<MochaDecorator>(std::make_unique<MilkDecorator>(std::make_unique<SugarDecorator>(std::make_unique<SimpleCoffee>())));std::cout<<"咖啡4: "<<coffee4->getDescription()<<",价格: "<<coffee4->cost()<<"元"<<std::endl;return0;}2.3 运行结果
咖啡1: 简单咖啡,价格: 10元 咖啡2: 简单咖啡 + 牛奶,价格: 12元 咖啡3: 简单咖啡 + 牛奶 + 糖,价格: 13元 咖啡4: 简单咖啡 + 糖 + 牛奶 + 摩卡,价格: 18元2.4 代码解析
- 抽象组件
Coffee:定义了所有咖啡都必须实现的两个方法:getDescription()和cost() - 具体组件
SimpleCoffee:最基础的咖啡,实现了抽象组件的接口 - 抽象装饰器
CoffeeDecorator:继承自Coffee,并且持有一个Coffee类型的智能指针。它的作用是统一所有装饰器的接口,使得装饰器可以嵌套使用 - 具体装饰器:
MilkDecorator、SugarDecorator、MochaDecorator,每个都只负责添加一种配料。它们重写了getDescription()和cost()方法,在原有咖啡的基础上添加自己的描述和价格
最关键的是嵌套装饰的能力:一个装饰器可以包装另一个装饰器,形成一个装饰链。这样我们就可以任意组合不同的配料,而不需要创建新的类。
三、装饰器模式的优缺点
3.1 优点
- 动态添加功能:可以在运行时给对象添加任意数量的功能,比继承灵活得多
- 避免类爆炸:不需要为每一种功能组合创建一个类,大大减少了类的数量
- 符合开闭原则:添加新功能时,只需要创建一个新的具体装饰器类,不需要修改现有代码
- 符合单一职责原则:每个装饰器只负责添加一个功能,职责清晰
- 可以多次装饰:同一个对象可以被多个装饰器多次装饰,实现功能的叠加
- 客户端透明:客户端不需要知道对象是否被装饰过,使用方式完全相同
3.2 缺点
- 产生大量小类:每个具体装饰器都是一个独立的类,会导致系统中出现大量的小类
- 多层装饰比较复杂:如果装饰层数过多,代码的可读性和调试难度会增加
- 容易出现重复装饰:如果不小心,可能会给同一个对象添加多个相同的装饰器
- 无法删除装饰:标准的装饰器模式不支持在运行时删除已经添加的装饰器
四、适用场景
装饰器模式特别适合以下场景:
- 需要动态给对象添加功能,并且这些功能可以动态撤销
- 需要给一个类的多个实例添加不同的功能组合
- 不能使用继承的情况:
- 类被
final修饰(C++11及以后),无法被继承 - 继承层次太深,导致代码难以维护
- 继承会导致子类数量爆炸
- 类被
- 需要在不影响其他对象的情况下,给单个对象添加功能
- 当采用继承扩展功能不切实际时
经典应用案例:
- Java的IO流体系(
FileInputStream→BufferedInputStream→DataInputStream) - C++ STL中的
std::stack和std::queue(本质上是对std::deque的装饰) - GUI组件的装饰(给按钮添加边框、阴影、动画等)
- 日志系统的装饰(给日志添加时间戳、线程ID、级别等信息)
五、与其他模式的对比
很多人容易把装饰器模式和其他结构型模式混淆,这里做一个清晰的对比:
| 模式 | 核心目的 | 与装饰器的区别 |
|---|---|---|
| 装饰器模式 | 动态给对象添加额外功能 | 不改变接口,增强功能,支持嵌套 |
| 适配器模式 | 转换接口,让不兼容的类一起工作 | 改变接口,不改变功能 |
| 代理模式 | 控制对对象的访问 | 不改变接口,控制访问,通常只包装一层 |
| 桥接模式 | 将抽象与实现分离,使它们可以独立变化 | 分离两个独立变化的维度,而不是动态添加功能 |
| 组合模式 | 将对象组合成树形结构以表示"部分-整体"层次 | 处理对象的组合关系,而不是添加功能 |
六、现代C++改进与变种
6.1 使用模板简化装饰器
如果我们有多个不同的抽象组件,每个都需要写一个抽象装饰器类,会比较繁琐。使用C++模板可以简化这个过程:
// 通用模板装饰器template<typenameComponent>classTemplateDecorator:publicComponent{protected:std::unique_ptr<Component>component_;public:explicitTemplateDecorator(std::unique_ptr<Component>component):component_(std::move(component)){}};// 使用模板装饰器定义具体装饰器classMilkDecorator:publicTemplateDecorator<Coffee>{public:usingTemplateDecorator::TemplateDecorator;std::stringgetDescription()constoverride{returncomponent_->getDescription()+" + 牛奶";}doublecost()constoverride{returncomponent_->cost()+2.0;}};6.2 函数式装饰器(C++11及以后)
对于只有一个方法的接口,我们可以使用std::function和Lambda表达式来实现更简洁的函数式装饰器:
#include<functional>// 定义咖啡函数类型usingCoffeeFunction=std::function<std::pair<std::string,double>()>;// 基础咖啡函数CoffeeFunctionsimpleCoffee(){return[](){returnstd::make_pair("简单咖啡",10.0);};}// 加奶装饰器函数CoffeeFunctionwithMilk(CoffeeFunction coffee){return[coffee](){auto[desc,cost]=coffee();returnstd::make_pair(desc+" + 牛奶",cost+2.0);};}// 加糖装饰器函数CoffeeFunctionwithSugar(CoffeeFunction coffee){return[coffee](){auto[desc,cost]=coffee();returnstd::make_pair(desc+" + 糖",cost+1.0);};}// 客户端代码intmain(){autocoffee=withMilk(withSugar(simpleCoffee()));auto[desc,cost]=coffee();std::cout<<"函数式装饰器: "<<desc<<",价格: "<<cost<<"元"<<std::endl;return0;}这种方式非常简洁,不需要定义任何类,适合简单的装饰场景。
6.3 可移除装饰器
标准的装饰器模式不支持在运行时移除装饰器。如果需要这个功能,可以在抽象装饰器中添加一个getComponent()方法,让客户端可以访问被包装的对象:
classCoffeeDecorator:publicCoffee{protected:std::unique_ptr<Coffee>coffee_;public:explicitCoffeeDecorator(std::unique_ptr<Coffee>coffee):coffee_(std::move(coffee)){}// 获取被包装的对象std::unique_ptr<Coffee>getComponent(){returnstd::move(coffee_);}};七、总结
装饰器模式是一种非常优雅的设计模式,它完美体现了**“组合优于继承”**的设计原则。通过动态包装对象的方式,我们可以在不修改原有代码的情况下,灵活地给对象添加任意数量的功能组合。
在实际开发中,当你遇到以下情况时,应该考虑使用装饰器模式:
- 需要给对象添加多个可以任意组合的功能
- 继承会导致类爆炸或代码难以维护
- 需要在运行时动态改变对象的行为
记住,设计模式不是银弹。装饰器模式虽然强大,但也不能滥用。如果功能组合很少且固定,使用继承可能会更简单。只有当你确实需要动态、灵活地扩展对象功能时,装饰器模式才是最佳选择。
