静态代理 (Proxy)、装饰器 (Decorator)、适配器 (Adapter)、桥接 (Bridge)详解
静态代理 (Proxy)、装饰器 (Decorator)、适配器 (Adapter)、桥接 (Bridge)详解
总结
他们都是结构性模式
终极四合一对比表(一网打尽)
| 模式 | 引用关系 | 核心目的 | 一句话人话 |
|---|---|---|---|
| 代理 (Proxy) | 引用同类型对象 | 控制访问(拦截、延迟、权限) | “过安检,决定让不让你进” |
| 装饰器 (Decorator) | 引用同类型对象 | 增强功能(叠加额外行为) | “穿装备,一层层变强” |
| 适配器 (Adapter) | 引用不同类型对象 | 接口转换(翻译不兼容的接口) | “带转接头,让美标插头插进国标插座” |
| 桥接 (Bridge) | 引用不同类型对象 | 解耦抽象与实现(双维度独立变化) | “遥控器(抽象)和电视品牌(实现)分开发展” |
辨别
当你看到一段代码,内部持有一个引用时:
- 先看接口:和自己是同一个接口吗?
- 是 → 再看意图:是拦住不让进(代理)?还是加了点料再放行(装饰器)?
- 否 → 再看意图:是为了翻译不兼容的接口(适配器)?还是为了把“做什么”和“怎么做”拆开(桥接)?
- 记住两个死对头:
- 代理 vs 装饰器:结构一样(都同类型),区别在意图(控制 vs 增强)。
- 适配器 vs 桥接:结构一样(都不同类型),区别在时机和维度(事后补救 vs 事前设计;单接口转换 vs 双维度扩展)。
面试加分金句(直接背)
“代理和装饰器不改变接口,但代理管‘能不能用’,装饰器管‘好不好用’;适配器和桥接都涉及接口不兼容,但适配器是‘打补丁’,桥接是‘搭架构’。”
代理(Proxy)
引用的是同类型的对象(实现同一接口),目的是控制访问。
- 引用关系:代理和目标对象实现同一个接口(或继承同一个抽象类),代理内部持有该接口的引用。
- 核心目的:控制对目标对象的访问(如权限校验、延迟加载、日志记录、远程代理),不改变对象行为,只决定“让不让用、什么时候用”。
- 控制性:代理是“守门员”,可以拦截请求、修改请求参数、甚至拒绝访问,但最终目标对象的逻辑不变。
- 典型场景:虚拟代理(大图占位)、保护代理(权限检查)、远程代理(RPC)、Spring AOP 动态代理。
// 代理和目标实现同一接口
interface Image { void display(); }
class HighResImage implements Image {
public HighResImage(String filename) {
loadFromDisk(); // 耗时操作
}
public void display() { /* 显示图片 */ }
}
class ProxyImage implements Image {
private String filename;
private HighResImage realImage; // 同类型引用
public void display() {
if (realImage == null) {
realImage = new HighResImage(filename); // 延迟加载(控制访问时机)
}
realImage.display(); // 委托给原对象
}
}
// 客户端:new ProxyImage("photo.jpg"),访问被代理控制,图片直到真正需要时才加载
装饰器(Decorator)
引用的是同类型的对象(实现同一接口),目的是增强功能。
- 引用关系:装饰器和被装饰对象实现同一个接口(或继承同一个抽象类),装饰器内部持有该接口的引用。
- 核心目的:动态叠加额外职责(如加缓存、加加密、加缓冲),不改变对象本质,只是“穿马甲”增强。
- 嵌套性:可以无限层层包装,每一层都增强一点。
- 典型场景:Java I/O 流(
new BufferedInputStream(new FileInputStream(...)))、Spring 缓存装饰。
// 装饰器和目标实现同一接口
interface DataSource { void write(String data); }
class FileDataSource implements DataSource { ... } // 原始对象
class EncryptionDecorator implements DataSource {
private DataSource wrapped; // 同类型引用
public void write(String data) {
String encrypted = encrypt(data); // 增强功能-前置增强
wrapped.write(encrypted); // 委托给原对象
System.out.println("写入完成:" + data); // 可以增加其他逻辑-后置增强
}
}
// 客户端:new EncryptionDecorator(new FileDataSource()),功能增强但不改接口
适配器(Adapter)
引用的是不同类型的对象(接口不兼容),目的是接口转换。
- 引用关系:适配器实现客户端期望的目标接口,内部持有被适配者(Adaptee)的引用,两者接口不同。
- 核心目的:让两个不兼容的接口能一起工作,做的是“翻译”或“转接头”的工作。
- 事后性:通常是事后补救(两个已存在的模块不兼容,强行搭桥),而不是事先设计。
- 典型场景:电源转接头、不同日志框架统一为 SLF4J、
Arrays.asList()将数组适配为 List。
// 适配器实现目标接口,但持有不同类型的被适配者引用
interface Target { void request(); } // 客户端期望的接口
class Adaptee { void specificRequest() { } } // 已有的不兼容接口
class Adapter implements Target {
private Adaptee adaptee; // 不同类型引用(不是Target类型)
public void request() {
adaptee.specificRequest(); // 翻译调用,接口转换
}
}
// 客户端:new Adapter(new Adaptee()),用Target接口的request方法调用了Adaptee的方法
桥接(Bridge)
引用的是不同类型的对象(实现接口与抽象接口不同),目的是解耦抽象与实现。
- 引用关系:抽象类持有实现接口的引用,两者是不同类型的接口体系(抽象接口 ≠ 实现接口)。
- 核心目的:将抽象部分与实现部分分离,让两者可以独立变化,避免因多维度变化导致类爆炸(M×N → M+N)。
- 事前性:是事先设计的结构型模式,用于应对未来多个维度的扩展,而不是事后打补丁。
- 典型场景:JDBC 驱动(
DriverManager抽象 + 各数据库实现)、不同形状(圆/方)+ 不同颜色(红/蓝)、不同操作系统 + 不同文件系统。
// 抽象和实现是两个独立的接口体系
interface Color { void applyColor(); } // 实现维度
class Red implements Color {
public void applyColor() { System.out.println("红色"); }
}
class Blue implements Color {
public void applyColor() { System.out.println("蓝色"); }
}
abstract class Shape { // 抽象维度
protected Color color; // 不同类型引用(Color 不是 Shape)
public Shape(Color color) { this.color = color; }
abstract void draw();
}
class Circle extends Shape {
public Circle(Color color) { super(color); }
public void draw() {
color.applyColor(); // 委托给实现,形状和颜色独立变化
System.out.println("画圆形");
}
}
class Square extends Shape {
public Square(Color color) { super(color); }
public void draw() {
color.applyColor(); // 委托给实现
System.out.println("画方形");
}
}
// 客户端:new Circle(new Red()),形状和颜色两个维度可以任意组合,无需新增类
桥接和适配器对照表
| 模式 | 代码特征 | 核心判断 | 一句话 |
|---|---|---|---|
| 桥接 | a.method1() 内部调用 b.method2(),method1 和 method2 语义一致,【分工合作:method2是method1需要完成功能的一部分】 |
两个接口天生匹配,无需翻译,只是把任务委托出去(我要画圆draw(),你帮我上色applyColor(),都是“画”这个大的语义范畴) | “我要用你的能力,直接调用” |
| 适配器 | a.method1() 内部调用 b.method2(),method1 和 method2 语义一致,【带转接头:method2和method1功能一致,但是方法名不一样,所以翻译一下】 |
两个接口本来不匹配,适配器做翻译,把 method1 转成 method2 |
“我要用你的能力,但接口对不上,得翻译一下” |
桥接:a.method1() 内部调用 b.method2(),语义一致。
- 【分工合作】:
method2是method1需要完成功能的一小部分(整体与局部的关系)。 - 桥接:
method2是method1为了完成职责,而拆解出来的内部协作部分。
适配器:a.method1() 内部调用 b.method2(),语义一致。
- 【带转接头】:
method2和method1功能完全等价,但方法名/参数不对,所以翻译一下(完全对等的关系)。 - 适配器:
method2是method1为了兼容外部系统,而引用的外部对等功能。
| 模式 | 核心比喻 | 精确的职责描述 |
|---|---|---|
| 桥接 | 分工合作(整体 vs 局部) | method2 是 method1 不可分割的内部实现细节(例如:画圆必须用到上色)。 |
| 适配器 | 带转接头(完全对等) | method2 是来自外部第三方、与 method1 完全对等的功能(例如:我要供电,你也能供电,但你是美标)。 |
适配器和桥接在代码层面可以完全一样,区别只在于"设计意图"和"上下文"。
证明:同一套代码,两种解释
通用代码结构
interface Target {
void doSomething();
}
class Adaptee {
void specificAction() { }
}
class SomeClass implements Target {
private Adaptee adaptee;
public SomeClass(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void doSomething() {
adaptee.specificAction();
}
}
问题: 这段代码是适配器还是桥接?
答案:取决于你怎么说
| 解释角度 | 说它是适配器的理由 | 说它是桥接的理由 |
|---|---|---|
| 接口关系 | Target 和 Adaptee 是两个不相关的系统,接口不兼容,需要翻译 |
Target 是抽象层,Adaptee 是实现层,两者本来就是设计好的两个维度 |
| 设计时机 | 事后补救:两个已存在的模块不兼容,强行搭桥 | 事前设计:架构师在设计时就把"做什么"和"怎么做"分开了 |
| 语义关系 | specificAction() 和 doSomething() 是等价的(都是完成同一个业务目标) |
specificAction() 是 doSomething() 的一部分(分工合作) |
| 扩展方向 | 单维度:适配器只是让一个外部类接入目标接口 | 双维度:Target 和 Adaptee 各自可以有很多子类,独立扩展 |
经典案例:JDBC 是适配器还是桥接?
// Java 代码
Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
官方说法: JDBC 是桥接模式(抽象是 JDBC API,实现是各数据库驱动)
但也可以说是适配器: 因为 MySQL、Oracle、PostgreSQL 的驱动接口完全不同,JDBC 把它们都适配成统一的 Connection、Statement 接口
真相: JDBC 两者都是!
- 从 Sun 公司设计 JDBC 的初衷看 → 桥接(抽象 API 和具体驱动独立发展)
- 从各数据库厂商接入 JDBC 的过程看 → 适配器(把自己的接口翻译成 JDBC 接口)
所以 JDBC 官方文档从不纠结它是什么模式,只说"它是一个分层架构"。
为什么设计模式会有这种"模糊地带"?
1. 模式是"意图",不是"公式"
设计模式不是数学公式,不是"代码长这样 = 模式 X"。它们是沟通工具,用来表达你的设计意图。
// 同一段代码
class X implements A {
private B b;
public void method() {
b.anotherMethod();
}
}
- 如果你说:"我为了解耦抽象和实现" → 桥接
- 如果你说:"我为了兼容第三方的 B 类" → 适配器
代码没变,变的是你的解释。
2. 设计模式可以"演化"
很多设计模式在实际项目中会相互转化:
项目初期:适配器(快速接入第三方)
↓ 演化
项目中期:桥接(抽象和实现独立扩展)
↓ 演化
项目后期:策略模式(实现变成可插拔的算法)
同一个类,在不同阶段可以有不同的模式解读。
某段代码关于它是代理还是装饰器的理解
问题:这个我感觉可以理解成代理也可以理解成装饰器吧,我更倾向装饰器
//抽象角色:租房
public interface Rent {
public void rent();
}
//真实角色: 房东,房东要出租房子
public class Host implements Rent{
public void rent() {
System.out.println("房屋出租");
}
}
//代理角色:中介
public class Proxy implements Rent {
private Host host;
public Proxy() { }
public Proxy(Host host) {
this.host = host;
}
//租房
public void rent(){
seeHouse();
host.rent();
fare();
}
//看房
public void seeHouse(){
System.out.println("带房客看房");
}
//收中介费
public void fare(){
System.out.println("收中介费");
}
}
解答
这段代码既是代理也是装饰器,区别在于你怎么解释它。从业务角色看,中介是房东的代理,控制租客对房东的访问,所以是代理模式;从代码结构看,它持有同类型引用,在租房的逻辑前后增加了额外行为,所以也符合装饰器模式。设计模式的核心是'意图',不是'代码形状'。在实际开发中,一个类同时具备多种模式的特征非常常见。
结论先行
| 视角 | 判断依据 | 结论 |
|---|---|---|
| 从"控制访问"看 | 中介控制了"租客能否见到房东"的入口 | ✅ 代理 |
| 从"增强功能"看 | 看房 + 收中介费 是"租房"行为的增强 | ✅ 装饰器 |
| 从"真实业务"看 | 现实中,中介就是"代理"(委托代理关系) | ✅ 代理更贴切 |
| 从"代码结构"看 | 和装饰器完全一样,都是持有同类型引用并增强 | ✅ 两者都可 |
最精准的说法: 这是代理模式的经典案例,但它的代码结构和装饰器完全相同,区别只在"你怎么解释它"。
1. 为什么说它是代理?(业务语义)
public class Proxy implements Rent {
private Host host;
public void rent() {
seeHouse(); // 前置增强:看房
host.rent(); // 委托给真正的房东
fare(); // 后置增强:收中介费
}
}
代理的理由:
- 控制访问:租客不能直接联系房东,必须通过中介
- 延迟/筛选:中介可以决定"不让某个租客看房"(控制访问权限)
- 现实映射:房地产中介就是典型的"代理"角色
这就是教科书上的"静态代理"案例。
2. 为什么也可以说是装饰器?(代码结构)
interface Rent { void rent(); }
class Host implements Rent { ... }
class Proxy implements Rent {
private Rent wrapped; // 同类型引用
public void rent() {
// 增强逻辑
wrapped.rent(); // 委托
// 增强逻辑
}
}
装饰器的理由:
- ✅ 实现了同一接口(
Rent) - ✅ 持有同类型引用(
Rent wrapped) - ✅ 在调用前后增加了额外功能(看房 + 收中介费)
- ✅ 不改变
Host.rent()的本质行为
从纯代码角度,它和装饰器一模一样。
3. 为什么你"更倾向装饰器"是对的?
你的直觉非常准!因为这段代码的增强性非常明显:
| 增强点 | 说明 |
|---|---|
seeHouse() |
前置增强:租房前先看房 |
host.rent() |
委托给原对象,行为不变 |
fare() |
后置增强:租房后收中介费 |
如果去掉"中介"这个业务背景,只看代码:
- 它就是一个 "增强的租房服务"
- 和
BufferedInputStream装饰FileInputStream没有任何区别
所以你"更倾向装饰器"完全有道理!
4. 那到底算哪个?—— 取决于上下文
| 场景 | 应该叫它什么 | 理由 |
|---|---|---|
| 你在讲"静态代理"的教程 | 代理 | 这是代理模式的经典范例 |
| 你在讲"增强租房功能" | 装饰器 | 它确实在增强 Host.rent() |
| 你在设计模式考试 | 代理 | 因为"中介"的角色就是代理 |
| 你在重构代码 | 装饰器 | 如果后续要嵌套多个增强(如"免中介费装饰器") |
| 你在面试 | 都可以 | 重点说清楚"为什么"(意图决定模式) |
5. 如果你要改成"纯装饰器"风格
// 装饰器风格:把 Host 传进去
class RentDecorator implements Rent {
private Rent wrapped; // 更通用的命名
public RentDecorator(Rent wrapped) {
this.wrapped = wrapped;
}
@Override
public void rent() {
seeHouse();
wrapped.rent();
fare();
}
}
// 客户端:像装饰器一样嵌套
Rent rent = new RentDecorator(new Host());
rent.rent();
这样写之后,它更"像"装饰器了,但业务上它还是代理。
6. 最终判断法则(看意图)
| 判断维度 | 代理 | 装饰器 |
|---|---|---|
| 核心意图 | 控制访问 | 增强功能 |
| 是否拦截 | 是(可以拒绝访问) | 否(不拒绝,只增强) |
| 是否嵌套 | 一般不嵌套 | 可以无限嵌套 |
| 创建目标对象 | 可以延迟创建 | 创建时必须传入 |
| 客户端感知 | 通常无感知 | 客户端知道在装饰 |
在这段代码里:
- 中介可以"拒绝带看"(控制访问) → 代理特征
- 中介的增强是一层层的 → 装饰器特征
- 创建时就传入了
Host→ 装饰器特征
浙公网安备 33010602011771号