Appearance
第76课:结构型模式
🎯 学习目标
- 理解结构型模式解决的是“类和对象如何组合”的问题。
- 掌握适配器、装饰器、代理、外观、组合、桥接、享元模式的核心思想。
- 能识别 Java IO、动态代理、Spring AOP、网关层、树形结构中的结构型模式。
- 能判断组合与继承的取舍。
- 避免把简单调用链包装成过度复杂的模式。
📖 一、结构型模式解决什么问题
业务系统会不断接入新接口、新组件、新协议。结构型模式关注:
text
接口不兼容怎么适配?
不改原类如何增强功能?
如何隐藏复杂子系统?
如何把对象组合成树?
如何用代理控制访问?它们的共同目标是降低耦合,让系统结构更容易扩展。
📖 二、适配器模式
适配器把一个已有接口转换成调用方期望的接口。
场景:系统内部统一使用 PaymentClient,但不同支付 SDK 接口不一致。
java
public interface PaymentClient {
PayResult pay(PayCommand command);
}第三方 SDK:
java
public class LegacyAlipaySdk {
public String doPay(String account, int amountInCent) {
return "SUCCESS";
}
}适配器:
java
public class AlipayAdapter implements PaymentClient {
private final LegacyAlipaySdk sdk;
public AlipayAdapter(LegacyAlipaySdk sdk) {
this.sdk = sdk;
}
@Override
public PayResult pay(PayCommand command) {
String status = sdk.doPay(command.account(), command.amountInCent());
return new PayResult("SUCCESS".equals(status));
}
}适配器适合隔离第三方 SDK,避免外部接口污染内部业务代码。
📖 三、装饰器模式
装饰器在不修改原对象的情况下动态增强功能。
Java IO 是经典例子:
java
InputStream input = new BufferedInputStream(
new FileInputStream("data.txt")
);FileInputStream 提供基本读取能力,BufferedInputStream 增加缓冲能力。
业务示例:
java
public interface MessageSender {
void send(String message);
}
public class SmsSender implements MessageSender {
public void send(String message) {
// 发送短信
}
}
public class LoggingMessageSender implements MessageSender {
private final MessageSender delegate;
public LoggingMessageSender(MessageSender delegate) {
this.delegate = delegate;
}
public void send(String message) {
log.info("send message start");
delegate.send(message);
log.info("send message success");
}
}装饰器强调“增强同一个接口的能力”,增强后仍然可以当作原接口使用。
📖 四、代理模式
代理控制对目标对象的访问,可以做权限、缓存、事务、远程调用等。
java
public interface UserService {
UserDTO getById(Long id);
}
public class UserServiceImpl implements UserService {
public UserDTO getById(Long id) {
return userRepository.findById(id);
}
}代理:
java
public class CachedUserServiceProxy implements UserService {
private final UserService target;
private final Cache<Long, UserDTO> cache;
public UserDTO getById(Long id) {
return cache.get(id, target::getById);
}
}Spring AOP、事务、缓存注解都大量使用代理思想。
java
@Transactional
public void createOrder(OrderCommand command) {
// Spring 通过代理在方法前后开启/提交/回滚事务
}📖 五、外观模式
外观模式为复杂子系统提供一个简单入口。
java
@Service
public class OrderFacade {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final DeliveryService deliveryService;
public OrderResult placeOrder(OrderCommand command) {
inventoryService.reserve(command);
paymentService.pay(command);
deliveryService.createDelivery(command);
return OrderResult.success();
}
}Controller 不应该直接编排大量底层服务:
text
Controller -> Facade -> 多个领域服务/基础服务外观模式能让接口层保持简单,但不要把 Facade 写成上帝类。
📖 六、组合模式
组合模式把对象组织成树,让调用方统一处理单个对象和组合对象。
典型场景:
text
菜单树
组织架构
文件目录
评论树
权限节点
表达式树示例:
java
public interface MenuNode {
String name();
void print(int depth);
}
public class MenuItem implements MenuNode {
private final String name;
public void print(int depth) {
System.out.println(" ".repeat(depth) + name);
}
}
public class MenuGroup implements MenuNode {
private final String name;
private final List<MenuNode> children = new ArrayList<>();
public void add(MenuNode node) {
children.add(node);
}
public void print(int depth) {
System.out.println(" ".repeat(depth) + name);
for (MenuNode child : children) {
child.print(depth + 2);
}
}
}调用方不需要区分叶子节点和组合节点。
📖 七、桥接模式
桥接模式把两个变化维度拆开,避免类爆炸。
场景:通知渠道和消息类型都在变化。
text
渠道:短信、邮件、站内信
类型:验证码、营销、告警如果用继承,可能出现:
text
SmsCodeMessage
SmsMarketingMessage
EmailCodeMessage
EmailMarketingMessage桥接写法:
java
public interface MessageChannel {
void send(String content);
}
public abstract class Message {
protected final MessageChannel channel;
protected Message(MessageChannel channel) {
this.channel = channel;
}
public abstract void send();
}
public class AlertMessage extends Message {
public AlertMessage(MessageChannel channel) {
super(channel);
}
public void send() {
channel.send("[告警] 服务异常");
}
}渠道和消息类型可以独立扩展。
📖 八、享元模式
享元模式复用大量相同或相似的小对象,降低内存占用。
Java 中的例子:
text
String 常量池
Integer 缓存 -128 到 127
线程池复用线程
数据库连接池复用连接业务示例:棋盘上的棋子类型共享。
java
public record PieceType(String name, String color) {
}
public class PieceFactory {
private final Map<String, PieceType> cache = new ConcurrentHashMap<>();
public PieceType get(String name, String color) {
return cache.computeIfAbsent(name + ":" + color, key -> new PieceType(name, color));
}
}享元适合对象数量巨大且内部状态可共享的场景。
⚠️ 九、常见陷阱
1. 适配器里混入业务逻辑
适配器应该转换接口和数据格式,不应该承载复杂业务规则。
2. 代理和装饰器混淆
装饰器强调增强功能,代理强调控制访问。代码结构可能相似,但意图不同。
3. 外观类变成上帝类
Facade 只做编排,不应该吞掉所有业务逻辑。
4. 组合树递归过深
树结构要考虑深度限制、循环引用和批量查询,避免递归栈溢出或 N+1 查询。
🆚 十、Java vs C 对比
| 维度 | C | Java |
|---|---|---|
| 适配 | 函数指针、包装函数 | 接口 + 适配器类 |
| 装饰 | 结构体嵌套、函数包装 | 同接口包装对象 |
| 代理 | 手写转发函数 | 动态代理、CGLIB、Spring AOP |
| 组合 | struct 指针树 | 接口 + List 子节点 |
Java 的接口、多态和动态代理让结构型模式更自然,Spring 框架中尤其常见。
💡 十一、最佳实践
- 接第三方系统时优先使用适配器隔离外部模型。
- 横切增强优先考虑代理或 AOP,不要散落到业务代码里。
- Controller 复杂编排可以引入 Facade,但不要让它变成业务黑洞。
- 树形结构要注意递归深度和查询次数。
- 优先组合而不是继承,除非存在明确的 is-a 关系。
- 设计模式要表达意图,类名可以直接体现 Adapter、Proxy、Facade。
🎓 小结
结构型模式关注“对象如何连接”。它们能让旧接口适配新系统、让对象获得额外能力、让复杂子系统暴露简单入口、让树形结构被统一处理。
学习结构型模式时,不要只背 UML,重点看它如何降低耦合和隔离变化。
🧭 十二、模式选择速查
| 问题 | 优先考虑 |
|---|---|
| 第三方接口和内部接口不一致 | 适配器 |
| 不改原类但要增强功能 | 装饰器 |
| 要控制访问、事务、缓存、权限 | 代理 |
| Controller 调用链太复杂 | 外观 |
| 菜单、组织、评论等树结构 | 组合 |
| 两个维度都在变化 | 桥接 |
| 大量重复小对象占内存 | 享元 |
判断时先问“变化点是什么”。如果没有变化点,只是为了让代码看起来高级,就不需要模式。
📌 十三、Spring 中的结构型模式
Spring 项目里经常能看到这些模式:
text
HandlerAdapter 适配器
BeanPostProcessor 装饰/扩展思想
Transactional Proxy 代理
Cache Proxy 代理
Spring MVC Dispatcher 外观
Security FilterChain 责任链,但结构上也有组合示例:@Transactional 不是魔法。调用方以为自己在调用 Service,实际调用的是 Spring 生成的代理对象。代理对象在方法前后开启事务、提交事务或回滚事务。
这也是为什么同一个类内部自调用事务方法可能失效:
java
public void outer() {
inner(); // 没有经过代理
}
@Transactional
public void inner() {
}理解代理模式能帮助你理解很多 Spring 行为。
🔍 十四、自测问题
text
适配器和外观有什么区别?
装饰器和代理代码结构相似,意图区别是什么?
Java IO 为什么是装饰器模式的经典例子?
Spring AOP 为什么属于代理思想?
组合模式适合哪些树形业务?
桥接模式解决什么类爆炸问题?
享元模式为什么要区分内部状态和外部状态?
为什么 Facade 不应该变成上帝类?