目录
[1. 定义统一接口](#1. 定义统一接口)
[2. 实现核心对象](#2. 实现核心对象)
[3. 抽象包装器](#3. 抽象包装器)
[4. 具体包装器](#4. 具体包装器)
[5. 客户端调用](#5. 客户端调用)
[四、Java 源码里的实际案例](#四、Java 源码里的实际案例)
在设计模式的标准目录里,包装器模式通常被称为装饰器模式(Decorator Pattern)。不管叫哪个名字,核心思想只有一个:像给礼物包包装纸一样,给对象动态添加功能,而不需要动原来的代码。
一、先说为什么要用它
假设你在做一个咖啡订购系统,最开始只有黑咖啡:
java
public class BlackCoffee {
public double cost() { return 5.0; }
public String desc() { return "黑咖啡"; }
}
后来要加牛奶、加糖、加摩卡酱。用继承来实现的话,类会长这样:
BlackCoffee
MilkCoffee
SugarCoffee
MilkSugarCoffee
MochaMilkSugarCoffee
...
10 种配料,理论上就有 2¹⁰ 种组合,也就是 1024 个类。更麻烦的是,哪天"牛奶"涨价,你可能要翻遍所有涉及牛奶的子类挨个改。
这个问题的根源在于:用继承来表达"功能叠加"本质上是错的。功能叠加是运行时的行为,继承是编译时固定的结构,两者天生不搭。
包装器模式的口号就是:组合优于继承。
二、核心原理
理解包装器模式,用"俄罗斯套娃"来比喻最直观:
- 最里面是核心对象;
- 外面套一个包装器,它和核心对象长得一样(实现相同接口),但手里握着核心对象的引用;
- 包装器在调用核心对象前后,可以加自己的逻辑;
- 包装器外面,还可以再套一层。
调用链长这样:
客户调用 → [ 糖包装器 → [ 奶包装器 → [ 黑咖啡 ] ] ]
每一层都只管自己那一点事,职责清晰,随意拆装。
三、代码实现
1. 定义统一接口
不管是原始咖啡还是加了料的咖啡,对外暴露的行为必须一致。
java
public interface Coffee {
double getCost();
String getDescription();
}
2. 实现核心对象
java
public class BlackCoffee implements Coffee {
@Override
public double getCost() { return 5.0; }
@Override
public String getDescription() { return "黑咖啡"; }
}
3. 抽象包装器
这是整个模式的关键,它同时做两件事:实现 Coffee 接口,并且持有一个 Coffee 对象的引用。
java
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
默认行为是直接委托给内部对象,子类只需要覆盖自己关心的方法。
4. 具体包装器
加牛奶:
java
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return super.getCost() + 2.0;
}
@Override
public String getDescription() {
return super.getDescription() + ", 牛奶";
}
}
加糖:
java
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return super.getCost() + 1.0;
}
@Override
public String getDescription() {
return super.getDescription() + ", 糖";
}
}
5. 客户端调用
java
public class Shop {
public static void main(String[] args) {
// 只要黑咖啡
Coffee order1 = new BlackCoffee();
System.out.println(order1.getDescription() + " : $" + order1.getCost());
// 黑咖啡 + 牛奶
Coffee order2 = new MilkDecorator(new BlackCoffee());
System.out.println(order2.getDescription() + " : $" + order2.getCost());
// 黑咖啡 + 牛奶 + 糖
Coffee order3 = new SugarDecorator(new MilkDecorator(new BlackCoffee()));
System.out.println(order3.getDescription() + " : $" + order3.getCost());
}
}
输出:
黑咖啡 : $5.0
黑咖啡, 牛奶 : $7.0
黑咖啡, 牛奶, 糖 : $8.0
10 种配料,现在只需要 10 个包装器类,想怎么组合怎么组合。
四、Java 源码里的实际案例
这不是玩具代码,Java 核心库里到处都在用这个结构。
Java IO 流是最典型的例子。你一定写过这样的代码:
java
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt")
)
);
拆开看:FileInputStream 是核心对象,只管读字节;InputStreamReader 包了一层,把字节转成字符;BufferedReader 再包一层,加上缓冲提高效率。三层套娃,每层职责单一,但组合起来功能完整。
Spring 里的 Request 包装也是同理。如果要在 Filter 里统一过滤 XSS,通常这样写:
java
HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) {
@Override
public String getParameter(String name) {
return super.getParameter(name).replaceAll("<", "<");
}
};
Spring 提供 HttpServletRequestWrapper 这个基类,就是把抽象包装器这一层帮你写好了,直接继承用就行。
五、和代理模式的区别
结构上两者几乎一样,都持有对象引用,也都实现相同接口,所以很多人会搞混。
区别在于目的:
- 包装器(Decorator) 是为了增强功能,关心的是"这个对象能做什么",比如给咖啡加糖、给 IO 加缓冲;
- 代理(Proxy) 是为了控制访问,关心的是"这个对象能不能被调用、怎么被调用",比如权限校验、事务管理、远程调用。
一句话区分:装饰器让对象更强,代理让对象更可控。
六、优缺点
优点很明显:不用为每个功能组合单独写子类,运行时可以随意叠加,新增功能也不需要改老代码,符合开闭原则。
缺点也值得注意。层层嵌套导致调试时堆栈很深,出错了要一层一层往里找。初始化代码里会出现 new A(new B(new C())) 这种连续嵌套,可读性一般。另外,包装之后的对象做 instanceof 检查会失效,如果代码里有这种判断要小心。
七、结构总结
[ 接口 Interface ] ← 客户端只依赖这个
↑
├── [ 核心实现 ] ← 最里面的娃娃
│
└── [ 抽象包装器 ] ← 持有 Interface 引用
↑
├── [ 具体包装器 A ] ← 增强功能 A
└── [ 具体包装器 B ] ← 增强功能 B
调用链:客户端 → 包装器 B → 包装器 A → 核心实现
下次如果你发现自己在写 CoffeeWithMilkAndSugarAndMocha 这种类名,停一下,这个模式就该派上用场了。