在上一篇文章中,我们已经深入探讨了装饰器模式的理论基础,并分析了如何利用它来解决那些在传统继承设计中显得棘手的扩展性问题。今天,我们将继续这段旅程,把理论转化为实践。我们将通过具体的代码实现,一步步拆解装饰器模式是如何在不修改原有代码的基础上,灵活地为对象添加新功能的。
如果你还没有尝试过自己实现这个模式,强烈建议你在阅读本文之前,先在脑海中构思一下代码结构。这不仅能帮助你更好地理解设计模式的精髓,还能让你在实际编码时更有“体感”。
回顾设计蓝图
在深入代码之前,让我们先快速回顾一下之前的设计类图。这个蓝图是我们今天编码的导航地图。如果你看过图,你会发现整个结构非常清晰:
- 抽象组件:这是所有类的核心接口,定义了我们最终需要的对象行为(比如 Pizza)。
- 具体组件:这是我们实际要被装饰的基础对象(比如各种口味的 Pizza)。
- 抽象装饰器:它持有一个组件对象的引用,并实现了与组件相同的接口。这是装饰器的关键。
- 具体装饰器:它们负责给组件添加具体的“配料”或功能。
Java 实战:构建你的披萨订购系统
让我们用一个经典的“披萨订购系统”作为实战案例。在这个场景中,我们需要计算各种披萨及其配料的价格。如果不使用装饰器模式,面对“芝士饼加双倍培根”这种需求,我们可能需要为每种组合都创建一个子类,这简直是维护噩梦。
#### 1. 定义基础抽象类
首先,我们需要定义 Pizza 这个抽象基类。它是我们所有口味的父类。
// 抽象 Pizza 类(所有具体披萨的基类)
abstract class Pizza {
// 描述信息,默认为未知
String description = "Unknown Pizza";
// 获取描述
public String getDescription() {
return description;
}
// 这是一个抽象方法,子类必须实现它来定义自己的价格
public abstract int getCost();
}
#### 2. 实现具体的披萨口味
现在,我们来做几个具体的披萨。它们非常简单,只需要定义自己的名字和价格即可。
// 具体披萨类:PeppyPaneer (芝士饼)
class PeppyPaneer extends Pizza {
public PeppyPaneer() { description = "PeppyPaneer"; }
public int getCost() { return 100; }
}
// 具体披萨类:FarmHouse (农家小屋)
class FarmHouse extends Pizza {
public FarmHouse() { description = "FarmHouse"; }
public int getCost() { return 200; }
}
// 具体披萨类:Margherita (玛格丽特)
class Margherita extends Pizza {
public Margherita() { description = "Margherita"; }
public int getCost() { return 100; }
}
// 具体披萨类:ChickenFiesta (鸡肉狂欢)
class ChickenFiesta extends Pizza {
public ChickenFiesta() { description = "ChickenFiesta"; }
public int getCost() { return 200; }
}
// 简单的基础披萨
class SimplePizza extends Pizza {
public SimplePizza() { description = "SimplePizza"; }
public int getCost() { return 50; }
}
#### 3. 构建装饰器的核心
这里是魔法发生的地方。INLINECODE72832f36 继承了 INLINECODE18da1244。你可能会有疑问:为什么装饰器要继承披萨?这正是装饰器模式的精髓所在——“is-a”关系。既然装饰器本质上还是披萨(只是加了料的披萨),它们就可以互相嵌套。
// 装饰器抽象类:它继承 Pizza 以便与其互换使用
// 这里的 abstract 确保了我们不会直接实例化这个基类
abstract class ToppingsDecorator extends Pizza {
// 我们强制要求所有配料装饰器都必须重写描述方法
// 这样我们就能拼接出完整的配料清单
public abstract String getDescription();
}
#### 4. 编写具体的配料装饰器
让我们看看具体的配料是如何实现的。注意看构造函数:它们接收一个 Pizza 对象。这意味着我们可以在运行时动态地组合这些对象,而不需要在编译期确定。
// 具体配料:新鲜番茄
class FreshTomato extends ToppingsDecorator {
// 我们需要一个引用指向我们要装饰的那个披萨对象
Pizza pizza;
public FreshTomato(Pizza pizza) { this.pizza = pizza; }
public String getDescription() {
// 关键点:调用被装饰对象的描述,并追加自己的名字
return pizza.getDescription() + ", Fresh Tomato ";
}
public int getCost() {
// 关键点:累加价格
return 40 + pizza.getCost();
}
}
// 具体配料:Barbeque (烧烤酱)
class Barbeque extends ToppingsDecorator {
Pizza pizza;
public Barbeque(Pizza pizza) { this.pizza = pizza; }
public String getDescription() {
return pizza.getDescription() + ", Barbeque ";
}
public int getCost() {
return 90 + pizza.getCost();
}
}
// 具体配料:Paneer (印度奶酪)
class Paneer extends ToppingsDecorator {
Pizza pizza;
public Paneer(Pizza pizza) { this.pizza = pizza; }
public String getDescription() {
return pizza.getDescription() + ", Paneer ";
}
public int getCost() {
return 70 + pizza.getCost();
}
}
#### 5. 驱动代码:见证组合的力量
最后,让我们来看看如何使用这些类。注意 pizza2 的创建过程,这是装饰器模式最迷人的地方。
class PizzaStore {
public static void main(String args[]) {
// 场景1:点一个普通的玛格丽特披萨
Pizza pizza = new Margherita();
printBill(pizza);
// 场景2:创建一个农家小屋披萨,并加料
Pizza pizza2 = new FarmHouse();
// 先加番茄
pizza2 = new FreshTomato(pizza2);
// 再加奶酪
pizza2 = new Paneer(pizza2);
printBill(pizza2);
// 场景3:极端情况测试(需要注意空指针)
// Pizza pizza3 = new Barbeque(null);
// System.out.println(pizza3.getDescription() + " Cost :" + pizza3.getCost());
}
// 一个简单的辅助方法来打印账单,展示多态性
private static void printBill(Pizza p) {
System.out.println(p.getDescription() + " Cost :" + p.getCost());
}
}
输出结果:
Margherita Cost :100
FarmHouse, Fresh Tomato , Paneer Cost :310
深入解析:为什么这样做更好?
你可能会想:“看起来只是把价格加起来而已,真的有必要这么麻烦吗?”
- 开闭原则:这是软件开发的圣杯。在这个设计中,如果我们想增加一种新的配料(比如 INLINECODEafcfa4e9,墨西哥辣椒),我们只需要写一个新的 INLINECODEe47e16c5 类。我们不需要去修改现有的 INLINECODE4b6daa65 类,也不需要修改 INLINECODE1a4442a1 类。这意味着我们引入新 Bug 的风险大大降低了。
- 动态组合:你可以想象一下,在运行时根据用户的输入来决定加什么配料。这种灵活性是静态继承无法提供的。
C++ 实战:在强类型世界中的演绎
为了让你在不同技术栈下都能游刃有余,让我们用 C++ 来实现同样的逻辑。C++ 的指针在这里起到了关键的连接作用。
#include
#include
using namespace std;
// 1. Component 接口
class MilkShake {
public:
virtual string Serve() = 0;
virtual float price() = 0;
// 虚析构函数,防止内存泄漏
virtual ~MilkShake() {}
};
// 2. Concrete Component (基础奶昔)
class BaseMilkShake : public MilkShake {
public:
string Serve() override {
return "MilkShake";
}
float price() override {
return 30;
}
};
// 3. Decorator 抽象类
// 注意:它继承自 MilkShake,但它也包含一个 MilkShake 的指针
class MilkShakeDecorator : public MilkShake {
protected:
MilkShake* m_MilkShake; // 组合关系:有一个奶昔
public:
// 构造函数注入:这里通过构造函数传入被装饰的对象
MilkShakeDecorator(MilkShake* baseMilkShake) : m_MilkShake(baseMilkShake) {}
// 默认实现:直接调用被装饰对象的方法
string Serve() override {
return m_MilkShake->Serve();
}
float price() override {
return m_MilkShake->price();
}
};
// 4. Concrete Decorator (添加芒果配料)
class MangoMilkShake : public MilkShakeDecorator {
public:
MangoMilkShake(MilkShake* base) : MilkShakeDecorator(base) {}
string Serve() override {
// 在原有描述上追加
return m_MilkShake->Serve() + " with Mango";
}
float price() override {
// 在原有价格上追加
return m_MilkShake->price() + 40;
}
};
// 5. Concrete Decorator (添加巧克力配料)
class ChocolateMilkShake : public MilkShakeDecorator {
public:
ChocolateMilkShake(MilkShake* base) : MilkShakeDecorator(base) {}
string Serve() override {
return m_MilkShake->Serve() + " with Chocolate";
}
float price() override {
return m_MilkShake->price() + 60;
}
};
int main() {
// 创建基础奶昔
MilkShake* baseShake = new BaseMilkShake();
cout <Serve() << " Cost: " <price() << endl;
// 装饰过程:加芒果
baseShake = new MangoMilkShake(baseShake);
cout <Serve() << " Cost: " <price() << endl;
// 再次装饰:加巧克力
baseShake = new ChocolateMilkShake(baseShake);
cout <Serve() << " Cost: " <price() << endl;
// 清理内存(在实际项目中应使用智能指针)
delete baseShake; // 这里会链式调用析构函数
return 0;
}
性能优化与最佳实践
虽然装饰器模式很强大,但在高性能系统中,它的递归调用结构可能会带来一点点的性能开销。此外,如果装饰器层级过深(比如套娃套了几十层),调试代码会变得非常困难。因此,我们在实际应用中应当注意:
- 保持接口精简:尽量减少 Component 接口中的方法数量。如果接口方法过多,每一个具体的装饰器都需要去实现这些方法,即使是仅仅转发调用,也会导致代码冗余。
- 使用智能指针:在 C++ 现代开发中,务必使用 INLINECODEfbbe9b45 或 INLINECODEd3b4eae6 来管理被装饰对象的生命周期,避免内存泄漏。
- 考虑空对象:正如我们在 Java 示例中看到的 INLINECODE870ad00d,如果传入空对象,装饰器可能会崩溃。在设计时,可以考虑引入一个 INLINECODE44f5904b 或
EmptyMilkShake类,它返回默认值,从而避免复杂的空值检查逻辑。
总结与展望
在这篇文章中,我们跨越了理论与实现的鸿沟。通过 Java 和 C++ 的双语言实战,你应该已经掌握了装饰器模式的核心:利用组合代替继承,动态地为对象增加职责。
你学会了如何设计一个灵活的系统,使得新功能的添加不再意味着对旧代码的修改。这在保持系统稳定性方面是无价的。
在接下来的学习中,我建议你思考一下,这种模式是否可以用在你当前的项目中?比如处理用户权限的叠加、或者处理数据流的层层加密。设计模式不仅是代码模板,更是一种思维方式。
下一篇文章,我们将探讨另一个与装饰器模式在结构上有些相似,但意图完全不同的设计模式。敬请期待!