设计模式入门: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实现咖啡例子我们就用开头提到的咖啡例子来实现装饰器模式。我们有基础的简单咖啡可以动态添加奶、糖、摩卡等配料。#includeiostream#includestring#includememory// 现代C智能指针// 抽象组件咖啡classCoffee{public:virtual~Coffee()default;virtualstd::stringgetDescription()const0;// 获取咖啡描述virtualdoublecost()const0;// 获取咖啡价格};// 具体组件简单咖啡被装饰的原始对象classSimpleCoffee:publicCoffee{public:std::stringgetDescription()constoverride{return简单咖啡;}doublecost()constoverride{return10.0;// 基础价格10元}};// 抽象装饰器咖啡装饰器classCoffeeDecorator:publicCoffee{protected:std::unique_ptrCoffeecoffee_;// 持有被装饰的咖啡对象public:// 构造函数接收一个咖啡对象explicitCoffeeDecorator(std::unique_ptrCoffeecoffee):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_ptrCoffeecoffee1std::make_uniqueSimpleCoffee();std::cout咖啡1: coffee1-getDescription()价格: coffee1-cost()元std::endl;// 2. 加奶咖啡std::unique_ptrCoffeecoffee2std::make_uniqueMilkDecorator(std::make_uniqueSimpleCoffee());std::cout咖啡2: coffee2-getDescription()价格: coffee2-cost()元std::endl;// 3. 加奶加糖咖啡嵌套装饰std::unique_ptrCoffeecoffee3std::make_uniqueSugarDecorator(std::make_uniqueMilkDecorator(std::make_uniqueSimpleCoffee()));std::cout咖啡3: coffee3-getDescription()价格: coffee3-cost()元std::endl;// 4. 豪华咖啡摩卡奶糖任意组合std::unique_ptrCoffeecoffee4std::make_uniqueMochaDecorator(std::make_uniqueMilkDecorator(std::make_uniqueSugarDecorator(std::make_uniqueSimpleCoffee())));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修饰C11及以后无法被继承继承层次太深导致代码难以维护继承会导致子类数量爆炸需要在不影响其他对象的情况下给单个对象添加功能当采用继承扩展功能不切实际时经典应用案例Java的IO流体系FileInputStream→BufferedInputStream→DataInputStreamC STL中的std::stack和std::queue本质上是对std::deque的装饰GUI组件的装饰给按钮添加边框、阴影、动画等日志系统的装饰给日志添加时间戳、线程ID、级别等信息五、与其他模式的对比很多人容易把装饰器模式和其他结构型模式混淆这里做一个清晰的对比模式核心目的与装饰器的区别装饰器模式动态给对象添加额外功能不改变接口增强功能支持嵌套适配器模式转换接口让不兼容的类一起工作改变接口不改变功能代理模式控制对对象的访问不改变接口控制访问通常只包装一层桥接模式将抽象与实现分离使它们可以独立变化分离两个独立变化的维度而不是动态添加功能组合模式将对象组合成树形结构以表示部分-整体层次处理对象的组合关系而不是添加功能六、现代C改进与变种6.1 使用模板简化装饰器如果我们有多个不同的抽象组件每个都需要写一个抽象装饰器类会比较繁琐。使用C模板可以简化这个过程// 通用模板装饰器templatetypenameComponentclassTemplateDecorator:publicComponent{protected:std::unique_ptrComponentcomponent_;public:explicitTemplateDecorator(std::unique_ptrComponentcomponent):component_(std::move(component)){}};// 使用模板装饰器定义具体装饰器classMilkDecorator:publicTemplateDecoratorCoffee{public:usingTemplateDecorator::TemplateDecorator;std::stringgetDescription()constoverride{returncomponent_-getDescription() 牛奶;}doublecost()constoverride{returncomponent_-cost()2.0;}};6.2 函数式装饰器C11及以后对于只有一个方法的接口我们可以使用std::function和Lambda表达式来实现更简洁的函数式装饰器#includefunctional// 定义咖啡函数类型usingCoffeeFunctionstd::functionstd::pairstd::string,double();// 基础咖啡函数CoffeeFunctionsimpleCoffee(){return[](){returnstd::make_pair(简单咖啡,10.0);};}// 加奶装饰器函数CoffeeFunctionwithMilk(CoffeeFunction coffee){return[coffee](){auto[desc,cost]coffee();returnstd::make_pair(desc 牛奶,cost2.0);};}// 加糖装饰器函数CoffeeFunctionwithSugar(CoffeeFunction coffee){return[coffee](){auto[desc,cost]coffee();returnstd::make_pair(desc 糖,cost1.0);};}// 客户端代码intmain(){autocoffeewithMilk(withSugar(simpleCoffee()));auto[desc,cost]coffee();std::cout函数式装饰器: desc价格: cost元std::endl;return0;}这种方式非常简洁不需要定义任何类适合简单的装饰场景。6.3 可移除装饰器标准的装饰器模式不支持在运行时移除装饰器。如果需要这个功能可以在抽象装饰器中添加一个getComponent()方法让客户端可以访问被包装的对象classCoffeeDecorator:publicCoffee{protected:std::unique_ptrCoffeecoffee_;public:explicitCoffeeDecorator(std::unique_ptrCoffeecoffee):coffee_(std::move(coffee)){}// 获取被包装的对象std::unique_ptrCoffeegetComponent(){returnstd::move(coffee_);}};七、总结装饰器模式是一种非常优雅的设计模式它完美体现了**“组合优于继承”**的设计原则。通过动态包装对象的方式我们可以在不修改原有代码的情况下灵活地给对象添加任意数量的功能组合。在实际开发中当你遇到以下情况时应该考虑使用装饰器模式需要给对象添加多个可以任意组合的功能继承会导致类爆炸或代码难以维护需要在运行时动态改变对象的行为记住设计模式不是银弹。装饰器模式虽然强大但也不能滥用。如果功能组合很少且固定使用继承可能会更简单。只有当你确实需要动态、灵活地扩展对象功能时装饰器模式才是最佳选择。