装饰器模式完全指南:从 Java IO 流到 HTTP 中间件的工程实战

装饰器模式可能是 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

组合模式完全指南:从 DOM 到表达式树的统一抽象

2026-5-15 11:50:57

技术教程

外观模式完全指南:从 jQuery 到 API 网关的分层简化

2026-5-15 11:57:55

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索