1. 装饰器模式:给对象"穿衣服"的艺术
作为一名在Java领域摸爬滚打多年的老码农,我至今还记得第一次在项目中误用继承导致类爆炸的惨痛教训。那是一个电商促销系统,各种优惠券、会员折扣、节日活动的组合需求让我写了20多个子类,直到团队里的架构师拍了拍我的肩膀说:"小伙子,该学学装饰器模式了"。
装饰器模式(Decorator Pattern)本质上是一种"即插即用"的功能扩展方式。想象你在给圣诞树挂装饰品——你可以随意组合彩灯、星星、礼物盒,而不需要为"彩灯+星星"、"彩灯+礼物盒"每种组合都造一棵新树。这就是装饰器模式的核心:动态地给对象添加职责,且比继承更灵活。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么需要装饰器模式?
2.1 继承带来的类爆炸问题
假设我们要开发一个咖啡订单系统。最初只有四种基础咖啡:
java复制abstract class Coffee {
abstract double cost();
}
class Americano extends Coffee { /*...*/ }
class Espresso extends Coffee { /*...*/ }
class Latte extends Coffee { /*...*/ }
class Cappuccino extends Coffee { /*...*/ }
现在要添加配料:牛奶、糖浆、奶油。如果使用继承,组合数量会呈指数增长:
code复制Americano
AmericanoWithMilk
AmericanoWithSyrup
AmericanoWithCream
AmericanoWithMilkAndSyrup
AmericanoWithMilkAndCream
AmericanoWithSyrupAndCream
AmericanoWithAllThree
...(其他咖啡同理)
这种类爆炸(Class Explosion)会让系统难以维护。而装饰器模式通过组合而非继承来解决这个问题。
2.2 开闭原则的完美实践
装饰器模式完美符合开闭原则(OCP):
- 对扩展开放:可以随时新增装饰器
- 对修改关闭:无需修改原有类
在我参与过的一个日志系统改造中,原始代码通过不断修改Logger类来添加新功能(如加密、压缩、格式化)。使用装饰器模式后,每个功能都是一个独立装饰器,可以任意组合:
java复制Logger logger = new EncryptionDecorator(
new CompressionDecorator(
new FormatDecorator(
new FileLogger())));
3. 装饰器模式的结构解析
3.1 UML类图核心要素
code复制<<interface>> Component
+operation()
ConcreteComponent
+operation()
<<abstract>> Decorator
-component: Component
+operation()
ConcreteDecoratorA
+operation()
+addedBehavior()
ConcreteDecoratorB
+operation()
+addedBehavior()
关键点:
- 所有装饰器都实现与被装饰对象相同的接口
- 装饰器持有被装饰对象的引用(组合)
- 可以在调用被装饰对象方法前后添加新行为
3.2 Java IO中的经典实现
Java的IO包是装饰器模式的教科书级案例:
java复制// 被装饰的组件
InputStream fileStream = new FileInputStream("data.txt");
// 装饰器链
InputStream buffered = new BufferedInputStream(fileStream);
InputStream gzipped = new GZIPInputStream(buffered);
DataInputStream dataInput = new DataInputStream(gzipped);
这种设计让功能组合变得极其灵活:
- 你可以单独用BufferedInputStream
- 可以组合缓冲+GZIP压缩
- 可以任意调整装饰顺序
4. 手把手实现咖啡店案例
4.1 基础组件定义
java复制// 组件接口
public interface Coffee {
String getDescription();
double cost();
}
// 具体组件
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Black coffee";
}
@Override
public double cost() {
return 2.0;
}
}
4.2 抽象装饰器基类
java复制public abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
}
4.3 具体装饰器实现
java复制public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}
@Override
public double cost() {
return decoratedCoffee.cost() + 0.5;
}
}
public class SugarDecorator extends CoffeeDecorator {
// 类似实现...
}
4.4 客户端使用示例
java复制Coffee myCoffee = new SugarDecorator(
new MilkDecorator(
new SimpleCoffee()));
System.out.println(myCoffee.getDescription());
// 输出: Black coffee, Milk, Sugar
System.out.println(myCoffee.cost());
// 输出: 3.0 (2.0 + 0.5 + 0.5)
5. 装饰器模式的实战技巧
5.1 与代理模式的区别
新手常混淆装饰器与代理模式,关键区别在于:
- 装饰器:增强功能(添加新职责)
- 代理:控制访问(不改变原始行为)
比如:
- 给图片加滤镜是装饰器
- 延迟加载图片是代理
5.2 多层装饰的性能考量
装饰器链过长会导致:
- 调用栈深度增加(每个装饰器都包装一层)
- 小对象数量增多(内存碎片)
在金融交易系统中,我们曾遇到因装饰器嵌套过深导致的性能问题。解决方案:
- 限制装饰层级(如最多5层)
- 对高频路径使用预组合的装饰器
5.3 与继承的选型决策
使用装饰器当满足以下条件:
- 需要运行时动态添加/移除功能
- 功能组合爆炸(如咖啡+配料)
- 不想影响其他对象
而继承更适合:
- 功能是对象固有属性(如"鸟会飞")
- 扩展关系稳定不变
6. 现代语言中的装饰器演进
6.1 Python的@decorator语法糖
Python通过@语法原生支持装饰器:
python复制def logger(func):
def wrapper(*args):
print(f"Calling {func.__name__}")
return func(*args)
return wrapper
@logger
def say_hello(name):
print(f"Hello {name}")
# 等效于:say_hello = logger(say_hello)
6.2 TypeScript装饰器
TypeScript装饰器可以修饰类、方法、属性:
typescript复制function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = "report";
}
6.3 Java注解与装饰器
虽然Java没有语法级装饰器,但注解+APT(Annotation Processing Tool)可以实现类似效果:
java复制@Decorator
public class LoggingService {
@Decorate
public void serve() {
// ...
}
}
7. 实际项目中的坑与解决方案
7.1 装饰顺序影响结果
在电商促销系统中,我们发现不同的装饰顺序会导致价格计算差异:
java复制// 方案1:先打折再加运费
new ShippingDecorator(
new DiscountDecorator(cart));
// 方案2:先加运费再打折
new DiscountDecorator(
new ShippingDecorator(cart));
最终我们引入装饰器优先级机制,在配置文件中定义顺序:
xml复制<decorators>
<decorator class="DiscountDecorator" order="1"/>
<decorator class="ShippingDecorator" order="2"/>
</decorators>
7.2 循环引用检测
装饰器相互装饰会导致栈溢出:
java复制DecoratorA decorates DecoratorB
DecoratorB decorates DecoratorA
我们的解决方案是在装饰器工厂中加入检测逻辑:
java复制public Decorator createDecorator(Component c, Class type) {
if (isCyclic(c, type)) {
throw new IllegalDecoratorException("Cyclic decoration detected");
}
// ...
}
7.3 与框架的整合问题
在Spring中使用装饰器时,要注意:
- 装饰器本身需要是Spring Bean
- 被装饰对象应该通过@Autowired注入
- 建议使用@Primary标注具体实现
最佳实践示例:
java复制@Configuration
public class AppConfig {
@Bean
@Primary
public Coffee coffee() {
return new SimpleCoffee();
}
@Bean
public Coffee decoratedCoffee() {
return new MilkDecorator(coffee());
}
}
8. 面试常见问题解析
8.1 "装饰器模式适用于什么场景?"
高分回答结构:
- 需要动态/透明地扩展功能时
- 不适合用继承的类爆炸场景
- 举例Java IO流、Spring缓存注解等
- 对比继承的优缺点
8.2 "装饰器模式和继承如何选择?"
对比维度表:
| 维度 | 装饰器模式 | 继承 |
|---|---|---|
| 扩展方式 | 动态组合 | 静态编译时确定 |
| 关系 | has-a(组合) | is-a |
| 灵活性 | 高(运行时改变) | 低(需修改代码) |
| 类数量 | 线性增长(n+m) | 指数增长(n^m) |
| 访问权限 | 只能访问公共方法 | 可以访问protected成员 |
8.3 "实现装饰器模式要注意什么?"
关键点:
- 保持组件接口简洁
- 装饰器要透明(不改变接口)
- 避免多层装饰的性能问题
- 处理好equals/hashCode方法
- 考虑线程安全性
9. 经典框架中的装饰器应用
9.1 Java Collections
Collections.unmodifiableXXX()系列方法返回的就是装饰器:
java复制List<String> list = new ArrayList<>();
List<String> unmodifiable = Collections.unmodifiableList(list);
unmodifiable.add("test"); // 抛出UnsupportedOperationException
9.2 Spring Security
Security的过滤器链本质是装饰器模式:
java复制http.addFilterBefore(
new CustomFilter(),
UsernamePasswordAuthenticationFilter.class);
9.3 React高阶组件
React的HOC(Higher-Order Component)是前端装饰器:
jsx复制const withLogger = (WrappedComponent) => {
return class extends React.Component {
componentDidMount() {
console.log('Component mounted');
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};
10. 从装饰器到AOP
装饰器模式是AOP(面向切面编程)的基础理念。在Spring AOP中,代理对象实际上就是装饰器:
java复制@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logMethodCall(ProceedingJoinPoint pjp) throws Throwable {
// 相当于方法装饰器
System.out.println("Before: " + pjp.getSignature());
Object result = pjp.proceed();
System.out.println("After: " + pjp.getSignature());
return result;
}
}
这种设计让横切关注点(日志、事务、安全)能够与业务逻辑解耦。
