装饰器链在工程中如何组装和治理?
简化版
装饰器链的组装要明确顺序、职责、配置来源和可观测性。工程中常用配置、工厂、依赖注入容器或装饰器注册表来组装链;治理重点是避免顺序混乱、重复装饰、职责膨胀、异常吞噬和排查困难。
详细版
装饰器模式在小例子里通常手写嵌套:
DataSource ds = new CacheDecorator(
new CompressionDecorator(
new EncryptionDecorator(
new FileDataSource())));
真实工程里不能到处手写这种嵌套,否则顺序难统一、配置难变更、排查难定位。更常见的方式是:
- 用工厂按配置组装装饰器链;
- 用 Spring 等容器按
@Order注入装饰器列表; - 用注册表根据能力开关选择装饰器;
- 给每个装饰器命名并输出链路信息;
- 对顺序敏感的装饰器写测试。
高频考点是:装饰器链不是越长越好。链越长,性能、异常传播、日志定位、状态一致性都更复杂。好的治理方式要让调用方知道最终对象被哪些装饰器包裹、顺序是什么、每层耗时是多少。
完整版教学
一、为什么手写嵌套不适合大型工程
手写嵌套在教学中很直观,但在工程中容易失控。假设系统有缓存、压缩、加密、限流、日志 5 个装饰器,不同场景启用不同组合。若每个调用点自己 new,顺序可能不一致。
new LogDecorator(new CacheDecorator(new FileDataSource()));
new CacheDecorator(new LogDecorator(new FileDataSource()));
这两段代码都能运行,但语义不同。日志在缓存外层时会记录缓存命中耗时;日志在缓存内层时可能只记录未命中后的真实读写耗时。顺序差异会直接影响指标解释。
记忆钩子:装饰器链不是简单套娃,顺序本身就是业务语义。
| 治理点 | 失控表现 | 推荐做法 |
|---|---|---|
| 顺序 | 不同调用点嵌套顺序不一致 | 工厂或容器集中组装 |
| 可观测性 | 线上不知道经过哪些装饰器 | 输出链路名称、耗时和异常位置 |
| 配置 | 开关分散在业务代码里 | 用注册表和配置统一声明 |
二、用工厂集中组装装饰器链
最直接的治理方式是工厂集中创建。调用方只表达需要哪些能力,工厂决定装饰器顺序和具体实现。
class DataSourceFactory {
public DataSource create(DataSourceConfig config) {
DataSource ds = new FileDataSource(config.path());
if (config.encryptionEnabled()) {
ds = new EncryptionDecorator(ds);
}
if (config.compressionEnabled()) {
ds = new CompressionDecorator(ds);
}
if (config.cacheEnabled()) {
ds = new CacheDecorator(ds);
}
ds = new MetricsDecorator(ds);
return ds;
}
}
这样所有调用点都走同一套组装规则。新增装饰器时,不需要在业务代码里到处搜索嵌套结构。工厂也可以输出最终链路,方便排查。
三、配置驱动装饰器链
当装饰器组合需要运行时调整时,可以用配置驱动。比如配置里声明启用哪些装饰器和顺序:
decorators:
- metrics
- cache
- compression
- encryption
工厂读取配置后从注册表取装饰器构造器:
base = FileDataSource
apply encryption
apply compression
apply cache
apply metrics
return metrics(cache(compression(encryption(base))))
注意配置列表的方向要定义清楚。是从内到外,还是从外到内?如果团队没有统一约定,配置会变成事故来源。建议在文档和日志中明确输出最终链路,例如:
DataSource chain: Metrics -> Cache -> Compression -> Encryption -> File
四、依赖注入容器如何参与组装
在 Spring 这类容器中,可以把每个装饰器注册为组件,并通过顺序注解或配置收集。核心思路是:容器负责发现装饰器,工厂负责按顺序应用装饰器。
interface DataSourceDecoratorFactory {
int order();
DataSource decorate(DataSource source);
}
class DataSourceChainBuilder {
private final List<DataSourceDecoratorFactory> factories;
DataSource build(DataSource source) {
DataSource current = source;
for (DataSourceDecoratorFactory factory : factories) {
current = factory.decorate(current);
}
return current;
}
}
这里要小心循环依赖和自动装配歧义。如果装饰器本身又依赖最终装饰后的对象,可能形成环。更稳的方式是让装饰器工厂只依赖必要配置和基础服务,不直接依赖最终组件。
五、装饰器链顺序如何测试
顺序敏感的装饰器必须测试。可以用一个简单组件记录调用事件,验证 before/after 顺序。
Metrics before
Cache before
Compression before
Encryption before
File write
Encryption after
Compression after
Cache after
Metrics after
如果有 4 个装饰器,理论排列有 4! = 24 种,不可能靠代码阅读保证每次都正确。至少要为关键组合写测试,尤其是缓存、压缩、加密这类顺序不同会导致结果不同的能力。
例如写入时通常先压缩再加密,读取时先解密再解压。如果反过来,压缩加密后的随机字节通常收益很差,甚至无法还原。
六、如何防止重复装饰
工程中常见问题是同一个能力被装饰两次。比如外层框架已经加了日志装饰器,业务工厂又加一次,最后一条请求打印两份日志。缓存、限流、压缩重复装饰也会引发更严重的问题。
可以用装饰器元数据治理:
Decorator metadata:
name = cache
repeatable = false
order = 20
组装时检查不可重复装饰器:
if (!decorator.repeatable() && applied.contains(decorator.name())) {
throw new IllegalStateException("duplicate decorator: " + decorator.name());
}
不是所有装饰器都不能重复。比如重试装饰器通常不应重复,日志装饰器可能允许不同级别重复,指标装饰器可能一个记录业务指标、一个记录系统指标。关键是显式声明,而不是靠约定猜。
七、链路可观测性怎么做
装饰器链一长,排查就会变难。建议每条链在启动时或首次构建时输出最终顺序,并在关键装饰器中记录耗时、命中率、异常数。
chain=Metrics->Cache->Compression->Encryption->File
metrics.total=12ms
cache.hit=false
compression.ratio=0.42
encryption.cost=1.3ms
这些信息能帮助定位问题。例如某次请求总耗时 80 ms,目标文件写入只花 5 ms,其余时间可能在压缩或加密层。没有分层指标时,调用方只会觉得“DataSource 很慢”。
八、常见误区与追问
- 误区:装饰器链手写嵌套最直观,工程里也应该到处这么写。 手写嵌套会让顺序和配置散落,后续难治理。
- 误区:装饰器越多越灵活。 链越长,性能、异常传播和排查成本越高。
- 误区:配置顺序大家看一眼就懂。 必须明确配置是从内到外还是从外到内,并输出最终链路。
- 追问:如何集中管理装饰器顺序? 用工厂、注册表、容器排序或配置驱动统一组装。
- 追问:如何避免重复装饰? 给装饰器声明 name、order、repeatable 等元数据,组装时检查。
- 追问:装饰器链如何排查性能问题? 记录最终链路和每层耗时、命中率、异常数。
- 追问:顺序敏感的装饰器怎么验证? 用集成测试记录事件顺序,并覆盖关键组合。
九、加强记忆
装饰器链治理记住“集中组装、显式顺序、防重复、可观测”。教学例子里手写嵌套没问题,工程里要把链路变成可配置、可测试、可追踪的结构,否则灵活性会反过来变成排查成本。