配套代码: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 设计模式学习指南

上一篇:装饰器 · 下一篇:享元