结构型模式(下)——外观、桥接、组合与享元
摘要
结构型模式的后半部分涵盖四个各有侧重的经典模式。外观模式(Facade) 是最易理解、也最常被忽视的模式——它不神秘,就是为复杂子系统提供一个简化的统一入口,用一个高层接口屏蔽内部复杂性。大量”Service 层”的价值实际上就是外观模式。桥接模式(Bridge) 解决的是一个深刻的架构问题:当一个抽象有多个维度都会独立变化时,继承会导致组合爆炸,桥接通过将”抽象”与”实现”分离为两个独立的继承层次,让它们可以独立扩展——JDBC 驱动架构是教科书级的例子。组合模式(Composite) 让客户端以统一的方式处理单个对象和对象组合(树形结构),是处理文件系统、组织架构、UI 组件树等树形数据的标准方案。享元模式(Flyweight) 通过共享大量细粒度对象的内部公共状态来节省内存,Java 的字符串常量池、Integer.valueOf() 缓存是经典实现。四个模式均以”不这样做会怎样”为驱动,深度剖析动机与边界。
第 1 章 外观模式(Facade)
1.1 动机:复杂子系统的简化入口
外观模式(Facade Pattern)的定义极简:为子系统中的一组接口提供一个一致的高层接口,使子系统更容易使用。
这个模式的动机来自一个普遍的现实:一个系统内部往往由多个相互协作的子系统构成,这些子系统有各自的职责和接口。当外部调用者需要使用这些子系统时,必须了解每个子系统的细节,并按照正确的顺序协调它们——这对调用者来说是巨大的心智负担,也将调用者与子系统内部结构耦合在了一起。
来看一个”订单下单”的例子。在没有外观的情况下,控制器需要知道并协调所有子系统:
// 没有外观:控制器直接操控所有子系统,复杂且耦合
@RestController
public class OrderController {
@Autowired private InventoryService inventoryService;
@Autowired private PricingService pricingService;
@Autowired private CouponService couponService;
@Autowired private PaymentService paymentService;
@Autowired private OrderRepository orderRepository;
@Autowired private NotificationService notificationService;
@Autowired private PointsService pointsService;
@PostMapping("/orders")
public OrderResponse createOrder(@RequestBody CreateOrderRequest req) {
// 控制器必须知道完整的下单流程细节
// 1. 校验库存
if (!inventoryService.checkAvailable(req.getProductId(), req.getQuantity())) {
throw new InsufficientInventoryException();
}
// 2. 计算价格
BigDecimal price = pricingService.calculatePrice(req.getProductId(), req.getQuantity());
// 3. 应用优惠券
if (req.getCouponCode() != null) {
price = couponService.apply(req.getCouponCode(), price);
couponService.markUsed(req.getCouponCode(), req.getUserId());
}
// 4. 扣减库存
inventoryService.deduct(req.getProductId(), req.getQuantity());
// 5. 发起支付
PaymentResult paymentResult = paymentService.charge(req.getUserId(), price);
// 6. 创建订单记录
Order order = orderRepository.save(Order.create(req, price, paymentResult));
// 7. 发送通知
notificationService.sendOrderConfirmation(req.getUserId(), order);
// 8. 累积积分
pointsService.addPoints(req.getUserId(), price);
return OrderResponse.from(order);
}
}这个控制器违反了 SRP:它既负责 HTTP 请求处理,又包含了完整的业务流程编排逻辑。更糟糕的是,如果”下单流程”在其他地方(如批量下单 API、内部运营工具)也需要复用,所有这些步骤都要重复一遍。
1.2 外观模式的结构
引入外观,把下单流程的编排逻辑收拢到一个专门的类中:
// 外观类:封装下单流程的完整编排逻辑
@Service
public class OrderFacade {
@Autowired private InventoryService inventoryService;
@Autowired private PricingService pricingService;
@Autowired private CouponService couponService;
@Autowired private PaymentService paymentService;
@Autowired private OrderRepository orderRepository;
@Autowired private NotificationService notificationService;
@Autowired private PointsService pointsService;
// 对外提供简洁的高层接口
@Transactional
public Order createOrder(CreateOrderRequest request) {
// 所有下单细节封装在这里,调用方无需知道
validateInventory(request);
BigDecimal finalPrice = calculateFinalPrice(request);
inventoryService.deduct(request.getProductId(), request.getQuantity());
PaymentResult payment = paymentService.charge(request.getUserId(), finalPrice);
Order order = orderRepository.save(Order.create(request, finalPrice, payment));
postOrderCreation(request.getUserId(), order, finalPrice);
return order;
}
private void validateInventory(CreateOrderRequest req) {
if (!inventoryService.checkAvailable(req.getProductId(), req.getQuantity())) {
throw new InsufficientInventoryException(req.getProductId());
}
}
private BigDecimal calculateFinalPrice(CreateOrderRequest req) {
BigDecimal price = pricingService.calculatePrice(req.getProductId(), req.getQuantity());
if (req.getCouponCode() != null) {
price = couponService.apply(req.getCouponCode(), price);
couponService.markUsed(req.getCouponCode(), req.getUserId());
}
return price;
}
private void postOrderCreation(Long userId, Order order, BigDecimal amount) {
notificationService.sendOrderConfirmation(userId, order);
pointsService.addPoints(userId, amount);
}
}
// 控制器变得极其简洁,只负责 HTTP 层的转换
@RestController
public class OrderController {
@Autowired private OrderFacade orderFacade;
@PostMapping("/orders")
public OrderResponse createOrder(@RequestBody CreateOrderRequest req) {
Order order = orderFacade.createOrder(req); // 一行调用
return OrderResponse.from(order);
}
}外观模式带来的好处:
- 简化调用:控制器只需调用
orderFacade.createOrder(),无需了解下单的所有步骤; - 业务流程复用:批量下单 API、运营工具都可以复用
OrderFacade.createOrder(); - 降低耦合:控制器不再直接依赖 6 个 Service,只依赖
OrderFacade; - 隐藏子系统变化:如果下单流程新增”风控检查”步骤,只需修改
OrderFacade,控制器无感知。
1.3 外观模式的边界
外观模式非常简单,但有两个容易走偏的方向:
过度集中的反面:如果 OrderFacade 承载了太多不相关的业务(既有下单、又有退款、又有查询报表),它会演变成”上帝类”(God Class),违反 SRP。外观应该按”一个完整的业务场景”来划分边界,而不是把所有子系统调用都塞进一个类。
外观 vs 中介者:外观简化了调用方与子系统的交互(单向依赖),而中介者(Mediator)协调了多个对象之间的相互引用(多向交互),两者意图不同。如果子系统之间需要互相感知和协调,应该用中介者模式而非外观模式。
第 2 章 桥接模式(Bridge)
2.1 动机:两个维度独立变化时的继承爆炸
桥接模式(Bridge Pattern)解决的问题是:当一个类体系在两个或多个维度上都需要扩展时,使用多层继承会导致类的数量爆炸。桥接将”抽象”和”实现”分离为两个独立的继承层次,通过组合而非继承将它们连接。
来看一个经典的例子——消息通知系统:
- 消息类型维度:文本消息、富文本消息、图片消息;
- 发送渠道维度:邮件、短信、微信推送、钉钉。
如果用继承来表达所有组合:
AbstractMessage
├── TextMessage
│ ├── EmailTextMessage
│ ├── SmsTextMessage
│ ├── WeChatTextMessage
│ └── DingTalkTextMessage
├── RichTextMessage
│ ├── EmailRichTextMessage
│ ├── SmsRichTextMessage
│ └── ...
└── ImageMessage
├── EmailImageMessage
└── ...
3 种消息类型 × 4 种渠道 = 12 个具体类。如果增加第 5 种渠道(飞书),需要新增 3 个类;如果增加第 4 种消息类型(视频消息),需要新增 4 个类。类的数量以乘积方式增长——这是典型的”多维度继承爆炸”问题。
2.2 桥接模式的结构
桥接模式将两个变化维度分别建模为两个独立的继承层次,然后通过组合(“桥”)连接它们:
// 维度一(实现维度):消息渠道的抽象接口
public interface MessageSender {
void send(String recipient, String title, String content);
}
// 实现一:邮件发送
public class EmailSender implements MessageSender {
@Override
public void send(String recipient, String title, String content) {
emailClient.send(recipient, title, content);
}
}
// 实现二:短信发送
public class SmsSender implements MessageSender {
@Override
public void send(String recipient, String title, String content) {
// 短信只有 content,忽略 title
smsGateway.send(recipient, content.substring(0, Math.min(content.length(), 70)));
}
}
// 实现三:微信推送
public class WeChatSender implements MessageSender {
@Override
public void send(String recipient, String title, String content) {
weChatApi.sendNotification(recipient, title, content);
}
}
// 维度二(抽象维度):消息类型抽象基类(持有"桥"—— MessageSender 引用)
public abstract class Message {
// "桥":持有实现维度的引用
protected final MessageSender sender;
protected Message(MessageSender sender) {
this.sender = sender;
}
// 发送消息的抽象方法(由子类定义不同类型消息的构建逻辑)
public abstract void send(String recipient, String rawContent);
}
// 具体消息类型一:文本消息
public class TextMessage extends Message {
public TextMessage(MessageSender sender) {
super(sender);
}
@Override
public void send(String recipient, String rawContent) {
// 文本消息:直接发送
sender.send(recipient, "通知", rawContent);
}
}
// 具体消息类型二:告警消息(在消息前后加特殊标记)
public class AlertMessage extends Message {
private final String alertLevel;
public AlertMessage(MessageSender sender, String alertLevel) {
super(sender);
this.alertLevel = alertLevel;
}
@Override
public void send(String recipient, String rawContent) {
// 告警消息:添加紧急程度标题
String title = "【" + alertLevel + "告警】";
String content = "⚠️ " + rawContent + "\n请立即处理!";
sender.send(recipient, title, content);
}
}现在,两个维度可以独立扩展:
// 组合方式 1:邮件 + 文本消息
Message emailText = new TextMessage(new EmailSender());
emailText.send("alice@example.com", "服务器 CPU 使用率过高");
// 组合方式 2:短信 + 告警消息
Message smsAlert = new AlertMessage(new SmsSender(), "P0");
smsAlert.send("13800138000", "服务器 CPU 使用率过高");
// 新增一个渠道(钉钉),无需修改任何已有类:
public class DingTalkSender implements MessageSender { ... }
// 新增一种消息类型(富文本),无需修改任何渠道类:
public class RichTextMessage extends Message { ... }相比原来需要 12 个类,现在只需 3(消息类型)+ 4(渠道)= 7 个类,且两个维度互相独立,各自扩展不影响对方。
2.3 JDBC 驱动架构:桥接模式的工业级实现
Java JDBC 是桥接模式在工业界最经典的应用。
JDBC 将”数据库操作”分为两个维度:
- 抽象维度(应用层 API):
java.sql.Connection、java.sql.PreparedStatement、java.sql.ResultSet——这些是 JDK 定义的标准接口,应用代码依赖这些接口; - 实现维度(具体驱动):MySQL Driver、PostgreSQL Driver、Oracle Driver——各数据库厂商提供各自的实现。
两个维度完全独立:
- 应用可以写一套代码,通过修改数据库驱动(JDBC URL)无缝切换数据库;
- 新的数据库厂商可以开发自己的驱动,无需修改任何应用代码和 JDBC 规范。
这正是桥接模式的精髓:“抽象”(JDBC API)与”实现”(数据库驱动)分别演进,通过接口连接。
第 3 章 组合模式(Composite)
3.1 动机:统一处理树形结构中的叶子与容器
组合模式(Composite Pattern)的适用场景非常具体:当系统中存在树形结构,且需要以统一的方式处理树中的叶节点和分支节点。
这个场景极其常见:文件系统(文件 + 目录)、GUI 组件树(按钮 + 面板 + 窗口)、组织架构(员工 + 部门)、菜单系统(菜单项 + 子菜单)、数学表达式(数字 + 运算符)。
没有组合模式时,处理树形结构的代码往往充斥着 instanceof 判断:
// 没有组合模式:处理文件系统时必须区分文件和目录
public long calculateSize(Object item) {
if (item instanceof File) {
return ((File) item).getSize();
} else if (item instanceof Directory) {
Directory dir = (Directory) item;
long total = 0;
for (Object child : dir.getChildren()) {
total += calculateSize(child); // 递归,但仍需 instanceof
}
return total;
}
throw new IllegalArgumentException("Unknown type: " + item.getClass());
}每次处理树形结构都要写这种 instanceof + 分支逻辑,重复且脆弱(新增节点类型时要修改所有处理逻辑)。
3.2 组合模式的结构
组合模式的核心是:让叶节点和分支节点实现相同的接口,使客户端可以用统一的方式处理两者:
// 统一接口(Component):叶节点和分支节点都实现它
public interface FileSystemItem {
String getName();
long getSize();
void print(String indent);
}
// 叶节点(Leaf):没有子节点
public class FileItem implements FileSystemItem {
private final String name;
private final long size;
public FileItem(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public String getName() { return name; }
@Override
public long getSize() { return size; } // 叶节点的大小就是自身大小
@Override
public void print(String indent) {
System.out.println(indent + "📄 " + name + " (" + size + " bytes)");
}
}
// 分支节点(Composite):有子节点,可以包含叶节点或其他分支节点
public class DirectoryItem implements FileSystemItem {
private final String name;
private final List<FileSystemItem> children = new ArrayList<>();
public DirectoryItem(String name) {
this.name = name;
}
public void add(FileSystemItem item) {
children.add(item);
}
public void remove(FileSystemItem item) {
children.remove(item);
}
@Override
public String getName() { return name; }
@Override
public long getSize() {
// 分支节点的大小 = 所有子节点大小之和(递归)
return children.stream().mapToLong(FileSystemItem::getSize).sum();
}
@Override
public void print(String indent) {
System.out.println(indent + "📁 " + name + "/");
for (FileSystemItem child : children) {
child.print(indent + " "); // 子节点递进缩进
}
}
}
// 使用:客户端统一处理,不需要 instanceof
FileSystemItem root = new DirectoryItem("root");
DirectoryItem docs = new DirectoryItem("docs");
docs.add(new FileItem("readme.md", 1024));
docs.add(new FileItem("guide.pdf", 512000));
DirectoryItem src = new DirectoryItem("src");
src.add(new FileItem("Main.java", 2048));
src.add(new FileItem("Utils.java", 4096));
root.add(docs);
root.add(src);
root.add(new FileItem("pom.xml", 3072));
// 统一调用 getSize(),无需关心是文件还是目录
System.out.println("Total: " + root.getSize() + " bytes"); // 递归计算总大小
root.print(""); // 打印整棵树3.3 组合模式的两种设计取向
在组合模式的设计上,有一个经典的权衡:是否在统一接口(FileSystemItem)中包含 add()、remove() 等只有分支节点才有意义的方法?
透明性(Transparency)方案:在统一接口中包含所有方法(包括 add()、remove()),叶节点对这些方法提供空实现或抛出异常。优点是客户端完全不需要区分叶节点和分支节点;缺点是叶节点实现了它不支持的方法,违反 ISP 和 LSP。
安全性(Safety)方案(上面示例的做法):统一接口只包含公共方法,分支节点独立提供 add()、remove() 方法。优点是接口纯粹、不违反 ISP;缺点是当需要向树中动态添加节点时,必须知道当前节点是否是分支节点(可能需要 instanceof 判断)。
GoF 建议在大多数情况下倾向于透明性方案,尤其是当代码中大量需要动态操作树结构时。安全性方案适合树结构构建完成后主要是遍历/查询的场景。
第 4 章 享元模式(Flyweight)
4.1 动机:大量重复对象造成的内存压力
享元模式(Flyweight Pattern)解决的问题是:当系统中需要大量几乎相同的细粒度对象时,通过共享这些对象的公共状态(内部状态)来节省内存。
这个场景在图形渲染、游戏开发、文档处理中最为典型。以文字处理软件为例:一篇 10 万字的文档,如果每个字符都是一个独立的 Character 对象(包含字符值、字体、字号、颜色),会消耗大量内存。但实际上,同一个字符(如字母 ‘A’)可能出现了 5000 次,每次出现时字体和字号都相同——没有必要维护 5000 个完全相同的对象。
享元模式的核心概念:
- 内部状态(Intrinsic State):对象中不随环境改变的状态,可以被所有实例共享。如字符的字符值(‘A’、‘B’)、棋盘上棋子的颜色(黑色、白色);
- 外部状态(Extrinsic State):对象中随环境改变的状态,不能共享,由客户端传入。如字符在文档中的位置、棋子在棋盘上的坐标。
4.2 享元模式的结构
以围棋为例:棋子颜色(黑/白)是内部状态,可以共享;棋子的位置是外部状态,由客户端传入:
// 享元接口
public interface ChessPiece {
// color 是内部状态(存储在享元对象中)
// x, y 是外部状态(由客户端传入,不存储在享元中)
void draw(int x, int y);
String getColor();
}
// 具体享元:黑色棋子(只创建一个实例,所有黑棋共享)
public class BlackChessPiece implements ChessPiece {
private final String color = "Black"; // 内部状态
@Override
public void draw(int x, int y) {
// x, y 是外部状态,每次调用时传入,不保存
System.out.printf("Drawing %s chess piece at (%d, %d)%n", color, x, y);
}
@Override
public String getColor() { return color; }
}
// 具体享元:白色棋子
public class WhiteChessPiece implements ChessPiece {
private final String color = "White";
@Override
public void draw(int x, int y) {
System.out.printf("Drawing %s chess piece at (%d, %d)%n", color, x, y);
}
@Override
public String getColor() { return color; }
}
// 享元工厂:管理享元对象的创建和缓存
public class ChessPieceFactory {
// 享元池:只存储内部状态不同的享元对象
private static final Map<String, ChessPiece> pool = new HashMap<>();
static {
pool.put("Black", new BlackChessPiece()); // 整个游戏只创建一个
pool.put("White", new WhiteChessPiece()); // 整个游戏只创建一个
}
public static ChessPiece getChessPiece(String color) {
ChessPiece piece = pool.get(color);
if (piece == null) throw new IllegalArgumentException("Unknown color: " + color);
return piece; // 返回共享的享元对象
}
}
// 棋局(客户端):存储的是享元引用 + 外部状态(位置)
public class ChessBoard {
// 每个落子记录:享元引用 + 外部状态(坐标)
private final List<int[]> blackPositions = new ArrayList<>();
private final List<int[]> whitePositions = new ArrayList<>();
private final ChessPiece blackPiece = ChessPieceFactory.getChessPiece("Black");
private final ChessPiece whitePiece = ChessPieceFactory.getChessPiece("White");
public void placeBlack(int x, int y) {
blackPositions.add(new int[]{x, y}); // 只存坐标,不创建新对象
}
public void placeWhite(int x, int y) {
whitePositions.add(new int[]{x, y});
}
public void render() {
// 渲染时,将外部状态(坐标)传给共享的享元对象
for (int[] pos : blackPositions) {
blackPiece.draw(pos[0], pos[1]);
}
for (int[] pos : whitePositions) {
whitePiece.draw(pos[0], pos[1]);
}
}
}内存节省的量化:一局围棋最多 361 个落子,如果不用享元,需要 361 个对象;用享元,只需要 2 个对象(黑棋享元 + 白棋享元),坐标数据存在列表中(原本也需要存储)。
4.3 Java 中内置的享元模式
Java 平台在多处内置了享元模式:
字符串常量池(String Pool):
JVM 维护一个字符串常量池。当代码中出现字符串字面量(如 "hello")时,JVM 先检查常量池中是否已有相同内容的字符串,有则直接返回池中的引用,无则创建并放入池中。
String s1 = "hello"; // 从常量池获取(或创建放入)
String s2 = "hello"; // 从常量池获取同一个对象
String s3 = new String("hello"); // 强制在堆上创建新对象
System.out.println(s1 == s2); // true:s1 和 s2 是同一个对象
System.out.println(s1 == s3); // false:s3 是堆上的新对象
System.out.println(s1.equals(s3)); // true:内容相同这就是为什么 Java 建议用 equals() 而非 == 比较字符串——== 比较的是引用(内存地址),而字符串的实际内存位置取决于它是字面量(常量池)还是 new String()(堆)。
Integer 缓存(-128 到 127):
Integer.valueOf(int) 对 -128 到 127 范围内的整数维护了一个缓存,每次调用返回同一个 Integer 对象:
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true:缓存命中,同一个对象
Integer c = Integer.valueOf(200);
Integer d = Integer.valueOf(200);
System.out.println(c == d); // false:超出缓存范围,不同对象这个缓存之所以限定在 -128 到 127,是因为这个范围内的整数在 Java 程序中使用最频繁,缓存它们能节省大量堆内存和 GC 压力。类似的缓存也存在于 Long.valueOf()、Short.valueOf()、Byte.valueOf()、Character.valueOf()(0-127)中。
Boolean 的两个对象:
Boolean.TRUE 和 Boolean.FALSE 是全局共享的两个享元对象。Boolean.valueOf(boolean) 总是返回这两个之一,从不创建新对象。
4.4 享元模式的适用边界
享元模式有一个前提条件:共享的享元对象必须是不可变的(Immutable),因为多个上下文共享同一个对象,如果对象可被修改,一个上下文的修改会影响所有其他上下文。
享元模式的代价:
- 复杂性增加:需要引入享元工厂和外部状态管理,代码结构变复杂;
- 外部状态的传递:每次调用需要传入外部状态,调用方式与普通对象不同;
- 并发安全:享元对象被多线程共享,必须确保线程安全(不可变是最佳保证)。
使用享元的决策标准:
- 系统中需要大量(几千个以上)几乎相同的对象;
- 这些对象消耗了可观的内存,值得优化;
- 对象状态可以明确分为内部(可共享)和外部(不可共享)两部分;
- 内部状态种类有限(否则享元池本身也会很大)。
享元的误用场景
不要为了”看起来省内存”就应用享元模式。如果对象数量不大(几百个以内),内存节省可以忽略不计,引入享元只会增加代码复杂度。享元是明显的内存优化手段,应该在通过内存分析(如 JProfiler、MAT)确认存在大量重复对象造成内存问题后,才考虑引入。
第 5 章 七种结构型模式总览
完成本篇与上篇的学习,七种结构型模式的完整图景如下:
| 模式 | 核心问题 | 关键结构特征 | 典型应用 |
|---|---|---|---|
| 代理 | 控制对目标对象的访问 | 代理与目标实现相同接口,代理持有目标引用 | Spring AOP、JDK 动态代理、Hibernate 懒加载 |
| 适配器 | 两个不兼容接口之间的转换 | 适配器实现目标接口,持有被适配者引用 | JDBC、Arrays.asList()、第三方 SDK 集成 |
| 装饰器 | 运行时动态添加功能 | 装饰器与被装饰对象实现相同接口,支持多层叠加 | Java I/O 流体系、Spring Security 过滤器链 |
| 外观 | 简化复杂子系统的调用 | 外观类协调多个子系统,对外提供统一接口 | Service 层、API 网关聚合接口 |
| 桥接 | 两个维度都需要独立扩展 | 抽象与实现分为两个独立继承层次,通过组合连接 | JDBC 驱动、消息渠道系统 |
| 组合 | 统一处理树形结构的叶子和容器 | 叶节点和分支节点实现相同接口,分支节点持有子节点列表 | 文件系统、菜单组件、组织架构树 |
| 享元 | 大量重复对象的内存优化 | 享元工厂缓存对象,外部状态由调用方传入 | String 常量池、Integer 缓存、棋盘游戏 |
总结
本篇完成了结构型模式的后半部分:
-
外观模式最简单但价值显著——Service 层本质上就是外观,将复杂的子系统协调逻辑收拢到一处,使调用方不需要了解内部细节。外观的边界应按完整的业务场景划分,避免演变成大型”上帝类”;
-
桥接模式解决了”两个维度都需要独立扩展”的类爆炸问题——将抽象(消息类型)和实现(发送渠道)分为两个独立的继承层次,通过组合连接,JDBC 是最权威的工业级实践;
-
组合模式让树形结构的处理变得优雅——叶节点和分支节点实现相同接口,客户端可以统一调用,消除了大量
instanceof判断。透明性方案(接口包含add/remove)适合需要频繁动态修改树的场景; -
享元模式通过共享不可变的内部状态节省大量内存——Java 的字符串常量池和
Integer缓存是内置实现。享元的前提是存在大量几乎相同的对象且内存消耗显著,不应提前优化引入。
下一篇进入行为型模式,从最常用的三个开始:策略模式(封装算法族,消除 if-else)、模板方法(固定骨架、开放扩展点)、观察者模式(发布-订阅的实现机制):06 行为型模式(上)——策略、模板方法与观察者。
参考资料
- GoF,《Design Patterns: Elements of Reusable Object-Oriented Software》, 1994
- Martin Fowler,《Patterns of Enterprise Application Architecture》(外观/Service 层)
- JDK 源码:
java.io.FilterInputStream(装饰器)、java.lang.Integer(享元)、java.sql.Connection(桥接)
思考题
- 适配器模式将不兼容的接口转换为兼容接口。Java 中
Arrays.asList()返回的 List 是一个适配器——它将数组适配为 List 接口,但不支持add()/remove()。这种’不完整适配’是否违反了里氏替换原则?在什么情况下适配器不需要实现目标接口的所有方法?- 桥接模式将抽象与实现分离,使它们可以独立变化。JDBC 的
Driver接口(抽象)与 MySQL/PostgreSQL 的具体实现(实现)就是桥接模式。与简单的接口多态相比,桥接模式多了一个’抽象维度’——在什么场景下你需要同时变化抽象和实现两个维度?举一个非 JDBC 的实际例子。- 外观模式(Facade)为复杂子系统提供简化接口。Spring 的
JdbcTemplate是 JDBC API 的外观——它封装了 Connection 获取、Statement 创建、ResultSet 处理和资源释放。但 Facade 隐藏了底层细节——当 Facade 提供的简化接口不能满足需求(如需要手动控制事务隔离级别)时,你是应该扩展 Facade 还是绕过它?