在最近一次 Code Review 中,我发现同事的代码里“继承满天飞”。为了实现一个核心类的功能扩展,他竟然创建了十几个子类。看着那庞大的类继承树,像族谱一样,手里的咖啡瞬间失去了香味——这正是过度使用继承导致的“类爆炸”问题。

许多初学者乃至资深开发者,在遇到“如何在不修改原有类代码的前提下动态扩展对象功能”这类问题时,第一时间想到的解决方案通常是继承。
然而,继承是一把双刃剑。当需要将多种功能进行排列组合时,继承机制极易导致“类爆炸”。
今天,让我们抛开枯燥的理论,以一杯奶茶为例,由浅入深地彻底掌握 PHP 装饰器模式(Decorator Pattern)的原理与实战用法。
1. 痛点:当需求开始“套娃”
假设我们正在开发一套奶茶店收银系统。
最初的需求非常单纯:只出售原味奶茶,每杯定价 10 元。
你可能会定义一个 MilkTea 类,其中包含一个 cost() 方法,直接返回 10。
然而,老板的需求总是来得猝不及防:
- 加珍珠 +2 元
- 加椰果 +3 元
- 加布丁 +4 元
- 加糖?去冰?…
如果采用继承方式,你可能会写出这样的子类:
PearlMilkTea (珍珠奶茶)
CoconutMilkTea (椰果奶茶)
PuddingMilkTea (布丁奶茶)
…
此时,顾客下单:“我要一杯加珍珠、加椰果、半糖、去冰的奶茶”。
这下糟了,难道要创建一个 PearlAndCoconutAndHalfSugarAndNoIceMilkTea 类吗?
如果配料有 10 种,排列组合出来的子类数量将是天文数字——这就是典型的“类爆炸”。
2. 破局之道:像“洋葱”一样层层包裹
装饰器模式的核心思想正是“组合优于继承”。
换个思路:不必为每种搭配创建新奶茶,而是将基础奶茶作为核心,通过动态组合来扩展功能。
- 加珍珠?只需在奶茶外层包裹一层“珍珠装饰器”。
- 再加椰果?就在刚才的整体外面再包一层“椰果装饰器”。
这样一层层包裹,如同俄罗斯套娃或洋葱结构。每层装饰器只负责增加自己的价格,并将剩余任务委托给内层对象。
3. 实战演练:用 PHP 逐步实现装饰器模式
Talk is cheap, show me the code. 我们使用 PHP 8 的语法优雅地实现它。
第一步:定义统一接口 (Contract)
无论是基础奶茶还是添加配料的扩展版本,本质上都属于“饮品”。因此需要定义一个统一的接口来规范行为。
第二步:实现原始对象 (Concrete Component)
这是最基础的组件——一杯纯粹的原味奶茶。
class SimpleMilkTea implements Beverage
{
public function getDescription(): string
{
return "原味奶茶";
}
public function cost(): int
{
return 10; // 基础价 10 元
}
}
第三步:构建装饰器基类 (Base Decorator)
这是装饰器模式的精髓。装饰器本身也实现了 Beverage 接口,但内部持有另一个 Beverage 对象。
注意:这里使用 abstract 关键字,子类只需实现具体的加料逻辑即可。
abstract class BeverageDecorator implements Beverage
{
// PHP 8 构造函数属性提升,直接注入被装饰的对象
public function __construct(protected Beverage $beverage) { }
// 默认行为:直接调用里面那层的方法
public function getDescription(): string
{
return $this->beverage->getDescription();
}
public function cost(): int
{
return $this->beverage->cost();
}
}
第四步:编写具体的装饰器 (Concrete Decorators)
现在,想加什么配料,就创建对应的装饰器类,即写即用,互不干扰。
加珍珠(Pearl):
class PearlDecorator extends BeverageDecorator
{
public function getDescription(): string
{
// 先获取里面的描述,再追加自己的描述
return $this->beverage->getDescription() . " + 珍珠";
}
public function cost(): int
{
// 核心价格 + 珍珠的价格(2元)
return $this->beverage->cost() + 2;
}
}
加布丁(Pudding):
class PuddingDecorator extends BeverageDecorator
{
public function getDescription(): string
{
return $this->beverage->getDescription() . " + 布丁";
}
public function cost(): int
{
return $this->beverage->cost() + 4; // 布丁贵一点
}
}
4. 效果演示:见证组合的威力
代码编写完毕,接下来看看在业务逻辑中如何灵活运用。你会发现装饰器的组合方式极其灵活。
// 1. 点一杯原味奶茶
$myDrink = new SimpleMilkTea();
echo "刚开始: " . $myDrink->getDescription() . " 价格:" . $myDrink->cost() . "";
// 2. 顾客说要加珍珠
// 把原味奶茶塞进珍珠装饰器里
$myDrink = new PearlDecorator($myDrink);
// 3. 顾客又说要加布丁
// 把刚才加了珍珠的奶茶,再塞进布丁装饰器里
$myDrink = new PuddingDecorator($myDrink);
echo "最终成品: " . $myDrink->getDescription() . "";
echo "最终价格: " . $myDrink->cost() . " 元";
运行结果如下:
刚开始: 原味奶茶 价格:10
最终成品: 原味奶茶 + 珍珠 + 布丁
最终价格: 16 元
看到了吗?你可以无限嵌套 new Decorator(new Decorator(...)),完全不需要修改 SimpleMilkTea 的代码,也无需构建复杂的继承层级。
5. 何时应该使用装饰器模式?
切勿“手里拿着锤子,看什么都是钉子”。以下场景强烈推荐使用装饰器模式:
- 动态扩展功能:需要在运行时为对象动态添加额外职责,例如给文本添加加粗效果、为 HTTP 请求注入 Token 等。
- 规避类爆炸:当类的变体过多或功能需要多种排列组合时,装饰器可避免子类疯狂增长。
- 践行开闭原则 (OCP):对扩展开放,对修改关闭。
在著名的 PHP 框架 Laravel 中,中间件 (Middleware) 的实现机制本质上就是装饰器模式的一种变体(洋葱模型),请求逐层穿过中间件,最后到达控制器。
恭喜你!现在你不仅掌握了装饰器模式,还理解了中间件的底层原理。
