装饰器链的执行顺序为什么重要?
简化版
装饰器链的执行顺序很重要,因为每一层装饰器都会影响下一层的输入或上一层的输出。不同顺序可能导致结果不同,比如先压缩再加密,和先加密再压缩,语义和效果都可能完全不一样。
详细版
装饰器链通常长这样:
Component component = new DecoratorC(
new DecoratorB(
new DecoratorA(
new ConcreteComponent()
)
)
);
调用时,执行顺序是从外到内进入:
DecoratorC 前置逻辑
DecoratorB 前置逻辑
DecoratorA 前置逻辑
ConcreteComponent 核心逻辑
DecoratorA 后置逻辑
DecoratorB 后置逻辑
DecoratorC 后置逻辑
以数据写入为例:
DataSource source = new EncryptDecorator(
new CompressDecorator(
new FileDataSource()
)
);
这里写入时可能是先加密再压缩。如果换成:
DataSource source = new CompressDecorator(
new EncryptDecorator(
new FileDataSource()
)
);
写入时可能变成先压缩再加密。实际系统里一般更倾向先压缩再加密,因为加密后的数据随机性更强,压缩效果会变差。
所以使用装饰器时,不只要知道“包了哪些层”,还要知道“顺序是否符合语义”。
完整版教学
一、装饰器链是怎么执行的
每个装饰器都持有下一个组件:
class DecoratorA implements Component {
private final Component delegate;
}
当外层装饰器收到调用时,它可以先做自己的前置逻辑,再调用内部组件:
public String operation() {
beforeA();
String result = delegate.operation();
return afterA(result);
}
如果有多层装饰器,调用会像洋葱一样一层层进入,再一层层返回。
这和递归调用很像:外层先拿到控制权,内层执行完后,外层又有机会处理返回结果。
二、前置增强和后置增强的顺序不同
假设装饰链是:
new ADecorator(new BDecorator(new Core()))
调用时前置逻辑顺序是:
A before -> B before -> Core
后置逻辑顺序是:
Core -> B after -> A after
这意味着外层装饰器最先看到输入,最后看到输出。内层装饰器离核心对象更近。
如果你在外层做日志,它记录的可能是最终输入和最终输出;如果你在内层做日志,它记录的可能是被外层处理后的中间状态。
三、压缩和加密为什么顺序敏感
压缩和加密是解释装饰器顺序的经典例子。
常见正确顺序是写入时:
原始数据 -> 压缩 -> 加密 -> 存储
读取时反过来:
读取密文 -> 解密 -> 解压 -> 原始数据
如果先加密再压缩:
原始数据 -> 加密 -> 压缩
加密后的数据通常随机性更强,压缩算法很难找到重复模式,压缩效果会变差。
这说明装饰器不是随便套,顺序要符合业务和技术语义。
四、HTTP 客户端装饰链的例子
一个 HTTP 客户端可能叠加:
- 日志;
- 签名;
- 重试;
- 限流;
- 压缩;
- 监控。
顺序不同,行为可能不同。
比如签名必须基于最终请求内容。如果先签名,再由压缩装饰器修改 body,就可能导致服务端验签失败。
再比如重试和日志顺序:
- 日志在重试外层:记录一次总体调用;
- 日志在重试内层:每次重试都记录一次。
两种都可能合理,但语义不同,需要按需求选择。
五、如何管理复杂装饰链
当装饰器很多时,不建议在业务代码里到处写:
new A(new B(new C(new D(target))))
更好的方式是:
- 用工厂方法集中装配;
- 用配置决定装饰器顺序;
- 给关键顺序写测试;
- 给装饰器命名清楚;
- 避免让装饰器之间产生隐式强依赖。
例如:
HttpClient client = HttpClientFactory.createDefaultClient();
工厂内部负责装饰链顺序,业务代码只使用最终对象。
六、前置与后置逻辑形成的栈结构
原文 100 KB 先压缩到 30 KB 再加密,网络传 30 KB;若先加密,高熵密文可能仍接近 100 KB,压缩收益几乎消失。
encode: compress -> encrypt -> send; decode: receive -> decrypt -> decompress
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 构造表达式从外向内读,实际前置调用通常从最外层进入,后置返回则反向展开。 |
| 适用边界 | 有逆操作的装饰必须保证编码与解码顺序互为逆序;日志、缓存、重试也要明确包在调用的哪一侧。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:构造表达式从外向内读,实际前置调用通常从最外层进入,后置返回则反向展开。
八、常见误区与追问
- 误区:装饰器交换顺序通常不会影响结果。 当增强不满足交换律时,顺序会改变语义、性能甚至可恢复性。
- 误区:装饰器可以任意改变组件接口。 透明装饰依赖共同的 Component 契约;随意增加只能向下转型调用的方法,会破坏可替换性和继续组合的能力。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:怎样测试装饰链的顺序? 用记录型假组件捕获进入与返回序列,并覆盖异常分支和逆操作 round-trip。
- 追问:装饰器链越长,扩展性就一定越好吗? 不一定;链越长,顺序、异常传播和排障成本越高,应由装配层集中管理并限制职责粒度。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
装饰器链像洋葱:调用从外层进,结果从内层出。顺序决定语义,尤其是压缩、加密、签名、重试、日志这类增强,套错顺序就可能不是小问题,而是业务错误。