结构型补充 — 外观、桥接、组合、享元模式¶
一句话记忆口诀:外观简化入口、桥接分离维度、组合统一树形、享元共享复用。
一、外观模式(Facade Pattern)¶
1. 引入:它解决了什么问题?¶
当一个子系统包含多个复杂的类和接口时,客户端需要了解每个类的细节才能完成操作:
// ❌ 反例:客户端直接调用多个子系统,耦合度极高
public class OrderService {
public void placeOrder(Order order) {
InventorySystem inventory = new InventorySystem();
inventory.checkStock(order.getProductId());
inventory.reserveStock(order.getProductId(), order.getQuantity());
PaymentSystem payment = new PaymentSystem();
payment.validateCard(order.getCardNumber());
payment.charge(order.getAmount());
ShippingSystem shipping = new ShippingSystem();
shipping.createShipment(order);
NotificationSystem notification = new NotificationSystem();
notification.sendEmail(order.getUserEmail(), "订单已创建");
}
}
问题根因:客户端与子系统的多个类直接耦合,调用逻辑分散,维护成本高。
2. 类比与定义¶
生活类比:酒店前台就是外观模式。住客不需要分别联系客房部、餐饮部、保洁部,只需要打电话给前台,前台协调所有部门完成服务。
外观模式为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。
3. 原理与实现¶
// ===== 外观类:统一入口,封装子系统调用逻辑 =====
public class OrderFacade {
private final InventorySystem inventory;
private final PaymentSystem payment;
private final ShippingSystem shipping;
private final NotificationSystem notification;
public OrderFacade(InventorySystem inventory, PaymentSystem payment,
ShippingSystem shipping, NotificationSystem notification) {
this.inventory = inventory;
this.payment = payment;
this.shipping = shipping;
this.notification = notification;
}
public boolean placeOrder(Order order) {
if (!inventory.checkStock(order.getProductId())) return false;
inventory.reserveStock(order.getProductId(), order.getQuantity());
payment.charge(order.getAmount());
shipping.createShipment(order);
notification.sendEmail(order.getUserEmail(), "订单已创建");
return true;
}
}
// ===== 客户端调用 =====
OrderFacade facade = new OrderFacade(inventory, payment, shipping, notification);
facade.placeOrder(order); // 一行代码搞定!
4. 在 Spring / JDK 中的应用¶
| 框架/类 | 说明 |
|---|---|
JdbcTemplate | 封装了 Connection、Statement、ResultSet 的复杂操作 |
SLF4J | 日志门面,统一 Log4j、Logback 等日志框架的接口 |
RestTemplate | 封装了 HTTP 连接、序列化、错误处理等细节 |
Spring ApplicationContext | 统一了 BeanFactory、ResourceLoader、EventPublisher 等子系统 |
5. 常见误区¶
- 误区:外观类变成"上帝类" → 外观类只做编排和委托,不包含业务逻辑
- 误区:有了外观就不能直接访问子系统 → 外观是可选的便捷入口,不阻止直接访问
二、桥接模式(Bridge Pattern)¶
1. 引入:它解决了什么问题?¶
当一个类存在两个或多个独立变化的维度时,使用继承会导致类爆炸:
// ❌ 反例:形状 × 颜色 = 类爆炸
// 2 种形状 × 3 种颜色 = 6 个类;新增 1 种形状要新增 3 个类!
class RedCircle extends Circle { }
class BlueCircle extends Circle { }
class GreenCircle extends Circle { }
class RedRectangle extends Rectangle { }
class BlueRectangle extends Rectangle { }
class GreenRectangle extends Rectangle { }
问题根因:多个维度的变化通过继承组合,子类数量呈笛卡尔积增长。
2. 类比与定义¶
生活类比:遥控器(抽象)和电视机(实现)。遥控器有基础版和高级版,电视有索尼和三星。两个维度独立变化,通过"遥控器持有电视引用"来桥接。
桥接模式将抽象部分与实现部分分离,使它们都可以独立变化。
3. 原理与实现¶
// ===== 实现维度:颜色接口 =====
public interface Color {
String fill(String shape);
}
public class Red implements Color {
@Override
public String fill(String shape) { return "红色的" + shape; }
}
public class Blue implements Color {
@Override
public String fill(String shape) { return "蓝色的" + shape; }
}
// ===== 抽象维度:形状 =====
public abstract class Shape {
protected Color color; // 桥接:持有实现维度的引用
public Shape(Color color) { this.color = color; }
public abstract void draw();
}
public class Circle extends Shape {
private double radius;
public Circle(double radius, Color color) {
super(color);
this.radius = radius;
}
@Override
public void draw() {
System.out.println(color.fill("圆形") + ",半径=" + radius);
}
}
// ===== 使用:两个维度自由组合 =====
Shape redCircle = new Circle(5.0, new Red());
Shape blueCircle = new Circle(3.0, new Blue());
// 新增绿色?只需新增 Green 类,不影响任何 Shape 子类!
4. 在 Spring / JDK 中的应用¶
| 框架/类 | 说明 |
|---|---|
JDBC Driver / Connection | 抽象(JDBC API)与实现(各数据库驱动)分离 |
PlatformTransactionManager | 事务管理抽象与具体实现(JDBC/JPA/JTA)分离 |
| SLF4J + Logback/Log4j | 日志抽象与日志实现分离 |
5. 常见误区¶
- 误区:桥接模式就是"用组合替代继承" → 桥接强调的是两个独立变化的维度通过组合连接
- 误区:桥接模式和策略模式一样 → 策略关注算法可替换性,桥接关注抽象与实现的分离
三、组合模式(Composite Pattern)¶
1. 引入:它解决了什么问题?¶
处理树形结构时,客户端需要区分"叶子节点"和"容器节点",代码充满类型判断:
// ❌ 反例:到处都是 instanceof 判断
public long calculateSize(Object node) {
if (node instanceof File) {
return ((File) node).getSize();
} else if (node instanceof Directory) {
long total = 0;
for (Object child : ((Directory) node).getChildren()) {
total += calculateSize(child);
}
return total;
}
throw new IllegalArgumentException("未知类型");
}
问题根因:叶子节点和容器节点接口不统一,客户端必须区分处理。
2. 类比与定义¶
生活类比:公司组织架构。无论是"部门"还是"员工",都可以统一执行"计算薪资总额"操作。部门的薪资 = 所有子节点薪资之和。
组合模式将对象组合成树形结构以表示"部分-整体"的层次结构,使客户端对单个对象和组合对象的使用具有一致性。
3. 原理与实现¶
// ===== 抽象组件 =====
public abstract class FileSystemNode {
protected String name;
public FileSystemNode(String name) { this.name = name; }
public abstract long getSize();
public abstract void print(String prefix);
}
// ===== 叶子节点:文件 =====
public class FileNode extends FileSystemNode {
private long size;
public FileNode(String name, long size) { super(name); this.size = size; }
@Override
public long getSize() { return size; }
@Override
public void print(String prefix) {
System.out.println(prefix + "📄 " + name + " (" + size + "B)");
}
}
// ===== 容器节点:目录 =====
public class DirectoryNode extends FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
public DirectoryNode(String name) { super(name); }
public void add(FileSystemNode node) { children.add(node); }
public void remove(FileSystemNode node) { children.remove(node); }
@Override
public long getSize() {
return children.stream().mapToLong(FileSystemNode::getSize).sum();
}
@Override
public void print(String prefix) {
System.out.println(prefix + "📁 " + name + " (" + getSize() + "B)");
children.forEach(child -> child.print(prefix + " "));
}
}
// ===== 使用示例 =====
DirectoryNode root = new DirectoryNode("project");
DirectoryNode src = new DirectoryNode("src");
src.add(new FileNode("Main.java", 2048));
src.add(new FileNode("Utils.java", 1024));
root.add(src);
root.add(new FileNode("README.md", 512));
root.print(""); // 统一接口,无需区分文件和目录
4. 在 Spring / JDK 中的应用¶
| 框架/类 | 说明 |
|---|---|
java.awt.Container / Component | Swing 组件树 |
MyBatis SqlNode | 动态 SQL 的 <if>、<choose>、<foreach> 组成树形结构 |
Spring Security AccessDecisionVoter | 投票器组合 |
Jackson JsonNode | ObjectNode 包含子节点,ValueNode 是叶子 |
5. 常见误区¶
- 误区:所有树形结构都要用组合模式 → 只有当需要统一处理叶子和容器时才需要
- 误区:叶子节点也要实现
add()/remove()→ 可以抛出UnsupportedOperationException,或使用"安全组合模式"
四、享元模式(Flyweight Pattern)¶
1. 引入:它解决了什么问题?¶
当系统中存在大量相似对象时,每个对象都独立存储会消耗大量内存:
// ❌ 反例:围棋游戏中每个棋子都是独立对象
// 361 个对象中,颜色/形状/纹理只有 2 种组合,大量内存被浪费
for (int i = 0; i < 361; i++) {
ChessPiece piece = new ChessPiece(color, shape, texture, x, y);
}
问题根因:大量对象中存在重复的内部状态(不变部分),没有被共享复用。
2. 类比与定义¶
生活类比:共享单车。城市中有成千上万的骑行需求,但不需要每人买一辆自行车。共享单车(享元对象)被多人复用,每次骑行的起点终点(外部状态)不同,但车本身(内部状态)是共享的。
享元模式运用共享技术有效地支持大量细粒度对象的复用,将对象的状态分为内部状态(可共享、不变)和外部状态(不可共享、随环境变化)。
3. 原理与实现¶
// ===== 享元接口 =====
public interface ChessPiece {
void draw(int x, int y); // x, y 是外部状态
}
// ===== 具体享元:包含内部状态(可共享) =====
public class ConcreteChessPiece implements ChessPiece {
private final String color; // 内部状态
private final String texture; // 内部状态
public ConcreteChessPiece(String color) {
this.color = color;
this.texture = loadTexture(color);
System.out.println("创建" + color + "棋子(仅创建一次)");
}
@Override
public void draw(int x, int y) {
System.out.printf("在(%d,%d)绘制%s棋子%n", x, y, color);
}
}
// ===== 享元工厂:缓存并复用享元对象 =====
public class ChessPieceFactory {
private static final Map<String, ChessPiece> pieces = new HashMap<>();
public static ChessPiece getChessPiece(String color) {
return pieces.computeIfAbsent(color, ConcreteChessPiece::new);
}
}
// ===== 使用示例 =====
ChessPiece black1 = ChessPieceFactory.getChessPiece("黑"); // 创建
ChessPiece black2 = ChessPieceFactory.getChessPiece("黑"); // 复用!
System.out.println(black1 == black2); // true,同一个对象!
4. 在 Spring / JDK 中的应用¶
| 框架/类 | 说明 |
|---|---|
String 常量池 | 相同字面量的字符串共享同一个对象 |
Integer.valueOf() | -128~127 范围内的 Integer 对象被缓存复用 |
Boolean.valueOf() | TRUE 和 FALSE 两个实例被全局共享 |
| 数据库连接池 | Connection 对象被多个请求复用 |
| 线程池 | Thread 对象被多个任务复用 |
5. 常见误区¶
- 误区:享元模式就是缓存 → 享元强调的是分离内部状态和外部状态,缓存只是实现手段
- 误区:所有重复对象都应该用享元 → 只有当对象数量大、内部状态占比高时才值得使用
// ⚠️ 经典面试题:为什么 Integer 在 -128~127 范围内 == 为 true?
Integer a = 127, b = 127;
System.out.println(a == b); // true —— 享元模式!IntegerCache 缓存
Integer c = 128, d = 128;
System.out.println(c == d); // false —— 超出缓存范围,新对象
// 结论:Integer 比较永远用 equals(),不要用 ==
五、四种模式对比总结¶
| 模式 | 核心思想 | 解决的问题 | 关键词 |
|---|---|---|---|
| 外观模式 | 统一入口 | 子系统复杂,客户端调用困难 | 简化、封装、门面 |
| 桥接模式 | 分离维度 | 多维度变化导致类爆炸 | 抽象与实现分离 |
| 组合模式 | 统一接口 | 树形结构中叶子和容器处理不一致 | 部分-整体、递归 |
| 享元模式 | 共享复用 | 大量相似对象消耗过多内存 | 内部状态、外部状态 |
复习检验标准:能否口述"这个模式解决了什么问题?不用它会怎样?Spring 中哪里用到了?"