装饰器模式可能是 GoF 23 个模式里"使用频率排前三、被误解程度也排前三"的一个。它的核心思想极其简单 —— 用一个包装对象给原对象加新功能,且包装可以叠加。但很多人把它和继承混为一谈,或者把它和 Python 装饰器、Java 注解当成同一件事。这篇文章把装饰器从原理讲到 Java IO 流、HTTP 中间件、React HOC,讲清楚它和代理、适配器、责任链的边界。
问题:用继承加功能会爆炸
想给一个 Coffee 类加"奶""糖""焦糖""巧克力"等配料,每种配料还能加不同份数。朴素继承:
class Coffee {
public double cost() { return 10; }
}
class CoffeeWithMilk extends Coffee { public double cost() { return 12; } }
class CoffeeWithSugar extends Coffee { ... }
class CoffeeWithMilkAndSugar extends Coffee { ... }
class CoffeeWithMilkAndSugarAndChocolate extends Coffee { ... }
// 4 种配料的所有组合是 2^4 = 16 个子类
组合爆炸再一次出现。如果再支持"加倍奶""半糖"这种参数化配料,根本写不完。
装饰器模式给出的答案:把每种配料做成一个"装饰器",可以套在 Coffee 外面;装饰器自己也是 Coffee,所以可以再被另一个装饰器套。
装饰器的标准结构
// 1. 共同抽象
interface Beverage {
double cost();
String description();
}
// 2. 具体被装饰对象
class Espresso implements Beverage {
public double cost() { return 8; }
public String description() { return "Espresso"; }
}
class HouseBlend implements Beverage {
public double cost() { return 10; }
public String description() { return "House Blend"; }
}
// 3. 抽象装饰器:同样实现 Beverage,内部持有一个 Beverage
abstract class CondimentDecorator implements Beverage {
protected Beverage beverage;
public CondimentDecorator(Beverage b) { this.beverage = b; }
}
// 4. 具体装饰器:加自己的行为,然后委托给被包装对象
class Milk extends CondimentDecorator {
public Milk(Beverage b) { super(b); }
public double cost() { return beverage.cost() + 2; }
public String description() { return beverage.description() + " + Milk"; }
}
class Sugar extends CondimentDecorator {
public Sugar(Beverage b) { super(b); }
public double cost() { return beverage.cost() + 1; }
public String description() { return beverage.description() + " + Sugar"; }
}
class Chocolate extends CondimentDecorator {
public Chocolate(Beverage b) { super(b); }
public double cost() { return beverage.cost() + 3; }
public String description() { return beverage.description() + " + Chocolate"; }
}
// 5. 任意组合
Beverage drink = new Chocolate(new Milk(new Sugar(new Espresso())));
System.out.println(drink.description() + " = $" + drink.cost());
// Espresso + Sugar + Milk + Chocolate = $14
关键观察:每个装饰器都是 Beverage,所以可以被另一个装饰器再次包装。这种"包装链"让 N 种配料的所有组合都能用 N 个装饰器表达,无需子类。加新配料 = 新写一个装饰器,旧代码完全不动。
实战 1:Java IO 流
Java IO 是装饰器模式在标准库的代表 —— 看一段最常见的文件读取:
// 底层:FileInputStream 是"具体被装饰对象",提供原始字节流
InputStream in = new FileInputStream("data.txt");
// 加缓冲:BufferedInputStream 是装饰器
in = new BufferedInputStream(in);
// 加压缩支持:GZIPInputStream 也是装饰器
in = new GZIPInputStream(in);
// 转字符流 + 缓冲读行:这层是 Reader 体系的装饰器
BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) System.out.println(line);
这是装饰器模式最直观的"用户体验" —— 每一层只关心自己的事(缓冲、解压、字符编码),组合起来满足复杂需求。Java 把"加缓冲""加压缩""加加密"等正交关注点都做成独立装饰器,组合可以无限灵活。
OutputStream 那一侧完全对称:new GZIPOutputStream(new BufferedOutputStream(new FileOutputStream(...)))。
实战 2:HTTP 中间件
Express、Koa、Gin、ASP.NET Core 的中间件机制本质都是装饰器:
// Express 风格的中间件链
app.use(loggingMiddleware); // 装饰器 1:记录请求
app.use(authMiddleware); // 装饰器 2:校验登录
app.use(cacheMiddleware); // 装饰器 3:命中缓存就直接返回
app.use(rateLimitMiddleware); // 装饰器 4:限流
app.get('/api/data', handler); // 最里层:真正的业务处理
// 实现简化版
type Handler = (req: Request, res: Response) => Promise<void>;
type Middleware = (h: Handler) => Handler;
const loggingMiddleware: Middleware = (next) => async (req, res) => {
console.log(`-> ${req.method} ${req.url}`);
const start = Date.now();
await next(req, res);
console.log(`<- ${Date.now() - start}ms`);
};
const authMiddleware: Middleware = (next) => async (req, res) => {
if (!req.headers.authorization) { res.status(401).send('unauth'); return; }
await next(req, res);
};
// 装饰组合
let handler: Handler = async (req, res) => res.send('hello');
handler = authMiddleware(handler);
handler = loggingMiddleware(handler);
// 请求进来时:logging -> auth -> handler -> auth 返回 -> logging 返回
每个中间件都是"装饰一个 Handler 得到新的 Handler"。链式调用既能在调用前做事(before),又能在调用后做事(after),还能短路(不调 next,直接返回)。
实战 3:React 高阶组件(HOC)
React 早期的 HOC 是装饰器的前端经典:
// 加日志
function withLogging<P>(Component: React.ComponentType<P>) {
return function Wrapped(props: P) {
useEffect(() => {
console.log(`mounted: ${Component.name}`);
return () => console.log(`unmount: ${Component.name}`);
}, []);
return <Component {...props} />;
};
}
// 加权限校验
function withAuth<P>(Component: React.ComponentType<P>) {
return function Wrapped(props: P) {
const { user } = useAuth();
if (!user) return <Redirect to="/login" />;
return <Component {...props} />;
};
}
// 任意组合(Hooks 时代多用自定义 Hook 替代,但 HOC 思路仍然成立)
const EnhancedProfile = withLogging(withAuth(ProfilePage));
每个 HOC 都"装饰"一个组件得到新组件。和 Java IO 一样,装饰链从外到内执行 before 逻辑,从内到外执行 after 逻辑。
实战 4:函数式装饰器(Python / JS)
Python 内置 @decorator 语法糖直接表达装饰器模式:
import time, functools
def timing(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = time.time()
result = fn(*args, **kwargs)
print(f"{fn.__name__} took {time.time() - start:.3f}s")
return result
return wrapper
def retry(times):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for i in range(times):
try: return fn(*args, **kwargs)
except Exception as e:
if i == times - 1: raise
print(f"retry {i+1}: {e}")
return wrapper
return decorator
@timing
@retry(3)
def fetch(url):
return requests.get(url).json()
fetch("https://api.example.com/data")
注意 @timing 在 @retry(3) 上面,但从下往上应用 —— 等价于 timing(retry(3)(fetch))。调用时先进 timing 再进 retry 再进真实 fetch。这就是装饰链的执行顺序。
装饰器 vs 继承
都是"扩展行为",区别根本:
- 继承:编译期决定,静态的。父类和子类的关系一旦确定,运行时改不了。
- 装饰器:运行时决定,动态的。同一个对象可以根据条件套不同装饰器,或者顺序不同。
// 继承:编译期就知道这个 Logger 写文件
class FileLogger extends Logger { ... }
// 装饰器:根据配置决定要不要加缓冲、要不要加压缩
Logger logger = new FileLogger();
if (config.useBuffer) logger = new BufferingDecorator(logger);
if (config.useEncrypt) logger = new EncryptDecorator(logger);
if (config.useCompress) logger = new CompressDecorator(logger);
这就是"组合优于继承"在装饰器上的体现。
装饰器 vs 代理 vs 适配器
这三个都是"包装一层",意图不同:
- 装饰器(Decorator):增强行为(加日志、加缓冲、加权限)。可以叠加多层。接口不变。
- 代理(Proxy):控制访问(权限、缓存、延迟、远程)。通常一层。接口不变。
- 适配器(Adapter):转换接口(老 API 变新 API)。接口变了。
装饰器和代理结构上看着一样,关键看意图:装饰器是"为了扩展功能",代理是"为了控制访问"。Spring AOP 同时具备两种能力 —— 你写的 @Around Advice 可能是加日志(装饰),也可能是权限校验拒绝调用(代理)。
各语言里的装饰器
TypeScript 内置装饰器语法(stage-3)
function logCall(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`call ${key}`, args);
const result = original.apply(this, args);
console.log(`return`, result);
return result;
};
}
class API {
@logCall
fetch(id: number) {
return { id, name: 'mores' };
}
}
new API().fetch(1);
// call fetch [ 1 ]
// return { id: 1, name: 'mores' }
注意:TypeScript 5+ 的"实验性装饰器"和"stage-3 装饰器"语法略有差异,需要在 tsconfig 里相应设置 experimentalDecorators 或不设置(用 stage-3)。Angular、NestJS 的 @Component / @Injectable 都依赖这套机制。
Go:没有装饰器关键字,用函数高阶组合
type Handler func(http.ResponseWriter, *http.Request)
func withLogging(next Handler) Handler {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}
}
func withAuth(next Handler) Handler {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "unauth", 401)
return
}
next(w, r)
}
}
handler := withLogging(withAuth(realHandler))
http.HandleFunc("/api/data", handler)
装饰器的常见坑
坑 1:装饰顺序敏感。 缓存装饰器在权限校验装饰器外面 vs 里面,结果完全不同 —— 在外面会"先看缓存",未登录用户能拿到别人的数据;在里面会"先校验",合理但缓存命中率低。设计装饰链的顺序时要明确思考"谁先谁后"。
坑 2:装饰器太多导致调试困难。 一个调用穿过 5 层装饰器,异常栈很深,日志难以串联。建议:每个装饰器可以加 __wrapped__ 引用(Python 的 functools.wraps 自动加),让你能"穿透"装饰器看到真实函数;加追踪 id,在每个装饰器里输出,串联整条调用链。
坑 3:装饰器修改了原对象的状态。 一个本应"无副作用"的装饰器(例如计时)如果偷偷往原对象塞字段,会破坏原对象的不变性,引起诡异 bug。装饰器应该尽量纯 —— 包装动作发生在调用前后,不应改变被装饰对象内部状态。
坑 4:装饰器和方法签名变化。 Python 装饰器如果不用 functools.wraps,被装饰函数的 __name__ / __doc__ / __signature__ 全没了,日志和反射都失效。一定要带上 wraps。
动态条件装饰
有时候装饰要按运行时条件决定 —— 比如"开发环境加日志,生产不加":
Beverage drink = new Espresso();
if (config.isDev()) drink = new LoggingDecorator(drink);
if (user.isVip()) drink = new VipPriceDecorator(drink);
if (couponCode != null) drink = new CouponDecorator(drink, couponCode);
double finalPrice = drink.cost();
这种"条件装饰"是装饰器最大的灵活性 —— 继承做不到。整套打包成一个"装饰策略"函数会更清爽:
Function<Beverage, Beverage> buildDecorators(Config c, User u) {
Function<Beverage, Beverage> chain = Function.identity();
if (c.isDev()) chain = chain.andThen(LoggingDecorator::new);
if (u.isVip()) chain = chain.andThen(VipPriceDecorator::new);
return chain;
}
Beverage drink = buildDecorators(config, user).apply(new Espresso());
什么时候不用装饰器
- 变化的不是"行为"而是"数据":加字段不该用装饰器,该用扩展类或组合。
- 装饰链固定不变:永远是 A → B → C 没有变体,直接合并成一个类更简单。
- 装饰器互相之间高耦合:加 A 必须配合 B 才能工作,这时装饰器的"独立可组合"假设破裂,要么合并要么用更适合的模式。
写在最后
装饰器模式的核心 idea 是"对扩展开放,对修改关闭"在运行时的极致表达。你不改原对象一行代码,通过包装就能加入任何新行为,且这些行为可以按需组合、按需开关。Java IO 流、HTTP 中间件、Python 装饰器、React HOC、Spring AOP —— 主流框架在最关键的位置都选了它,不是偶然。
下次面对"我要给这个类加一个小功能,但又不想动它"的场景,先停一秒,想想是不是装饰器最合适。如果功能可叠加、独立、有条件开启 —— 装饰器是最优解;如果只是一次性补丁,直接组合或继承更轻便。设计模式不是越多越好,但用对场景的装饰器,能让你的代码瞬间从"硬编码"升级到"可插拔"。
—— 别看了 · 2026