文章目录
-
- 前言
- 一、核心定义
- 二、标准体系结构图
- 三、场景推演
- 三、场景推演
- 四、实战案例
-
- [4.1 需求分析](#4.1 需求分析)
- 三、场景推演
-
- [4.1 需求分析](#4.1 需求分析)
- [4.2 架构图](#4.2 架构图)
-
- [4.2.1 普通方式架构图](#4.2.1 普通方式架构图)
- [4.2.2 外观模式架构图](#4.2.2 外观模式架构图)
- [4.3 时序图](#4.3 时序图)
-
- [4.3.1 普通代码时序图](#4.3.1 普通代码时序图)
- [4.3.2 外观模式时序图](#4.3.2 外观模式时序图)
- [4.4 代码分析](#4.4 代码分析)
-
- [4.4.1 普通代码(if-else/硬编码)](#4.4.1 普通代码(if-else/硬编码))
- [4.4.2 外观模式代码](#4.4.2 外观模式代码)
- 总结
前言
在日常开发中,我们经常会遇到这样一类代码:业务目标本身很简单,但为了完成它,客户端不得不依次调用很多底层类。
以本项目为例,客户端只是想把一个视频文件转换成指定格式。但如果直接使用底层媒体处理库,就需要自己完成加载视频、识别源编码器、选择目标编码器、读取码率、执行转换、修复音频等一整套流程。
这类问题的核心不是"代码能不能跑",而是"调用者是否被迫理解太多细节"。
外观模式正是为了解决这种复杂调用暴露给客户端的问题。

- 图片来自:外观设计模式(门面模式)
一、核心定义
外观模式(Facade Pattern)是一种结构型设计模式。
它为复杂子系统提供一个统一、简洁的高层接口,让客户端通过这个接口完成操作,而不需要直接依赖子系统中的多个类。
可以把它理解成:
外观类负责把复杂流程组织好,客户端只调用一个简单方法。
**举例来说:**当你通过电话给商店下达订单时, 接线员就是该商店的所有服务和部门的外观。 接线员为你提供了一个同购物系统、 支付网关和各种送货服务进行互动的简单语音接口。
外观模式的目标不是替代子系统,也不是隐藏所有能力,而是为常见使用场景提供一个更友好的入口。
二、标准体系结构图
调用统一接口
协调调用
协调调用
协调调用
Client
+main()
Facade
+operation()
SubsystemA
+stepA()
SubsystemB
+stepB()
SubsystemC
+stepC()
角色说明:
Client:客户端,只关心业务目标,不想理解子系统内部细节。Facade:外观类,对外提供简洁接口,对内编排复杂调用。Subsystem:子系统类,负责具体能力,通常数量较多、调用顺序也有要求。
外观模式并不禁止客户端直接访问子系统。它只是额外提供了一条更简单、更稳定的调用路径。
三、场景推演
三、场景推演
生活中有很多"外观模式"的例子。
比如我们去餐厅吃饭。顾客真正想做的事情很简单:点一份套餐。但餐厅内部实际要做很多事:前台接单、后厨备菜、厨师炒菜、饮品区做饮料、服务员上菜、收银系统结账。
如果没有"前台"这个统一入口,顾客可能要自己去找厨师、找饮品区、找收银员,整个过程就会非常混乱。
前台就像外观类:
- 顾客只需要对前台说:"我要一份套餐。"
- 前台负责把请求分发给后厨、饮品区、收银系统。
- 顾客不需要知道餐厅内部怎么协作。
用代码简单模拟:
java
class Kitchen {
public void cook() {
System.out.println("后厨:开始做主菜");
}
}
class DrinkBar {
public void makeDrink() {
System.out.println("饮品区:制作饮料");
}
}
class Cashier {
public void pay() {
System.out.println("收银台:完成结账");
}
}
class RestaurantFacade {
private Kitchen kitchen = new Kitchen();
private DrinkBar drinkBar = new DrinkBar();
private Cashier cashier = new Cashier();
public void orderSetMeal() {
cashier.pay();
kitchen.cook();
drinkBar.makeDrink();
System.out.println("前台:套餐准备完成");
}
}
public class Client {
public static void main(String[] args) {
RestaurantFacade restaurant = new RestaurantFacade();
restaurant.orderSetMeal();
}
}
客户端代码非常简单:
restaurant.orderSetMeal();
它不需要知道主菜怎么做、饮料谁负责、账单怎么处理。
回到软件开发中也是一样。很多业务功能表面上只是一个动作,但内部需要多个类协作完成。如果这些细节全部暴露给调用者,客户端代码就会越来越重。外观模式的作用,就是给复杂系统套上一层"服务窗口",让调用者通过一个简单入口完成复杂操作。
四、实战案例
4.1 需求分析
三、场景推演
生活中有很多"外观模式"的例子。
比如我们去餐厅吃饭。顾客真正想做的事情很简单:点一份套餐。但餐厅内部实际要做很多事:前台接单、后厨备菜、厨师炒菜、饮品区做饮料、服务员上菜、收银系统结账。
如果没有"前台"这个统一入口,顾客可能要自己去找厨师、找饮品区、找收银员,整个过程就会非常混乱。
前台就像外观类:
- 顾客只需要对前台说:"我要一份套餐。"
- 前台负责把请求分发给后厨、饮品区、收银系统。
- 顾客不需要知道餐厅内部怎么协作。
用代码简单模拟:
java
class Kitchen {
public void cook() {
System.out.println("后厨:开始做主菜");
}
}
class DrinkBar {
public void makeDrink() {
System.out.println("饮品区:制作饮料");
}
}
class Cashier {
public void pay() {
System.out.println("收银台:完成结账");
}
}
class RestaurantFacade {
private Kitchen kitchen = new Kitchen();
private DrinkBar drinkBar = new DrinkBar();
private Cashier cashier = new Cashier();
public void orderSetMeal() {
cashier.pay();
kitchen.cook();
drinkBar.makeDrink();
System.out.println("前台:套餐准备完成");
}
}
public class Client {
public static void main(String[] args) {
RestaurantFacade restaurant = new RestaurantFacade();
restaurant.orderSetMeal();
}
}
客户端代码非常简单:
java
restaurant.orderSetMeal();
它不需要知道主菜怎么做、饮料谁负责、账单怎么处理。
回到软件开发中也是一样。很多业务功能表面上只是一个动作,但内部需要多个类协作完成。如果这些细节全部暴露给调用者,客户端代码就会越来越重。外观模式的作用,就是给复杂系统套上一层"服务窗口",让调用者通过一个简单入口完成复杂操作。
4.1 需求分析
本项目案例是模拟的是一个视频格式转换功能。
客户端的目标很明确:把一个视频文件转换成指定格式,例如把 youtubevideo.ogg 转换成 mp4。
但从代码可以看出,真正完成这个功能并不只是调用一个类,而是需要多个底层类协作:
VideoFile:根据文件名创建视频文件对象,并解析文件格式。CodecFactory:根据源视频文件提取对应的源编码器。MPEG4CompressionCodec:表示 MP4 目标编码器。OggCompressionCodec:表示 OGG 目标编码器。BitrateReader:负责读取视频内容并执行格式转换。AudioMixer:负责修复转换后的视频音频。Codec:编码器统一接口。
如果客户端直接调用这些类,就必须自己维护完整流程:
- 创建视频文件对象。
- 提取源文件编码器。
- 根据目标格式判断使用哪种编码器。
- 读取视频内容。
- 执行格式转换。
- 修复音频。
- 获取最终文件。
这会带来几个问题:
- 客户端依赖太多底层类。
- 调用顺序必须由客户端自己保证。
- 格式判断逻辑暴露在客户端。
- 多个地方需要视频转换时,流程代码会重复出现。
- 底层流程一旦变化,多个客户端都可能跟着修改。
因此,本案例需要一个统一的视频转换入口,把这些复杂步骤封装起来。
VideoConversionFacade 就承担了这个角色。它对外只提供:
java
public File convertVideo(String fileName, String format)
客户端只需要传入文件名和目标格式,剩下的编码器选择、读取、转换、音频修复,都交给外观类内部完成。
所以本案例的核心需求可以总结为:
为复杂的视频转换子系统提供一个简单入口,让客户端只关心"转换视频"这个业务动作,而不关心底层转换流程。
4.2 架构图
4.2.1 普通方式架构图
普通方式下,客户端直接依赖多个底层类。
创建文件对象
提取源编码器
选择 mp4 编码器
选择 ogg 编码器
读取与转换
修复音频
ApiTest
+test_no_design()
VideoFile
Codec
CodecFactory
MPEG4CompressionCodec
OggCompressionCodec
BitrateReader
AudioMixer
这种写法的问题在于:客户端既要知道"做什么",也要知道"怎么做"。调用者承担了流程编排责任。
4.2.2 外观模式架构图
引入外观模式后,客户端只依赖 VideoConversionFacade。
调用视频转换
ApiTest
+test_facade()
VideoConversionFacade
+convertVideo(fileName, format) : File
VideoFile
CodecFactory
MPEG4CompressionCodec
OggCompressionCodec
BitrateReader
AudioMixer
Codec
此时,客户端的依赖关系明显变简单了。底层复杂度没有消失,但被集中到了外观类内部。
4.3 时序图
4.3.1 普通代码时序图
AudioMixer BitrateReader CodecFactory VideoFile ApiTest.test_no_design AudioMixer BitrateReader CodecFactory VideoFile ApiTest.test_no_design new VideoFile(fileName) extract(file) sourceCodec 根据 format 选择目标编码器 read(file, sourceCodec) buffer convert(buffer, destinationCodec) intermediateResult fix(intermediateResult) result
普通写法中,所有步骤都由客户端主动发起。流程越长,客户端越容易变得臃肿。
4.3.2 外观模式时序图
AudioMixer BitrateReader CodecFactory VideoFile VideoConversionFacade ApiTest.test_facade AudioMixer BitrateReader CodecFactory VideoFile VideoConversionFacade ApiTest.test_facade convertVideo(fileName, format) new VideoFile(fileName) extract(file) sourceCodec 根据 format 选择目标编码器 read(file, sourceCodec) buffer convert(buffer, destinationCodec) intermediateResult fix(intermediateResult) result result
外观写法中,客户端只发起一次调用。具体执行步骤由外观类内部完成。
4.4 代码分析
4.4.1 普通代码(if-else/硬编码)
ApiTest.test_no_design() 展示了不使用设计模式时的写法。
java
@Test
public void test_no_design(){
String fileName = "youtubevideo.ogg";
String format = "mp4";
VideoFile file = new VideoFile(fileName);
Codec sourceCodec = CodecFactory.extract(file);
Codec destinationCodec;
if (format.equals("mp4")) {
destinationCodec = new MPEG4CompressionCodec();
} else {
destinationCodec = new OggCompressionCodec();
}
VideoFile buffer = BitrateReader.read(file, sourceCodec);
VideoFile intermediateResult = BitrateReader.convert(buffer, destinationCodec);
File result = (new AudioMixer()).fix(intermediateResult);
}
这段代码能够完成视频转换,但存在几个明显问题:
- 客户端知道了太多底层类。
- 客户端必须维护正确的调用顺序。
if (format.equals("mp4"))这样的格式选择逻辑直接暴露在调用端。- 如果转换流程变化,调用端也要跟着变化。
- 如果多个地方都需要视频转换,这段流程很容易被复制多份。
这类代码的坏味道不一定是"复杂到无法阅读",而是"复杂度放错了位置"。客户端只是想转换视频,却被迫参与了转换流程的细节编排。
4.4.2 外观模式代码
VideoConversionFacade 是本案例的外观类。
java
public class VideoConversionFacade {
public File convertVideo(String fileName, String format) {
System.out.println("VideoConversionFacade: conversion started.");
VideoFile file = new VideoFile(fileName);
Codec sourceCodec = CodecFactory.extract(file);
Codec destinationCodec;
if (format.equals("mp4")) {
destinationCodec = new MPEG4CompressionCodec();
} else {
destinationCodec = new OggCompressionCodec();
}
VideoFile buffer = BitrateReader.read(file, sourceCodec);
VideoFile intermediateResult = BitrateReader.convert(buffer, destinationCodec);
File result = (new AudioMixer()).fix(intermediateResult);
System.out.println("VideoConversionFacade: conversion completed.");
return result;
}
}
客户端调用代码变成:
java
@Test
public void test_facade(){
VideoConversionFacade converter = new VideoConversionFacade();
File mp4Video = converter.convertVideo("youtubevideo.ogg", "mp4");
}
外观类承担了三件事:
- 封装流程:把多个子系统调用组合成一个完整的视频转换流程。
- 屏蔽细节:客户端不再关心编码器选择、码率读取、音频修复等步骤。
- 降低依赖:客户端只依赖
VideoConversionFacade,而不是直接依赖一组媒体库类。
需要注意的是,外观模式并没有让底层代码消失。它只是把复杂度从客户端移动到了更合适的位置。
这样的设计让子系统类继续保持自己的职责,同时让客户端获得更简单的调用体验。
总结
外观模式的核心价值,就是给复杂子系统提供一个简单入口。
它并不是把复杂逻辑消灭了,而是把复杂逻辑收拢到更合适的位置。
客户端不再直接面对一堆底层类,也不需要关心调用顺序、对象创建、流程编排这些细节,只需要调用外观类提供的高层方法。
外观模式带来的变化:把复杂留给内部,把简单交给外部。
外观模式适合用在这些场景:
- 子系统类很多,调用链比较长。
- 客户端只需要使用某个高层功能。
- 多个地方重复编写同一套流程代码。
- 希望降低客户端和底层系统之间的耦合。
- 希望给复杂模块提供一个统一访问入口。
但使用时也要注意:外观类不要变成"万能类"。它应该封装的是一组明确的业务流程,而不是把所有功能都堆进去。
否则,外观类本身也会变成新的复杂点。