外观模式
从家庭影院说起
你买了一套家庭影院,有功放、调谐器、DVD 机、投影仪、屏幕、爆米花机……看个电影要做 15 件事:调暗灯光→放下屏幕→打开投影仪→选 DVD 输入→打开功放→设音量→打开 DVD→放片……每次都要按这一串顺序,漏一步就出问题。
这正是复杂子系统暴露太多细节的典型问题。解决方案是创建一个 HomeTheaterFacade(外观),把所有步骤封装进 watchMovie() 和 endMovie() 两个方法。你只需要一行调用,不需要了解任何子系统细节。
🔍 定义
外观模式(Facade)为复杂的子系统提供一个简单的高层接口,让客户端只需通过这个接口就能完成常见操作,无需了解子系统内部细节。
设计原则:最少知识原则(迪米特法则)—— 只和你的密友交流。
客户端只认识 HomeTheaterFacade,不直接接触 Amplifier/DVDPlayer/Projector。
⚠️ 不使用外观存在的问题
| FacadeBadExample.java |
|---|
| package com.example.structural.facade;
/**
* 外观模式 - 反例
* 问题:客户端直接调用所有子系统,依赖细节,顺序容易出错
*/
public class FacadeBadExample {
public static void main(String[] args) {
// ❌ 客户端需要知道所有子系统的调用细节和顺序
LightsBad lights = new LightsBad();
ProjectorBad projector = new ProjectorBad();
AmplifierBad amp = new AmplifierBad();
DVDPlayerBad dvd = new DVDPlayerBad();
// 开启电影模式(客户端必须记住每个步骤)
lights.dim(10);
projector.on();
projector.setInput("HDMI");
amp.on();
amp.setVolume(50);
dvd.on();
dvd.play("星际穿越");
System.out.println("--- 停止 ---");
// 关闭时顺序也要正确
dvd.stop();
dvd.off();
amp.off();
projector.off();
lights.on(); // 每次都要记顺序 ❌
}
}
class LightsBad {
public void on() { System.out.println("灯光:打开"); }
public void off() { System.out.println("灯光:关闭"); }
public void dim(int level){ System.out.println("灯光:调暗到 " + level + "%"); }
}
class ProjectorBad {
public void on() { System.out.println("投影仪:开启"); }
public void off() { System.out.println("投影仪:关闭"); }
public void setInput(String src) { System.out.println("投影仪:输入源 " + src); }
}
class AmplifierBad {
public void on() { System.out.println("功放:开启"); }
public void off() { System.out.println("功放:关闭"); }
public void setVolume(int v) { System.out.println("功放:音量 " + v); }
}
class DVDPlayerBad {
public void on() { System.out.println("DVD:开机"); }
public void off() { System.out.println("DVD:关机"); }
public void play(String m){ System.out.println("DVD:播放 " + m); }
public void stop() { System.out.println("DVD:停止"); }
}
|
🏗️ 设计模式结构(家庭影院)
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class HomeTheaterFacade {
-lights: Lights
-projector: Projector
-amplifier: Amplifier
-dvd: DVDPlayer
+watchMovie(title) void
+endMovie() void
}
class Lights {
+on() void
+dim(level) void
}
class Projector {
+on() void
+off() void
+setInput(input) void
}
class Amplifier {
+on() void
+off() void
+setVolume(level) void
}
class DVDPlayer {
+on() void
+off() void
+play(title) void
+stop() void
}
HomeTheaterFacade o--> Lights
HomeTheaterFacade o--> Projector
HomeTheaterFacade o--> Amplifier
HomeTheaterFacade o--> DVDPlayer
note for HomeTheaterFacade "外观(Facade)"
note for Lights "子系统(Subsystem)"
note for Projector "子系统(Subsystem)"
note for Amplifier "子系统(Subsystem)"
note for DVDPlayer "子系统(Subsystem)"
外观类聚合所有子系统,暴露 watchMovie() / endMovie() 两个简单操作。子系统依然存在,高级用户也可以绕过外观直接使用——外观不锁住任何功能。
💻 设计模式举例说明
| FacadeExample.java |
|---|
| package com.example.structural.facade;
/**
* 外观模式 - 正例
* HomeTheaterFacade 封装所有子系统调用,客户端只需一行命令
*/
public class FacadeExample {
public static void main(String[] args) {
// ✅ 客户端只依赖外观,不需要了解子系统细节
HomeTheaterFacade theater = new HomeTheaterFacade(
new Lights(), new Projector(), new Amplifier(), new DVDPlayer()
);
theater.watchMovie("星际穿越"); // 一行完成所有设备联动 ✅
System.out.println("--- 结束 ---");
theater.endMovie(); // 一行完成所有关闭 ✅
}
}
// 子系统(单独保持不变)
class Lights {
public void on() { System.out.println("灯光:打开"); }
public void off() { System.out.println("灯光:关闭"); }
public void dim(int level) { System.out.println("灯光:调暗到 " + level + "%"); }
}
class Projector {
public void on() { System.out.println("投影仪:开启"); }
public void off() { System.out.println("投影仪:关闭"); }
public void setInput(String s) { System.out.println("投影仪:输入 " + s); }
}
class Amplifier {
public void on() { System.out.println("功放:开启"); }
public void off() { System.out.println("功放:关闭"); }
public void setVolume(int v){ System.out.println("功放:音量 " + v); }
}
class DVDPlayer {
public void on() { System.out.println("DVD:开机"); }
public void off() { System.out.println("DVD:关机"); }
public void play(String m) { System.out.println("DVD:播放《" + m + "》"); }
public void stop() { System.out.println("DVD:停止"); }
}
// ✅ 外观:封装"观影模式"和"结束模式"两组复杂操作
class HomeTheaterFacade {
private final Lights lights;
private final Projector projector;
private final Amplifier amplifier;
private final DVDPlayer dvdPlayer;
public HomeTheaterFacade(Lights lights, Projector projector,
Amplifier amplifier, DVDPlayer dvdPlayer) {
this.lights = lights;
this.projector = projector;
this.amplifier = amplifier;
this.dvdPlayer = dvdPlayer;
}
// 一键开启观影模式
public void watchMovie(String movie) {
System.out.println("=== 准备观影模式 ===");
lights.dim(10);
projector.on();
projector.setInput("HDMI");
amplifier.on();
amplifier.setVolume(50);
dvdPlayer.on();
dvdPlayer.play(movie);
}
// 一键关闭所有设备
public void endMovie() {
System.out.println("=== 关闭影院模式 ===");
dvdPlayer.stop();
dvdPlayer.off();
amplifier.off();
projector.off();
lights.on();
}
}
|
⚖️ 优缺点
优点:
- 大幅简化客户端代码,降低与子系统的耦合
- 子系统内部可以自由重构,客户端不受影响
缺点:
- 外观类容易成为"上帝类",所有操作都堆在里面
- 新增子系统功能时,外观类也需要修改
🔗 与其它模式的关系
| 模式 |
意图 |
方向 |
| 外观(Facade) |
简化子系统访问接口 |
客户端 → 子系统(单向包装) |
| 中介者(Mediator) |
协调多个对象之间的通信 |
双向:各组件 ↔ 中介者 |
| 适配器(Adapter) |
改变接口使其兼容 |
包装一个不兼容的接口 |
🗂️ 应用场景
- 为复杂子系统提供简单入口(如 SDK 门面类)
- 对遗留系统封装,向外提供清晰的接口
- Spring:
JdbcTemplate 封装了 DataSource/Connection/Statement/ResultSet 的操作细节
🏭 工业视角
接口粒度困境:细粒度与粗粒度的博弈
在微服务架构中,接口粒度设计面临两难:细粒度接口职责单一、可复用性强,但调用方需要多次调用才能完成一个业务;粗粒度接口使用方便,但定制性强、复用性差,接口数量随调用方需求增多而爆炸式膨胀。
《设计模式之美》给出的基本原则是:尽量保持接口可复用性(细粒度),同时允许为特定调用方提供冗余的门面接口(粗粒度)。两者并不冲突——门面层是对细粒度接口的聚合,而非替代。
粒度分层原则
- 底层服务:细粒度、职责单一,利于复用
- 门面层:粗粒度、面向特定场景,利于易用
- 两层可以同时存在,调用方按需选择
BFF 模式:门面模式的微服务落地
微服务架构中,前端(App/Web/小程序)往往需要同时聚合多个后端服务的数据,导致网络请求次数过多、响应慢。**BFF(Backend for Frontend)层**正是门面模式的工业级实现:
- 每类客户端对应一个 BFF 服务
- BFF 对内调用多个微服务的细粒度接口,对外暴露面向页面需求的粗粒度接口
- 前端只发起 1 次请求,由 BFF 在内网并行或串行调用多个下游服务
| BFF 门面聚合示例 |
|---|
| // BFF 层:聚合用户、订单、推荐三个微服务,一次返回首页所需全部数据
public class HomepageFacade {
private UserService userService;
private OrderService orderService;
private RecommendService recommendService;
public HomepageVO getHomepageData(long userId) {
UserDTO user = userService.getUser(userId); // 细粒度接口 1
List<OrderDTO> orders = orderService.getRecentOrders(userId); // 细粒度接口 2
List<ItemDTO> items = recommendService.recommend(userId); // 细粒度接口 3
return HomepageVO.assemble(user, orders, items); // 聚合为单次响应
}
}
|
门面 vs 适配器:意图截然不同
两者都做"封装",容易混淆,但解决的问题根本不同:
| 模式 |
核心问题 |
接口变化 |
典型场景 |
| 门面(Facade) |
接口太多、调用繁琐 |
新增高层聚合接口,原接口保留 |
BFF、SDK 入口类 |
| 适配器(Adapter) |
接口不兼容、无法直接使用 |
转换旧接口以匹配目标接口 |
对接第三方 SDK、旧系统改造 |
常见设计误区
门面不做接口格式转换,只做接口聚合。如果你的"门面"还需要做数据格式转换或协议映射,它实际上同时承担了适配器的职责,应当明确拆分或清晰命名(如 XxxAdapter),避免职责混淆。
用门面接口替代分布式事务
跨服务操作(如"创建用户 + 创建钱包")若分别调用两个接口,在分布式环境下很难保证原子性,引入分布式事务框架代价较高。更简单的方案是:设计一个门面接口将两个 SQL 操作合并到同一个服务方法中,借助 Spring 的 @Transactional 在单次数据库事务中完成。门面接口把两次网络调用变成了一次本地事务,以更低的复杂度解决了一致性问题。