装饰模式:在不改原类的情况下叠加能力
装饰模式(Decorator Pattern)是一种结构型设计模式。它的核心目的,是在不修改原始对象代码的前提下,动态地给对象增加职责或行为。
一句话概括:
当你想给对象一层一层叠加能力,但又不想通过继承制造大量子类时,可以使用装饰模式。
问题从哪里来
假设我们要做一个咖啡点单系统,基础咖啡有:
- 美式咖啡
- 拿铁咖啡
可选加料有:
- 牛奶
- 糖
- 摩卡
- 奶油
如果直接用继承,很容易写出这样的类:
MilkAmericanoSugarAmericanoMilkSugarAmericanoMochaLatteCreamMochaLatte
问题是:加料可以自由组合。每增加一种加料,组合数量都会快速膨胀。
装饰模式的做法是:
- 先抽象出统一组件接口,例如
Coffee。 - 基础对象实现这个接口,例如
Americano、Latte。 - 装饰器也实现同一个接口,并持有一个
Coffee。 - 每个装饰器只负责增加一小段行为,例如加牛奶、加糖、加摩卡。
这样能力可以像套娃一样叠加:
text
MilkDecorator -> SugarDecorator -> Americano
结构
装饰模式通常包含四个角色:
Component:组件接口,定义对象的统一行为。ConcreteComponent:具体组件,也就是原始对象。Decorator:抽象装饰器,持有一个组件对象。ConcreteDecorator:具体装饰器,给组件增加额外行为。
放到咖啡例子里:
Coffee是组件接口。Americano、Latte是具体组件。CoffeeDecorator是抽象装饰器。MilkDecorator、SugarDecorator是具体装饰器。
Java 示例
先定义咖啡接口:
java
public interface Coffee {
String getDescription();
double cost();
}
实现基础咖啡:
java
public class Americano implements Coffee {
@Override
public String getDescription() {
return "美式咖啡";
}
@Override
public double cost() {
return 12.0;
}
}
public class Latte implements Coffee {
@Override
public String getDescription() {
return "拿铁咖啡";
}
@Override
public double cost() {
return 18.0;
}
}
定义抽象装饰器。它同样实现 Coffee,并把默认行为委托给被包装的对象。
java
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
protected CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double cost() {
return coffee.cost();
}
}
实现具体装饰器:
java
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 牛奶";
}
@Override
public double cost() {
return coffee.cost() + 3.0;
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 糖";
}
@Override
public double cost() {
return coffee.cost() + 1.0;
}
}
使用方式:
java
public class Demo {
public static void main(String[] args) {
Coffee coffee = new Americano();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription());
System.out.println(coffee.cost());
}
}
输出结果类似:
text
美式咖啡 + 牛奶 + 糖
16.0
这里的关键是:客户端拿到的始终是 Coffee,不需要关心它到底是基础咖啡,还是被装饰过多次的咖啡。
Java 真实示例
Java 标准库里的 IO 流是装饰模式的经典例子。最底层的 FileInputStream 只负责从文件中读取字节,本身不提供缓冲能力。如果直接频繁读取小块数据,每次都可能触发底层 IO,性能会比较差。
BufferedInputStream 就是在 InputStream 外面加了一层缓冲装饰:
java
try (InputStream input = new BufferedInputStream(
new FileInputStream("data.txt")
)) {
int value = input.read();
}
对应到装饰模式:
InputStream是组件接口。FileInputStream是具体组件,负责真实文件读取。FilterInputStream是抽象装饰器,内部持有另一个InputStream。BufferedInputStream是具体装饰器,在读取能力外面叠加缓冲能力。
调用方拿到的仍然是 InputStream,所以可以继续把它传给任何只依赖 InputStream 的代码。缓冲只是被包装进去的一层额外能力,而不是改掉原来的文件读取类。
TypeScript 示例
TypeScript 中可以用接口和类直接表达装饰模式。
先定义组件接口:
ts
interface Coffee {
getDescription(): string;
cost(): number;
}
实现基础咖啡:
ts
class Americano implements Coffee {
getDescription(): string {
return "美式咖啡";
}
cost(): number {
return 12;
}
}
class Latte implements Coffee {
getDescription(): string {
return "拿铁咖啡";
}
cost(): number {
return 18;
}
}
定义抽象装饰器:
ts
abstract class CoffeeDecorator implements Coffee {
protected constructor(protected readonly coffee: Coffee) {}
getDescription(): string {
return this.coffee.getDescription();
}
cost(): number {
return this.coffee.cost();
}
}
实现具体装饰器:
ts
class MilkDecorator extends CoffeeDecorator {
getDescription(): string {
return `${this.coffee.getDescription()} + 牛奶`;
}
cost(): number {
return this.coffee.cost() + 3;
}
}
class SugarDecorator extends CoffeeDecorator {
getDescription(): string {
return `${this.coffee.getDescription()} + 糖`;
}
cost(): number {
return this.coffee.cost() + 1;
}
}
使用:
ts
let coffee: Coffee = new Americano();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.getDescription());
console.log(coffee.cost());
如果项目偏函数式,也可以用高阶函数来实现类似装饰效果:
ts
type CoffeeFn = () => {
description: string;
cost: number;
};
const americano: CoffeeFn = () => ({
description: "美式咖啡",
cost: 12,
});
const withMilk = (coffee: CoffeeFn): CoffeeFn => () => {
const base = coffee();
return {
description: `${base.description} + 牛奶`,
cost: base.cost + 3,
};
};
const withSugar = (coffee: CoffeeFn): CoffeeFn => () => {
const base = coffee();
return {
description: `${base.description} + 糖`,
cost: base.cost + 1,
};
};
const order = withSugar(withMilk(americano));
console.log(order());
这不是 GoF 书里最传统的类结构,但仍然保留了装饰模式的核心思想:不修改原对象,通过包装叠加行为。
TypeScript 真实示例
TypeScript 项目里很常见的装饰场景,是给同一个请求函数叠加鉴权、日志、重试或缓存能力。比如业务代码统一依赖一个 HttpRequest 函数类型:
ts
type HttpRequest = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
const withAuth = (request: HttpRequest, token: string): HttpRequest => {
return (input, init = {}) =>
request(input, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`,
},
});
};
const withLogging = (request: HttpRequest): HttpRequest => {
return async (input, init) => {
console.log("request", input);
return request(input, init);
};
};
const request = withLogging(withAuth(fetch, "access-token"));
这里 fetch 是原始组件,withAuth 和 withLogging 是装饰器。它们不改变调用方看到的函数签名,只是在原有请求能力外面继续叠加行为。
什么时候适合用
适合使用装饰模式的场景:
- 想在运行时给对象动态增加能力。
- 额外能力可以自由组合。
- 不希望通过继承产生大量组合类。
- 希望每个增强能力保持独立,例如缓存、日志、鉴权、限流、压缩。
- 原始类不方便修改,或者修改会影响已有逻辑。
不适合的场景:
- 增强逻辑很少,直接改原类更清晰。
- 装饰链太长,导致调试困难。
- 每层装饰器都依赖具体实现细节,无法只通过统一接口工作。
- 对象生命周期和状态非常复杂,包装之后容易出现状态不一致。
和代理模式的区别
装饰模式和代理模式结构很像:它们都持有一个目标对象,并实现相同接口。
区别在目的:
- 装饰模式关注增强能力,让对象在原有行为基础上增加职责。
- 代理模式关注控制访问,例如延迟加载、权限校验、远程调用、缓存访问。
简单说:
- 装饰器是为了"加功能"。
- 代理是为了"管访问"。
和继承的区别
继承是在编译期固定能力组合,而装饰模式是在运行时组合能力。
如果使用继承:
text
MilkSugarAmericano extends Americano
这个组合在类定义时就固定了。
如果使用装饰模式:
java
Coffee coffee = new SugarDecorator(new MilkDecorator(new Americano()));
组合可以根据用户选择、配置文件、运行环境动态决定。
常见误区
误区一:装饰器只是简单包装
不是。普通包装可能只是转发调用,而装饰模式要求包装对象和被包装对象遵循同一个抽象接口,客户端可以用同一种方式使用它们。
误区二:装饰器越多越灵活
不一定。装饰器太多会让调用链变长,排查问题时需要一层层追踪。每个装饰器最好只做一件清晰的小事。
误区三:装饰模式可以替代所有继承
不能。装饰模式适合叠加对象行为,不适合表达稳定的"是什么"关系。如果模型本身就是天然的分类层级,继承或接口实现仍然可能更合适。
总结
装饰模式解决的是"能力组合爆炸"的问题。它通过统一接口和对象包装,让增强能力可以一层层叠加,而不用为每一种组合创建一个新类。
在 Java 中,装饰模式通常体现为"接口 + 抽象装饰器 + 具体装饰器";在 TypeScript 中,它既可以用类表达,也可以用高阶函数表达。判断是否需要装饰模式时,关键看能力是否需要自由组合,以及这些能力是否可以独立地包在原对象外面。