Skip to content

2.4 装饰器模式 (Decorator Pattern)

欢迎来到“核心入门模式”的最后一站!我们将学习一个非常有趣且实用的模式——装饰器模式。

核心定义:在不改变原对象的基础上,动态地为该对象添加新的功能。它像一个包装纸,可以在保持原对象接口不变的情况下,为其添加额外的职责。

一、生活中的场景:DIY 你的星巴克咖啡

想象一下你在星巴克点单:

  1. 核心产品:你首先点了一杯基础的“美式咖啡”。这是你的原始对象
  2. 第一次装饰:你觉得太苦,让店员加一份“奶”。现在这杯饮料变成了“美式 + 奶”。“奶”就是一个装饰器,它为咖啡增加了新的风味(功能)。
  3. 第二次装饰:你还想要甜一点,又加了一份“糖浆”。现在它变成了“美式 + 奶 + 糖浆”。“糖浆”是第二个装饰器
  4. 第三次装饰:你还想更有嚼劲,又加了一份“珍珠”。现在是“美式 + 奶 + 糖浆 + 珍珠”。

重点是:

  • 核心不变:无论加多少东西,它的核心依然是那杯“美式咖啡”。
  • 动态组合:你可以任意选择加什么、加多少,组合非常灵活。
  • 接口一致:无论怎么加料,它最终还是一杯“饮料”,你可以用同样的方式(用吸管喝)来使用它。

装饰器模式就像这里的“加料”,它允许你一层层地为对象添加新功能。

二、没有装饰器模式的代码:爆炸的子类

假设我们有一个基础的 Button 类,它只有一个 onClick 方法。

javascript
class Button {
  onClick() {
    console.log("按钮被点击了");
  }
}

现在,需求来了:

  1. 我们需要一个“点击时会上报日志”的按钮。
  2. 我们还需要一个“点击时会弹出二次确认框”的按钮。
  3. 我们甚至需要一个“点击时既要上报日志,又要弹窗确认”的按钮!

如果用继承来实现,会怎么样?

javascript
// 方案一:继承
class ButtonWithLog extends Button {
  onClick() {
    console.log("---上报日志---");
    super.onClick();
  }
}

class ButtonWithConfirm extends Button {
  onClick() {
    if (window.confirm("确定要点击吗?")) {
      super.onClick();
    }
  }
}

// 问题来了:既要日志又要确认怎么办?再写一个类?
class ButtonWithLogAndConfirm extends Button {
  onClick() {
    console.log("---上报日志---");
    if (window.confirm("确定要点击吗?")) {
      super.onClick();
    }
  }
}

const btn1 = new ButtonWithLogAndConfirm();
btn1.onClick();

// 如果再来一个“点击防抖”的需求呢?类的数量会呈指数级爆炸!
// ButtonWithLogAndDebounce, ButtonWithConfirmAndDebounce, ButtonWithLogAndConfirmAndDebounce...

使用继承的方案显然非常僵化和笨重,会导致创建大量功能重复的子类,难以维护。

三、使用装饰器模式重构

装饰器模式的核心是“包装”。在 JavaScript 中,我们可以非常优雅地利用高阶函数来实现这一点。装饰器就是一个函数,它接收一个旧的函数,返回一个包装了新功能的函数。

javascript
// =================== 装饰器 ===================
// 1. 日志装饰器
function withLog(target) {
  // 保存原始的 onClick 方法
  const oldOnClick = target.onClick;

  // 重写 onClick 方法
  target.onClick = function () {
    console.log("---上报日志---");
    // 调用原始的 onClick 方法,并保持 this 指向
    oldOnClick.apply(this, arguments);
  };
  return target;
}

// 2. 确认框装饰器
function withConfirm(target) {
  const oldOnClick = target.onClick;
  target.onClick = function () {
    if (window.confirm("确定要点击吗?")) {
      oldOnClick.apply(this, arguments);
    }
  };
  return target;
}

// =================== 使用 ===================
// 原始对象
class Button {
  onClick() {
    console.log("按钮被点击了");
  }
}

let button = new Button();

// 动态地为 button 实例添加功能
// 像穿衣服一样,一层层包装
button = withConfirm(button);
button = withLog(button); // 后装饰的会先执行

// 现在,这个 button 实例已经同时拥有了日志和确认功能
button.onClick();
// 当点击时:
// 1. 弹出确认框
// 2. 如果点“确定”,则会在控制台打印:
//    ---上报日志---
//    按钮被点击了

看,我们没有创建任何子类!我们只是创建了两个可复用的“装饰”函数,然后像搭积木一样,按需将它们应用到原始对象上。这种方式灵活、可组合、易于维护。

深入一点:AOP 面向切面编程

装饰器模式是 AOP (Aspect Oriented Programming) 面向切面编程 思想的一种完美体现。

  • 切面 (Aspect):像日志、确认框、性能统计、事务处理这些分散在各个业务模块中的通用功能,就是“切面”。
  • 切点 (Pointcut):我们希望将这些通用功能插入到哪个核心业务逻辑中,那个点就是“切点”(比如我们的 onClick 方法)。

装饰器允许我们在不修改核心业务逻辑 (onClick 内部代码) 的情况下,在它的“之前” (before) 或“之后” (after) 动态地插入我们的“切面”代码。

现代 JavaScript 中的装饰器

这种强大的编程思想在现代 JavaScript 中已经有了语言层面的支持,即 ES7 Decorator。它使用 @ 语法糖,让代码更简洁、更具声明性。

javascript
// 这是一个非常简化的示例,旨在展示语法
// 需要 Babel 或 TypeScript 等编译器支持

function log(target, name, descriptor) {
  // ... 实现日志逻辑
}

class MyClass {
  @log
  myMethod() {
    // ...
  }
}

当你看到 @ 符号时,就可以理解为它正在用一个函数来“装饰”它下面的类或方法。

四、优缺点与适用场景

优点

  1. 高度灵活:可以动态地、按需地为对象添加功能,也可以移除。
  2. 避免类爆炸:相比继承,可以避免创建大量功能琐碎的子类。
  3. 职责分离:每个装饰器只关心自己的功能,符合单一职责原则。

缺点

  1. 调试困难:由于存在多层包装,调试时可能会遇到多层嵌套的调用栈。
  2. 增加复杂性:会产生许多功能单一的小对象(函数),如果滥用,可能会让代码结构变得零散。

适用场景

  • 当你需要为一个对象动态添加功能,并且这些功能可以自由组合时。
  • 当使用继承会导致子类数量失控时。
  • 当你想为一组兄弟类添加一个统一的功能,但又不想修改它们的父类时。

核心入门模式小结

恭喜你!至此,我们已经学习完了四个最核心、最基础的设计模式:

  • 工厂模式:解决了创建对象的问题,将创建与使用解耦。
  • 单例模式:解决了实例数量的问题,保证全局唯一。
  • 观察者模式:解决了对象通信的问题,实现了一对多的松耦合联动。
  • 装饰器模式:解决了动态添加功能的问题,提供了比继承更灵活的扩展方式。

这四个模式分别代表了创建型、行为型、结构型中的典型思想。掌握了它们,你就已经具备了进入更广阔的设计模式世界的基础。