本文是【GoF设计模式】系列第9篇,更多内容欢迎关注公众号:咖啡八杯

前言
为什么需要外观模式?
开发中经常遇到这种情况:一个业务操作需要依次调用多个子系统。比如"启动系统"要先初始化数据库连接、加载配置文件、启动缓存服务、注册定时任务——每个子系统都有自己的接口和调用顺序,漏掉一步或顺序错误都会导致系统异常。
如果让客户端直接操作这些子系统,代码会变成这样:
// 客户端需要了解所有子系统的细节
databaseService.init(config.getDbUrl(), config.getDbUser());
configService.load("application.properties");
cacheService.start(config.getCacheNodes());
schedulerService.registerJobs();
一旦子系统数量增加或调用顺序调整,所有使用的地方都要改。这就是外观模式要解决的问题——为复杂的子系统提供一个简单的统一入口。
概念
外观模式(Facade Pattern)是一种结构型设计模式,核心思想是为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。
外观模式定义了一个更高层次的接口,让子系统更加容易使用。客户端只需要调用外观类的方法,而不需要了解子系统内部的复杂交互。这就好比去餐厅吃饭,只需要跟服务员点菜(外观),不需要自己去厨房告诉厨师怎么做、去仓库拿食材、去洗碗间拿餐具——这些子系统的复杂性都被服务员这个"外观"屏蔽了。
外观模式的结构比较简单,只包含两个角色:
- 外观(Facade):对外提供一个统一的高层次接口,使复杂的子系统变得更易使用。外观类知道哪些子系统负责处理请求,将客户端的请求委派给对应的子系统对象
- 子系统类(Subsystem):实现子系统的功能,处理外观类指派的任务。子系统类不感知外观的存在,对子系统而言外观只是另一个客户端

Client 只依赖 Facade,完全不感知 SubsystemA/B/C 的存在。Facade 内部协调调用多个子系统的方法,将复杂的交互逻辑封装在内部。
实现
基础实现
外观模式的基本实现分为以下几个步骤:
- 定义子系统类,实现各自的业务功能
- 定义外观类,持有子系统对象的引用,提供统一的简化接口
- 外观类的方法内部协调调用多个子系统的方法
- 客户端通过外观类访问子系统,无需了解子系统内部细节
// 子系统A
class SubsystemA {public void operationA() {System.out.println("SubsystemA operation");}
}// 子系统B
class SubsystemB {public void operationB() {System.out.println("SubsystemB operation");}
}// 子系统C
class SubsystemC {public void operationC() {System.out.println("SubsystemC operation");}
}// 外观类
class Facade {private SubsystemA a;private SubsystemB b;private SubsystemC c;public Facade() {a = new SubsystemA();b = new SubsystemB();c = new SubsystemC();}// 简化接口:客户端只需调用这一个方法public void operation() {a.operationA();b.operationB();c.operationC();}
}// 客户端代码
Facade facade = new Facade();
facade.operation(); // 一次调用,完成三个子系统的操作
引入一个例子:「去餐厅吃饭,客人只和服务员打交道。服务员接到点菜请求后,会依次通知厨房做菜、仓库拿食材、洗碗间准备餐具——这些子系统的协作过程对客人完全透明」。
餐厅对应 Facade(外观),厨房/仓库/洗碗间对应 Subsystem(子系统),客人对应 Client——服务员封装了三个子系统的协作流程,客人只需要说"点菜",后续一切自动完成。
// 子系统:厨房
class Kitchen {public void cook(String dish) {System.out.println("厨房正在制作: " + dish);}
}// 子系统:仓库
class Warehouse {public String getIngredients(String dish) {System.out.println("仓库正在配送食材: " + dish);return "食材-" + dish;}
}// 子系统:洗碗间
class Dishwashing {public void prepareUtensils() {System.out.println("洗碗间正在准备餐具");}
}// 外观类:服务员
class WaiterFacade {private Kitchen kitchen;private Warehouse warehouse;private Dishwashing dishwashing;public WaiterFacade() {this.kitchen = new Kitchen();this.warehouse = new Warehouse();this.dishwashing = new Dishwashing();}// 简化接口:客人只需说"点菜"public void orderDish(String dish) {// 外观内部协调子系统dishwashing.prepareUtensils();String ingredients = warehouse.getIngredients(dish);kitchen.cook(dish);System.out.println("菜品 " + dish + " 已准备好,请享用!");}
}// 客人(客户端)只和服务员打交道
WaiterFacade waiter = new WaiterFacade();
waiter.orderDish("红烧肉"); // 一行代码,背后三个子系统协作完成
外观模式最大的好处是简化:客户端不需要知道子系统有多少个、调用顺序是什么、每个子系统需要什么参数——所有复杂性都被外观封装了。
总结
外观模式本质上是一层"统一入口"——将多个子系统的复杂交互封装成一个简单的接口。
什么时候用:
- 想为复杂的子系统提供一个简单的统一入口
- 客户端与多个子系统之间存在强耦合,需要降低依赖
- 分层架构中,需要为每一层定义清晰的入口点
什么时候不用:
- 子系统本身就很简单,不需要额外的封装层
- 客户端需要细粒度控制子系统,外观会限制灵活性
- 外观类可能变成"上帝类",承担过多职责
简单记忆:
外观解决"调用复杂"的问题,是给子系统"开一个统一窗口"。能直接调用子系统时,不必强行加外观。
相似模式区分
外观模式与其他结构型模式在"包装对象"这一结构上相似,但意图完全不同。
| 模式 | 接口关系 | 核心意图 | 典型场景 |
|---|---|---|---|
| 外观 | 目标接口是新设计的 | 简化复杂子系统的调用 | JdbcTemplate、SLF4J |
| 适配器 | 目标接口 ≠ 被包装对象接口 | 转换接口,让不兼容的类协同 | 第三方SDK接入、Java I/O |
| 装饰器 | 目标接口 = 被包装对象接口 | 增强功能,接口不变 | Java I/O流嵌套 |
| 代理 | 目标接口 = 被包装对象接口 | 控制访问,附加访问前后逻辑 | Spring AOP、MyBatis Mapper |
外观 vs 中介者
| 维度 | 外观模式 | 中介者模式 |
|---|---|---|
| 核心意图 | 为子系统提供统一接口,简化外部访问 | 协调多个对等对象之间的交互 |
| 结构差异 | 外观是子系统的"门面",子系统不感知外观 | 中介者是对象间的"协调者",对象感知中介者 |
| 关注点 | 简化调用方的使用体验 | 解耦多个对象之间的网状依赖 |
| 典型场景 | 为复杂子系统提供简单API | GUI组件间交互、多对象协作 |
逐步区分法:
- 如果目的是简化子系统的调用方式,子系统之间不需要互相通信 → 选外观模式
- 如果目的是协调多个对等对象之间的交互,对象之间需要互相感知 → 选中介者模式
- 如果子系统不感知外观的存在,外观只是单向调用 → 选外观模式
- 如果对象通过中介者双向通信,中介者协调各方 → 选中介者模式
外观 vs 代理
| 维度 | 外观模式 | 代理模式 |
|---|---|---|
| 核心意图 | 简化对多个子系统的访问,提供新接口 | 控制对真实对象的访问,保持相同接口 |
| 结构差异 | 外观定义新接口,子系统有独立接口 | 代理与真实对象实现同一接口 |
| 关注点 | 降低使用复杂度 | 控制访问(延迟加载、权限校验、缓存等) |
| 典型场景 | 封装复杂流程为单一调用 | 远程代理、虚拟代理、保护代理 |
逐步区分法:
- 如果需要封装多个子系统的复杂调用,提供更简单的接口 → 选外观模式
- 如果需要控制对单个对象的访问(延迟、权限、远程) → 选代理模式
- 如果接口是新定义的,与子系统不同 → 选外观模式
- 如果接口与真实对象一致,只是增强访问控制 → 选代理模式
外观 vs 适配器
| 维度 | 外观模式 | 适配器模式 |
|---|---|---|
| 核心意图 | 简化接口,让子系统更易用 | 接口转换,让不兼容的类协同工作 |
| 结构差异 | 外观提供新接口,子系统不变 | 适配器将已有接口转换为目标接口 |
| 关注点 | 降低使用门槛 | 解决不兼容问题 |
| 典型场景 | 为复杂子系统提供统一入口 | 集成第三方库、旧系统对接 |
逐步区分法:
- 如果子系统接口已经兼容,只是调用复杂 → 选外观模式
- 如果子系统接口不兼容,需要转换才能协同工作 → 选适配器模式
- 如果目的是简化调用,减少客户端代码量 → 选外观模式
- 如果目的是复用已有类,但接口不匹配 → 选适配器模式
练习题目
智能家居控制系统
题目描述:小明有一个智能家居系统,包含四个子系统:
- 照明系统(Light):开灯、关灯
- 空调系统(AirConditioner):开启、关闭、设置温度
- 安防系统(SecuritySystem):布防、撤防
- 窗帘系统(Curtain):打开、关闭
每天出门时,小明需要依次:关灯 → 关空调 → 关窗帘 → 安防布防。
每天回家时,小明需要依次:安防撤防 → 开灯 → 开空调并设置温度为26度 → 开窗帘。
请使用外观模式设计智能家居控制台,提供 leaveHome() 和 goHome() 两个简化接口,让小明一键操作。
输入描述:第一行是一个整数 N(1 ≤ N ≤ 100),表示后续有 N 条指令。接下来的 N 行,每行一个字符串:leave 表示离家,go 表示回家。
输出描述:按顺序输出每条指令触发的子系统操作结果。
输入示例:
3
leave
go
leave
输出示例:
Light is turned off.
AirConditioner is turned off.
Curtain is closed.
SecuritySystem is armed.
=== Left Home ===
SecuritySystem is disarmed.
Light is turned on.
AirConditioner is turned on, temperature set to 26.
Curtain is opened.
=== Went Home ===
Light is turned off.
AirConditioner is turned off.
Curtain is closed.
SecuritySystem is armed.
=== Left Home ===
解题思路:智能家居系统是外观模式的典型应用。四个子系统分别控制灯、空调、窗帘和安防,如果不使用外观模式,小明每次出门/回家都要依次调用四个子系统的方法,一旦某个子系统新增了操作步骤,所有调用的地方都要修改。外观类 SmartHomeFacade 将出门和回家的操作封装成两个简化接口,客户端不需要了解各子系统的调用顺序和细节。
import java.util.*;public class Main {public static void main(String[] args) {Scanner sc = new Scanner(System.in);int n = sc.nextInt();SmartHomeFacade facade = new SmartHomeFacade();while (n-- > 0) {String op = sc.next();if ("leave".equals(op)) {facade.leaveHome();} else {facade.goHome();}}}
}// 照明子系统
class LightSystem {public void turnOn() {System.out.println("Light is turned on.");}public void turnOff() {System.out.println("Light is turned off.");}
}// 空调子系统
class AirConditionerSystem {public void turnOn(int temperature) {System.out.println("AirConditioner is turned on, temperature set to " + temperature + ".");}public void turnOff() {System.out.println("AirConditioner is turned off.");}
}// 安防子系统
class SecuritySystem {public void arm() {System.out.println("SecuritySystem is armed.");}public void disarm() {System.out.println("SecuritySystem is disarmed.");}
}// 窗帘子系统
class CurtainSystem {public void open() {System.out.println("Curtain is opened.");}public void close() {System.out.println("Curtain is closed.");}
}// 外观类:封装智能家居的简化操作
class SmartHomeFacade {private LightSystem light;private AirConditionerSystem airConditioner;private SecuritySystem security;private CurtainSystem curtain;public SmartHomeFacade() {this.light = new LightSystem();this.airConditioner = new AirConditionerSystem();this.security = new SecuritySystem();this.curtain = new CurtainSystem();}public void leaveHome() {light.turnOff();airConditioner.turnOff();curtain.close();security.arm();System.out.println("=== Left Home ===");}public void goHome() {security.disarm();light.turnOn();airConditioner.turnOn(26);curtain.open();System.out.println("=== Went Home ===");}
}
扩展:实际项目中的外观模式
Spring 的 JdbcTemplate
原生 JDBC 操作非常繁琐——获取连接、创建 Statement、执行 SQL、处理结果集、关闭连接,每一步都要处理异常和资源释放。Spring 的 JdbcTemplate 就是外观模式的典型应用,它将这些步骤封装成一个简单的方法调用。
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List<Map<String, Object>> users = jdbcTemplate.queryForList("SELECT * FROM user WHERE age > ?", 18);
开发者只需要关注 SQL 和参数,如果不用 JdbcTemplate,同样功能的代码量要多 3-5 倍。这就是外观模式"简化调用"的体现。
SLF4J 日志门面
Java 生态中有多种日志实现(Log4j、Logback、java.util.logging 等),如果业务代码直接依赖某个具体实现,切换日志框架时需要修改大量代码。SLF4J 就是一个外观,为所有日志框架提供统一接口。
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
logger.info("创建订单: {}", orderId);
SLF4J 本身不实现日志功能,它只是外观接口。底层可以无缝切换 Logback、Log4j2 等实现,业务代码零修改。
Spring Security 的 SecurityContextHolder
在 Web 应用中获取当前登录用户信息,涉及到 SecurityContext、Authentication、UserDetails 等多个类的交互。Spring Security 提供了简化的外观接口:
String username = SecurityContextHolder.getContext().getAuthentication().getName();
一行代码,背后涉及从 Session/Cookie 中获取上下文、认证管理器、用户详情服务等多个子系统的协作。开发者不需要关心这些细节。
MyBatis 的 SqlSession
MyBatis 的 SqlSession 是对 JDBC 操作的外观封装,一个方法调用背后可能涉及连接获取、SQL 解析、参数映射、结果集映射、事务管理等多个子系统的协作。
SqlSession session = sqlSessionFactory.openSession();
User user = session.selectOne("com.example.mapper.UserMapper.selectById", 1L);
session.commit();
session.close();
SqlSession 将 Executor、StatementHandler、ParameterHandler、ResultSetHandler 等组件封装成简单的 API。
文件上传服务
文件上传通常涉及多个步骤——文件校验、存储、元数据记录、URL 生成等。通过外观类将这些步骤封装成一个简单的方法,Controller 只需要一行调用:
FileUploadFacade facade = new FileUploadFacade();
String url = facade.upload(file);
如果没有外观类,Controller 中需要注入三个服务,按顺序调用并处理异常,代码会非常冗长。
支付网关
在线支付涉及多个步骤——创建支付单、调用第三方支付接口、更新订单状态、发送通知等。通过外观类将这些步骤封装,业务方只需要调用一个支付方法:
PaymentFacade paymentFacade = new PaymentFacade();
boolean success = paymentFacade.pay("ORDER_001", new BigDecimal("99.99"), "WECHAT");
支付流程涉及 4 个子系统的协作,外观类将这些复杂性封装,Controller 只需要关注"发起支付"这一个动作。
现在可能还用不到这些,但等到需要接入复杂子系统、封装繁琐流程的时候,会突然发现:"这不就是外观模式吗?"——那时候就真的懂了。
技术交流 & 更多原创内容,关注公众号:咖啡八杯
