Appearance
2.4 装饰器模式 (Decorator Pattern)
欢迎来到“核心入门模式”的最后一站!我们将学习一个非常有趣且实用的模式——装饰器模式。
核心定义:在不改变原对象的基础上,动态地为该对象添加新的功能。它像一个包装纸,可以在保持原对象接口不变的情况下,为其添加额外的职责。
一、生活中的场景:DIY 你的星巴克咖啡
想象一下你在星巴克点单:
- 核心产品:你首先点了一杯基础的“美式咖啡”。这是你的原始对象。
- 第一次装饰:你觉得太苦,让店员加一份“奶”。现在这杯饮料变成了“美式 + 奶”。“奶”就是一个装饰器,它为咖啡增加了新的风味(功能)。
- 第二次装饰:你还想要甜一点,又加了一份“糖浆”。现在它变成了“美式 + 奶 + 糖浆”。“糖浆”是第二个装饰器。
- 第三次装饰:你还想更有嚼劲,又加了一份“珍珠”。现在是“美式 + 奶 + 糖浆 + 珍珠”。
重点是:
- 核心不变:无论加多少东西,它的核心依然是那杯“美式咖啡”。
- 动态组合:你可以任意选择加什么、加多少,组合非常灵活。
- 接口一致:无论怎么加料,它最终还是一杯“饮料”,你可以用同样的方式(用吸管喝)来使用它。
装饰器模式就像这里的“加料”,它允许你一层层地为对象添加新功能。
二、没有装饰器模式的代码:爆炸的子类
假设我们有一个基础的 Button 类,它只有一个 onClick 方法。
javascript
class Button {
onClick() {
console.log("按钮被点击了");
}
}现在,需求来了:
- 我们需要一个“点击时会上报日志”的按钮。
- 我们还需要一个“点击时会弹出二次确认框”的按钮。
- 我们甚至需要一个“点击时既要上报日志,又要弹窗确认”的按钮!
如果用继承来实现,会怎么样?
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() {
// ...
}
}当你看到 @ 符号时,就可以理解为它正在用一个函数来“装饰”它下面的类或方法。
四、优缺点与适用场景
优点
- 高度灵活:可以动态地、按需地为对象添加功能,也可以移除。
- 避免类爆炸:相比继承,可以避免创建大量功能琐碎的子类。
- 职责分离:每个装饰器只关心自己的功能,符合单一职责原则。
缺点
- 调试困难:由于存在多层包装,调试时可能会遇到多层嵌套的调用栈。
- 增加复杂性:会产生许多功能单一的小对象(函数),如果滥用,可能会让代码结构变得零散。
适用场景
- 当你需要为一个对象动态添加功能,并且这些功能可以自由组合时。
- 当使用继承会导致子类数量失控时。
- 当你想为一组兄弟类添加一个统一的功能,但又不想修改它们的父类时。
核心入门模式小结
恭喜你!至此,我们已经学习完了四个最核心、最基础的设计模式:
- 工厂模式:解决了创建对象的问题,将创建与使用解耦。
- 单例模式:解决了实例数量的问题,保证全局唯一。
- 观察者模式:解决了对象通信的问题,实现了一对多的松耦合联动。
- 装饰器模式:解决了动态添加功能的问题,提供了比继承更灵活的扩展方式。
这四个模式分别代表了创建型、行为型、结构型中的典型思想。掌握了它们,你就已经具备了进入更广阔的设计模式世界的基础。