设计模式实战:结构型七君子——适配器、装饰器、代理、外观、享元、组合、桥接
设计模式实战:结构型七君子——适配器、装饰器、代理、外观、享元、组合、桥接
设计模式实战:结构型七君子——适配器、装饰器、代理、外观、享元、组合、桥接是软件工程中解决特定问题的经典方案,它提供了可复用的设计思路和代码结构。
本文深入分析了设计模式实战:结构型七君子——适配器、装饰器、代理、外观、享元、组合、桥接的适用场景、优缺点和实现方式,是提升代码质量的重要参考。
将一个类的接口转换成客户端期望的另一个接口。让原本接口不兼容的类可以一起工作。说白了就是——"转换插头"。
1.2 实际场景:统一多种支付渠道
系统中接入了微信支付、支付宝支付、银联支付,它们的 SDK 接口各不相同。我们用一个适配器统一它们:
// 目标接口:统一的支付接口
public interface PaymentService {
PayResult pay(PayRequest request);
}
// 被适配者:微信支付 SDK(第三方,接口不能改)
public class WeChatPayClient {
public WechatPayResponse doPay(WechatPayRequest req) {
// 微信支付逻辑
return new WechatPayResponse("SUCCESS", "微信支付成功");
}
}
// 被适配者:支付宝 SDK
public class AlipayClient {
public AlipayResult tradePay(AlipayParam param) {
return new AlipayResult("ok", "支付宝支付成功");
}
}
// 适配器:微信支付
public class WeChatPayAdapter implements PaymentService {
private final WeChatPayClient client = new WeChatPayClient();
@Override
public PayResult pay(PayRequest request) {
// 转换请求参数
WechatPayRequest wechatReq = new WechatPayRequest();
wechatReq.setOpenId(request.getUserId());
wechatReq.setAmount(request.getAmount().toString());
wechatReq.setOutTradeNo(request.getOrderId());
// 调用第三方
WechatPayResponse response = client.doPay(wechatReq);
// 转换响应结果
return new PayResult(
"SUCCESS".equals(response.getCode()),
response.getMessage(),
request.getOrderId()
);
}
}
// 适配器:支付宝
public class AlipayAdapter implements PaymentService {
private final AlipayClient client = new AlipayClient();
@Override
public PayResult pay(PayRequest request) {
AlipayParam param = new AlipayParam();
param.setBuyerId(request.getUserId());
param.setTotalAmount(request.getAmount().toString());
param.setOutTradeNo(request.getOrderId());
AlipayResult result = client.tradePay(param);
return new PayResult(
"ok".equals(result.getStatus()),
result.getMsg(),
request.getOrderId()
);
}
}使用:
public class PayController {
private final Map<String, PaymentService> payServices = new HashMap<>();
public PayController() {
payServices.put("wechat", new WeChatPayAdapter());
payServices.put("alipay", new AlipayAdapter());
}
public PayResult pay(String channel, PayRequest request) {
PaymentService service = payServices.get(channel);
if (service == null) {
throw new IllegalArgumentException("不支持的支付渠道: " + channel);
}
return service.pay(request);
}
}1.3 Spring 中的适配器
Spring MVC 的 HandlerAdapter 就是适配器模式的经典应用:
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler);
}不同的 Controller 类型(@RequestMapping 注解、HttpRequestHandler、SimpleController)接口不同,HandlerAdapter 把它们统一适配成相同的调用方式。
1.4 避坑指南
- 适配器是补救措施:如果是新系统,直接统一接口就好,别故意设计成需要适配的
- 适配层次不宜过深:A 适配 B,B 适配 C,C 适配 D——这种链式适配极难维护
- 优先用对象适配器(组合)而非类适配器(继承),更灵活
二、装饰器模式(Decorator)
2.1 什么是装饰器模式
动态地给一个对象添加额外职责,比继承更灵活。核心是"包装"——用同接口的包装类来增强原对象。
2.2 实际场景:IO 流的层层包装
Java IO 是装饰器模式的教科书级案例:
// 最内层是节点流,外层是装饰流
InputStream in = new BufferedInputStream(
new FileInputStream("data.txt")
);2.3 自己实现一个装饰器:增强数据源
// 组件接口
public interface DataSource {
String readData();
void writeData(String data);
}
// 具体组件
public class FileDataSource implements DataSource {
private final String filename;
public FileDataSource(String filename) {
this.filename = filename;
}
@Override
public String readData() {
try {
return Files.readString(Path.of(filename));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void writeData(String data) {
try {
Files.writeString(Path.of(filename), data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 装饰器基类
public class DataSourceDecorator implements DataSource {
protected final DataSource wrappee;
public DataSourceDecorator(DataSource wrappee) {
this.wrappee = wrappee;
}
@Override
public String readData() {
return wrappee.readData();
}
@Override
public void writeData(String data) {
wrappee.writeData(data);
}
}
// 具体装饰器:加密
public class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource wrappee) {
super(wrappee);
}
@Override
public void writeData(String data) {
super.writeData(encrypt(data)); // 写入前加密
}
@Override
public String readData() {
return decrypt(super.readData()); // 读取后解密
}
private String encrypt(String data) {
return new String(Base64.getEncoder().encode(data.getBytes()));
}
private String decrypt(String data) {
return new String(Base64.getDecoder().decode(data));
}
}
// 具体装饰器:压缩
public class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource wrappee) {
super(wrappee);
}
@Override
public void writeData(String data) {
super.writeData(compress(data)); // 写入前压缩
}
@Override
public String readData() {
return decompress(super.readData()); // 读取后解压
}
private String compress(String data) {
// 简化示例,实际用 GZIP
return "[COMPRESSED]" + data;
}
private String decompress(String data) {
return data.replace("[COMPRESSED]", "");
}
}使用——可以任意组合装饰器:
// 只加密
DataSource encrypted = new EncryptionDecorator(new FileDataSource("secret.txt"));
encrypted.writeData("hello world");
// 先压缩再加密
DataSource compressedAndEncrypted = new EncryptionDecorator(
new CompressionDecorator(new FileDataSource("secret.txt"))
);
compressedAndEncrypted.writeData("hello world");2.4 装饰器 vs 代理
两者结构几乎一样(都是包装),但意图不同:
| 维度 | 装饰器 | 代理 |
|---|---|---|
| 意图 | 增强/添加功能 | 控制访问 |
| 调用方 | 知道有装饰器,主动包装 | 不一定知道是代理 |
| 关注点 | 给对象加料 | 管理对象访问 |
三、代理模式(Proxy)
3.1 什么是代理模式
为其他对象提供一种代理以控制对这个对象的访问。典型应用:远程代理、虚拟代理、保护代理、智能引用。
3.2 静态代理
public interface UserService {
User getUser(Long id);
}
public class UserServiceImpl implements UserService {
@Override
public User getUser(Long id) {
System.out.println("查询数据库");
return new User(id, "张三");
}
}
public class UserServiceProxy implements UserService {
private final UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public User getUser(Long id) {
// 前置增强
System.out.println("[日志] 开始查询用户, id=" + id);
long start = System.currentTimeMillis();
User user = target.getUser(id);
// 后置增强
long cost = System.currentTimeMillis() - start;
System.out.println("[日志] 查询完成, 耗时=" + cost + "ms");
return user;
}
}使用:
UserService service = new UserServiceProxy(new UserServiceImpl());
User user = service.getUser(1L);缺点:每个目标类都要手写代理类,爆炸。
3.3 JDK 动态代理
public class LogInvocationHandler implements InvocationHandler {
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[日志] 调用 " + method.getName() + ", 参数: " + Arrays.toString(args));
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long cost = System.currentTimeMillis() - start;
System.out.println("[日志] " + method.getName() + " 完成, 耗时=" + cost + "ms");
return result;
}
}使用:
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogInvocationHandler(target)
);
proxy.getUser(1L);JDK 动态代理要求目标类必须实现接口。
3.4 CGLIB 动态代理
public class LogInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("[日志] 调用 " + method.getName());
long start = System.currentTimeMillis();
Object result = proxy.invokeSuper(obj, args);
System.out.println("[日志] 耗时=" + (System.currentTimeMillis() - start) + "ms");
return result;
}
}使用:
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class); // 没有接口也能代理
enhancer.setCallback(new LogInterceptor());
UserService proxy = (UserService) enhancer.create();
proxy.getUser(1L);CGLIB 通过生成子类来实现代理,不需要接口,但不能代理 final 类/方法。
3.5 Spring AOP 的代理选择
Spring 动态选择代理方式:
目标类实现了接口 → 默认使用 JDK 动态代理
目标类没有接口 → 使用 CGLIB 代理
强制使用 CGLIB → 设置 proxyTargetClass = trueSpring Boot 2.x 之后默认使用 CGLIB(spring.aop.proxy-target-class=true)。
3.6 避坑指南
| 坑 | 说明 | 解决方案 |
|---|---|---|
| JDK 代理需要接口 | 没实现接口的类不能用 JDK 代理 | 用 CGLIB |
| CGLIB 不能代理 final | final 类/方法无法被继承 | 避免在代理目标上用 final |
| 内部调用不走代理 | methodA() 调 this.methodB(),B 的增强不生效 | 注入自身代理或用 AopContext |
| 代理对象 equals 问题 | 代理后 equals 判断可能出问题 | 用业务唯一标识判断 |
内部调用不走代理的经典坑:
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 业务逻辑
this.sendNotification(order); // ⚠️ 直接 this 调用,不走代理!
}
@Async // 这个注解不会生效!
public void sendNotification(Order order) {
// 发送通知
}
}解决方案:
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身代理
@Transactional
public void createOrder(Order order) {
self.sendNotification(order); // 通过代理调用,@Async 生效
}
@Async
public void sendNotification(Order order) {
// ...
}
}四、外观模式(Facade)
4.1 什么是外观模式
为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,让子系统更容易使用。
4.2 实际场景:下单流程
下单涉及多个子系统:库存、优惠券、支付、物流。给前端一个简单接口:
// 子系统:库存服务
public class InventoryService {
public boolean deduct(String sku, int qty) {
System.out.println("扣减库存: " + sku + " x " + qty);
return true;
}
}
// 子系统:优惠券服务
public class CouponService {
public void useCoupon(String couponId) {
System.out.println("使用优惠券: " + couponId);
}
}
// 子系统:支付服务
public class PaymentService {
public PayResult pay(String orderId, BigDecimal amount) {
System.out.println("支付: " + orderId + ", 金额: " + amount);
return new PayResult(true, "支付成功", orderId);
}
}
// 子系统:物流服务
public class LogisticsService {
public String createShipment(String orderId, String address) {
System.out.println("创建物流单: " + orderId);
return "SF" + System.currentTimeMillis();
}
}
// 外观类
public class OrderFacade {
private final InventoryService inventoryService = new InventoryService();
private final CouponService couponService = new CouponService();
private final PaymentService paymentService = new PaymentService();
private final LogisticsService logisticsService = new LogisticsService();
public OrderResult placeOrder(OrderRequest request) {
// 1. 扣减库存
inventoryService.deduct(request.getSku(), request.getQuantity());
// 2. 使用优惠券
if (request.getCouponId() != null) {
couponService.useCoupon(request.getCouponId());
}
// 3. 支付
PayResult payResult = paymentService.pay(request.getOrderId(), request.getAmount());
if (!payResult.isSuccess()) {
return OrderResult.fail("支付失败");
}
// 4. 创建物流单
String trackingNo = logisticsService.createShipment(
request.getOrderId(), request.getAddress()
);
return OrderResult.success(request.getOrderId(), trackingNo);
}
}前端只需要调一个方法:
OrderFacade facade = new OrderFacade();
OrderResult result = facade.placeOrder(request);4.3 Spring 中的外观模式
JdbcTemplate是对 JDBC 的外观封装RestTemplate是对 HttpClient 的外观封装- Spring Boot 的
ApplicationContext本身就是外观,统一管理所有 Bean
4.4 避坑指南
- 外观不是万能封装:不要把所有逻辑塞进 Facade,它只做编排,不做业务
- 保留子系统接口:高级用户仍可直接访问子系统做精细控制
五、享元模式(Flyweight)
5.1 什么是享元模式
运用共享技术有效地支持大量细粒度的对象。核心是"共享"——把不变的部分抽出来共享,变化的部分作为外部状态传入。
5.2 实际场景:线程池中的线程
Java 的 Integer 缓存就是享元模式:
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(a == b); // true,用了缓存
Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println(c == d); // false,超出缓存范围5.3 实现享元:连接池
public class ConnectionPool {
private final List<Connection> pool = new ArrayList<>();
private final int maxSize;
public ConnectionPool(int maxSize) {
this.maxSize = maxSize;
for (int i = 0; i < maxSize; i++) {
pool.add(createConnection());
}
}
private Connection createConnection() {
try {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public synchronized Connection getConnection() {
if (pool.isEmpty()) {
throw new RuntimeException("连接池已满");
}
return pool.remove(pool.size() - 1);
}
public synchronized void releaseConnection(Connection conn) {
pool.add(conn);
}
}5.4 避坑指南
- 线程安全:共享对象必须线程安全,或用线程池/ThreadLocal 隔离
- 区分内外状态:内部状态(不变、可共享)vs 外部状态(变化、不共享)
- 别过度优化:对象不多时不需要享元,过早优化是万恶之源
六、组合模式(Composite)
6.1 什么是组合模式
将对象组合成树形结构以表示"部分-整体"的层次。组合模式让用户对单个对象和组合对象的使用具有一致性。
6.2 实际场景:组织架构树
// 组件接口
public interface Organization {
String getName();
int getHeadcount();
void print(int level);
}
// 叶子节点:员工
public class Employee implements Organization {
private final String name;
public Employee(String name) {
this.name = name;
}
@Override
public String getName() { return name; }
@Override
public int getHeadcount() { return 1; }
@Override
public void print(int level) {
System.out.println(" ".repeat(level) + "👤 " + name);
}
}
// 组合节点:部门
public class Department implements Organization {
private final String name;
private final List<Organization> children = new ArrayList<>();
public Department(String name) {
this.name = name;
}
public void add(Organization org) {
children.add(org);
}
public void remove(Organization org) {
children.remove(org);
}
@Override
public String getName() { return name; }
@Override
public int getHeadcount() {
return children.stream().mapToInt(Organization::getHeadcount).sum();
}
@Override
public void print(int level) {
System.out.println(" ".repeat(level) + "📁 " + name + " (" + getHeadcount() + "人)");
children.forEach(child -> child.print(level + 1));
}
}使用:
Department techDept = new Department("技术部");
techDept.add(new Employee("张三"));
techDept.add(new Employee("李四"));
Department productDept = new Department("产品部");
productDept.add(new Employee("王五"));
productDept.add(new Employee("赵六"));
Department company = new Department("我的公司");
company.add(techDept);
company.add(productDept);
company.print(0);
// 📁 我的公司 (4人)
// 📁 技术部 (2人)
// 👤 张三
// 👤 李四
// 📁 产品部 (2人)
// 👤 王五
// 👤 赵六6.3 MyBatis 中的组合模式
MyBatis 的 SqlNode 体系就是组合模式:
// MixedSqlNode 是组合节点,包含子 SqlNode 列表
// TextSqlNode、IfSqlNode 是叶子节点
// 解析时统一调用 apply() 方法,不需要区分是单个还是组合6.4 避坑指南
- 叶子节点和组合节点实现同一接口:某些方法对叶子节点无意义,可抛 UnsupportedOperationException
- 注意循环引用:树结构中 A 包含 B,B 又包含 A 会死循环
七、桥接模式(Bridge)
7.1 什么是桥接模式
将抽象部分与它的实现部分分离,使它们都可以独立变化。核心是"组合优于继承"——用组合关系代替多层继承。
7.2 实际场景:消息发送器
消息可以按类型分(普通消息、加急消息、紧急消息),也可以按渠道分(邮件、短信、钉钉)。如果用继承,需要 3×3=9 个类。用桥接只需 3+3=6 个:
// 实现者接口:消息渠道
public interface MessageChannel {
void send(String to, String content);
}
// 具体实现:邮件渠道
public class EmailChannel implements MessageChannel {
@Override
public void send(String to, String content) {
System.out.println("📧 发送邮件到 " + to + ": " + content);
}
}
// 具体实现:短信渠道
public class SmsChannel implements MessageChannel {
@Override
public void send(String to, String content) {
System.out.println("📱 发送短信到 " + to + ": " + content);
}
}
// 具体实现:钉钉渠道
public class DingTalkChannel implements MessageChannel {
@Override
public void send(String to, String content) {
System.out.println("💬 发送钉钉到 " + to + ": " + content);
}
}
// 抽象:消息类型(持有渠道引用——这就是"桥")
public abstract class Message {
protected MessageChannel channel; // 桥接的引用
public Message(MessageChannel channel) {
this.channel = channel;
}
public abstract void send(String to, String content);
}
// 扩展抽象:普通消息
public class NormalMessage extends Message {
public NormalMessage(MessageChannel channel) {
super(channel);
}
@Override
public void send(String to, String content) {
channel.send(to, content);
}
}
// 扩展抽象:加急消息
public class UrgentMessage extends Message {
public UrgentMessage(MessageChannel channel) {
super(channel);
}
@Override
public void send(String to, String content) {
channel.send(to, "【加急】" + content);
}
}
// 扩展抽象:紧急消息
public class CriticalMessage extends Message {
public CriticalMessage(MessageChannel channel) {
super(channel);
}
@Override
public void send(String to, String content) {
channel.send(to, "🚨【紧急】" + content + " 🚨");
}
}使用——两个维度独立变化:
// 邮件发普通消息
Message msg1 = new NormalMessage(new EmailChannel());
msg1.send("zhangsan@example.com", "系统升级通知");
// 短信发紧急消息
Message msg2 = new CriticalMessage(new SmsChannel());
msg2.send("13800138000", "数据库宕机");
// 钉钉发加急消息
Message msg3 = new UrgentMessage(new DingTalkChannel());
msg3.send("张三", "请尽快审批");7.3 JDBC 中的桥接模式
JDBC 就是桥接模式的经典应用:
// 抽象:DriverManager
// 实现:各数据库驱动(MySQL Driver、Oracle Driver、PostgreSQL Driver)
// 两者通过 Driver 接口桥接,可以独立变化
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db");
// 换 Oracle 只需换 URL 和驱动,应用代码不变7.4 避坑指南
- 识别多维度变化:有两个以上独立变化的维度时才考虑桥接
- 不要为了用而用:只有一个变化维度时用继承就够了
八、七种模式对比总结
| 模式 | 核心问题 | 一句话总结 | 框架应用 |
|---|---|---|---|
| 适配器 | 接口不兼容 | 转换插头 | Spring HandlerAdapter |
| 装饰器 | 动态增强功能 | 套娃包装 | Java IO 流 |
| 代理 | 控制访问 | 替身使者 | Spring AOP |
| 外观 | 简化复杂子系统 | 统一入口 | JdbcTemplate |
| 享元 | 大量细粒度对象 | 共享复用 | Integer 缓存、连接池 |
| 组合 | 树形结构 | 部分整体一致 | MyBatis SqlNode |
| 桥接 | 多维度变化 | 组合代替继承 | JDBC |
九、面试要点
高频面试题
Q1:装饰器模式和代理模式的区别?
答:结构相似但意图不同。装饰器关注"增强功能",调用方知道有装饰器并主动包装;代理关注"控制访问",调用方通常不感知代理的存在。装饰器通常多层嵌套,代理通常一层。
Q2:JDK 动态代理和 CGLIB 的区别?
答:JDK 动态代理通过实现接口生成代理类,要求目标类必须实现接口;CGLIB 通过生成子类实现代理,不要求接口但不能代理 final 类/方法。Spring AOP 默认有接口用 JDK 代理,没接口用 CGLIB;Spring Boot 2.x 后默认 CGLIB。
Q3:Spring AOP 中为什么内部调用不走代理?
答:this.method() 中的 this 是目标对象本身而非代理对象,所以增强逻辑不会执行。解决方式:注入自身代理(@Autowired private XxxService self)或使用 AopContext.currentProxy()。
Q4:适配器模式和装饰器模式的区别?
答:适配器改变接口(让不兼容的接口协作),装饰器不改变接口(只增强功能)。适配器是"翻译",装饰器是"加料"。
Q5:桥接模式和策略模式的区别?
答:桥接模式关注"抽象和实现两个维度独立变化",是结构性组合;策略模式关注"算法可替换",是行为型替换。桥接的"桥"是持久性的结构关系,策略的切换是运行时的行为选择。
面试加分项
- 能结合 Spring 源码说出具体模式的应用位置
- 能画出 JDK 动态代理的调用链路
- 知道 Spring Boot 2.x 后默认 CGLIB 的原因
- 能解释内部调用不走代理的原理和解决方案
总结
结构型模式的核心是"怎么组合更优雅":
- 适配器:转换不兼容接口
- 装饰器:动态增强,套娃包装
- 代理:控制访问,AOP 的基础
- 外观:简化复杂系统,统一入口
- 享元:共享复用,减少对象数量
- 组合:树形结构,部分整体一致
- 桥接:多维度解耦,组合优于继承
实际开发中最常用的是代理模式(AOP 基础)和装饰器模式(IO 流),其次是适配器(对接第三方)和外观(Facade 编排)。理解这些模式后再读 Spring 源码,会有豁然开朗的感觉。