Java IO 流就是装饰器模式,但 90% 的人只把它当“套娃“
Java IO 流就是装饰器模式,但 90% 的人只把它当"套娃"
你初学 Java 时一定写过这种代码:
java BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("data.txt"), StandardCharsets.UTF_8 ) )
一层包一层,像俄罗斯套娃。老师告诉你这叫"装饰器模式",但你可能直到工作几年都没真正用过它——除了 IO 流。
装饰器模式是 GoF 23 个模式里最容易被低估的一个。它不是 IO 流的专利,而是解决"给对象加功能但不改继承树"的最佳方案。问题是,很多人把它和代理模式、继承混为一谈,用错了场景。
装饰器模式到底解决什么问题
假设你在做一个订单服务,需要给 OrderService 加功能:
- 第 1 版:基本的创建订单
- 第 2 版:加日志
- 第 3 版:加缓存
- 第 4 版:加限流
- 第 5 版:加事务
如果用继承,你的类会变成这样:
java OrderService ├── LoggedOrderService ├── CachedOrderService ├── RateLimitedOrderService ├── TransactionalOrderService ├── LoggedCachedOrderService ├── CachedRateLimitedOrderService └── ... // 组合爆炸
每加一个新功能,组合数量翻倍。继承方案在这里彻底失效。
装饰器模式的做法是:把每个功能做成一个独立的"包装器",运行时按需组合。
```java public interface OrderService { Order createOrder(CreateOrderRequest request); }
// 基础实现 public class BasicOrderService implements OrderService { ... }
// 日志装饰器 public class LoggingOrderService implements OrderService { private final OrderService delegate;
public Order createOrder(CreateOrderRequest request) {
log.info("createOrder start: {}", request);
Order result = delegate.createOrder(request);
log.info("createOrder end: {}", result);
return result;
}
}
// 缓存装饰器 public class CachingOrderService implements OrderService { private final OrderService delegate; private final Cache cache;
public Order createOrder(CreateOrderRequest request) {
// 创建订单其实不适合缓存,这里只示例结构
return delegate.createOrder(request);
}
} ```
组合方式:
java OrderService service = new LoggingOrderService( new RateLimitedOrderService( new TransactionalOrderService( new BasicOrderService() ) ) );
功能可以任意叠加,不需要为每种组合写一个子类。
装饰器 vs 代理:很多人分不清
装饰器和代理模式的 UML 几乎一样:都是一个类持有另一个同接口对象的引用,方法调用时转发给被持有对象。
区别在意图,不在结构**:
- 装饰器:目的是增强功能。被装饰对象和装饰器可以独立存在,用户通常主动选择用哪个装饰器。
- 代理:目的是控制访问。代理对象替代真实对象,用户通常不知道自己用的是代理。
举个例子:
```java // 装饰器:增强 InputStream 的缓冲能力 InputStream buffered = new BufferedInputStream(new FileInputStream("file.txt"));
// 代理:控制对真实对象的访问 UserService userService = (UserService) Proxy.newProxyInstance(...); // JDK 动态代理 ```
Spring 的 @Transactional 和 @Cacheable 底层用的是代理,不是装饰器。因为应用代码通常不主动选择"要不要事务",而是由框架根据注解决定。
但如果你在业务层自己写一组可插拔的增强组件(日志、限流、鉴权、审计),那更接近装饰器。
装饰器 vs 继承:什么时候该用哪个
继承的语义是"是一个"。Cat is an Animal,这是继承。
装饰器的语义是"包装一个"。BufferedReader wraps a Reader,这不是"是一种 Reader",而是"给 Reader 加缓冲能力"。
判断标准很简单:
- 如果子类和父类是同一概念的不同分类 → 继承
- 如果要给对象动态添加职责,且这些职责可以独立变化 → 装饰器
比如电商促销:
```java // 继承的坏味道 public class DiscountedMemberFirstOrder extends Order { ... }
// 装饰器更合适 Order order = new MemberDiscountDecorator( new FirstOrderDiscountDecorator( new TimeLimitedDiscountDecorator(new BasicOrder()) ) ); ```
促销规则可以叠加、可以调整顺序、可以运行时组合,用继承会死得很惨。
装饰器模式的三个工程陷阱
陷阱一:装饰顺序影响结果
装饰器是链式调用,顺序不同结果可能不同。
```java // 先限流,后日志 new LoggingOrderService(new RateLimitedOrderService(new BasicOrderService())); // 被限流的请求不会打日志
// 先日志,后限流 new RateLimitedOrderService(new LoggingOrderService(new BasicOrderService())); // 所有请求都先打日志,再判断是否限流 ```
两种顺序都是合法的,但行为不同。团队里必须约定装饰器顺序规则,否则线上行为会变。
陷阱二:装饰器破坏对象身份
```java InputStream raw = new FileInputStream("file.txt"); InputStream buffered1 = new BufferedInputStream(raw); InputStream buffered2 = new BufferedInputStream(raw);
buffered1 == buffered2; // false buffered1.equals(buffered2); // false ```
装饰器每次 new 都是新对象。如果你的代码依赖对象身份(比如用 == 判断、或者 HashMap key),装饰器会引入 bug。
陷阱三:过度装饰导致调试困难
我见过一个项目,一个 Service 接口被包了 7 层装饰器:
java Service service = new MetricsService( new LoggingService( new AuthService( new CacheService( new RateLimitService( new CircuitBreakerService( new ValidationService( new BasicService() ) ) ) ) ) ) );
调用栈深不见底,报错时堆栈 30 多层。装饰器是解耦利器,但解耦过头会把"简单调用"变成"考古现场"。
一般来说,业务层装饰器超过 4 层就要开始警惕。要么合并同类装饰器,要么改用 AOP/拦截器机制。
Java IO 流为什么是典型的装饰器
回到开头的例子:
java BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("data.txt"), StandardCharsets.UTF_8 ) );
这里每一层都有独立职责:
FileInputStream:从文件读字节InputStreamReader:把字节按 UTF-8 解码成字符BufferedReader:给字符流加缓冲,减少系统调用
三层可以独立替换。比如把 FileInputStream 换成 ByteArrayInputStream,上面两层不用动。这就是装饰器的威力:职责垂直拆分,运行时水平组合。
什么时候不要用装饰器
装饰器不是银弹。以下场景它并不合适:
- 接口方法太多。 如果接口有 20 个方法,装饰器要转发 20 次, boilerplate 爆炸。Java IO 的
InputStream方法不多,所以适合。Spring 的BeanFactory接口方法多,就不适合装饰器。 - 需要访问被装饰对象的私有状态。 装饰器只能通过公共接口调用,拿不到内部状态。
- 装饰逻辑需要跨多个方法共享上下文。 比如事务需要记录 begin/commit/rollback 状态,单个方法装饰器很难优雅处理,AOP 或显式事务管理更合适。
总结
装饰器模式的价值,一句话概括:用组合代替继承,给对象动态添加职责。
它最适合的场景是: - 功能可以独立变化 - 功能可以叠加组合 - 不想改原有类
但它也带来成本:调用链变深、顺序敏感、对象身份改变。用之前先想清楚,你的场景是真需要装饰器,还是只是想让类结构看起来"很设计模式"。
设计模式不是勋章,是工具。用对了省代码,用错了加复杂度。
我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章。
更多推荐


所有评论(0)