12|外观:为多组件协作提供业务入口
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.facade。
系列导航:Java 25 设计模式学习指南
发布一篇文章需要校验标题、保存文章,再更新搜索索引。如果按钮处理器、批量导入和定时发布各自复制这段过程,就会出现某条路径忘记建立索引的情况。
外观提供一个 publish 入口,让调用方表达“发布文章”这件事。保存和索引仍是各自独立的子系统,外观负责给常见任务组织一条容易使用的路径。
从问题提炼设计意图
外观为一组子系统提供统一的高层接口,降低调用方理解和协调多个组件的成本。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
PublishingFacade |
面向调用方的发布入口 |
DraftStore |
文章存储子系统替身 |
SearchIndex |
搜索索引子系统替身 |
Demo |
只发起高层发布动作 |
classDiagram
Demo --> PublishingFacade : publish
PublishingFacade --> DraftStore : save
PublishingFacade --> SearchIndex : index
代码思路:变化应该落在哪个对象上
外观构造器明确接收两个依赖,调用者在装配时决定具体对象,发布入口不在内部隐藏创建全局对象。这样测试可以直接观察两边的结果。
publish 先校验标题,随后调用 store.save 和 index.add。先校验再产生副作用,可以保证本例的非法输入不会留下已保存但未索引的数据。
存储和索引仍保留自己的职责:DraftStore 记录列表,SearchIndex 维护集合。外观没有把它们的所有方法重新暴露一遍,而是围绕一个有意义的业务动作组织协作。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.facade,源码目录为 src/main/java/com/hanserwei/patterns/facade/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
PublishingFacade.java:
package com.hanserwei.patterns.facade;
import java.util.Objects;
/** 向调用方提供统一的文章发布入口. */
public final class PublishingFacade {
/** 负责保存文章的子系统. */
private final DraftStore store;
/** 负责文章检索的子系统. */
private final SearchIndex index;
/** 注入本次发布需要的两个子系统. */
public PublishingFacade(DraftStore store, SearchIndex index) {
this.store = Objects.requireNonNull(store, "store");
this.index = Objects.requireNonNull(index, "index");
}
/** 校验后按顺序保存、索引;本内存示例不提供跨系统事务. */
public void publish(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Title must not be blank");
}
store.save(title);
index.add(title);
}
}
DraftStore.java:
package com.hanserwei.patterns.facade;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** 用内存模拟文章存储子系统. */
public final class DraftStore {
/** 已保存的标题,按发布顺序记录. */
private final List<String> titles = new ArrayList<>();
/** 保存非空标题. */
public void save(String title) {
titles.add(Objects.requireNonNull(title, "title"));
}
/** 返回已保存标题的快照. */
public List<String> titles() {
return List.copyOf(titles);
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.facade;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
DraftStore store = new DraftStore();
SearchIndex index = new SearchIndex();
new PublishingFacade(store, index).publish("Java 25");
System.out.println(store.titles() + " indexed=" + index.contains("Java 25"));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runFacade
./gradlew test --tests 'com.hanserwei.patterns.facade.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
[Java 25] indexed=true
用测试确认模式的行为
发布后存储与索引都可观察到标题;非法输入在任何副作用前失败。测试并未证明真实数据库与搜索服务之间的事务一致性。
对应测试位于 src/test/java/com/hanserwei/patterns/facade/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 应用服务向 Web 控制器或命令行提供清晰的用例入口。
- 复杂 SDK、媒体转换或文件导入提供常用流程封装。
- 在旧子系统前建立渐进迁移边界,减少新调用方的依赖数量。
适用边界与容易踩的坑
统一入口不等于原子事务。真实环境中若保存成功而索引失败,外观不会自动回滚已保存的数据;需要事务边界、重试、补偿或事务性消息等独立机制。本例没有跨系统故障模拟,也不宣称解决了该问题。
重复发布同一标题时,内存列表会出现重复项,集合索引却只有一项。这正好提醒我们:幂等性必须以业务标识和操作语义设计,不能通过“只有一个发布方法”推断已经具备幂等性。
避免把整个系统所有动作都塞进一个巨大的 Facade。按用例或子系统边界拆分,让依赖关系仍然可以被理解;高层接口应该简化常见任务,而不是隐藏所有错误和成本。
与相近模式比较
中介者集中协调同事对象之间的交互,同事通常知道中介者;外观面向外部调用方组织子系统,子系统无需知道外观存在。
动手练习
先定义存储和索引接口,再注入一个必定失败的索引替身,观察保存后的状态。写一段恢复策略说明:你准备回滚、记录待索引事件,还是允许稍后重建?不要直接在 catch 中吞掉异常。
系列导航:Java 25 设计模式学习指南