设计模式 · 外观模式(Facade Pattern)

前言

在日常开发中,我们经常会遇到这样一类代码:业务目标本身很简单,但为了完成它,客户端不得不依次调用很多底层类。

以本项目为例,客户端只是想把一个视频文件转换成指定格式。但如果直接使用底层媒体处理库,就需要自己完成加载视频、识别源编码器、选择目标编码器、读取码率、执行转换、修复音频等一整套流程。

这类问题的核心不是“代码能不能跑”,而是“调用者是否被迫理解太多细节”。

外观模式正是为了解决这种复杂调用暴露给客户端的问题。

image-20260519203623122


一、核心定义

外观模式(Facade Pattern)是一种结构型设计模式。

它为复杂子系统提供一个统一、简洁的高层接口,让客户端通过这个接口完成操作,而不需要直接依赖子系统中的多个类。

可以把它理解成:

外观类负责把复杂流程组织好,客户端只调用一个简单方法。

**举例来说:**当你通过电话给商店下达订单时, 接线员就是该商店的所有服务和部门的外观。 接线员为你提供了一个同购物系统、 支付网关和各种送货服务进行互动的简单语音接口。

外观模式的目标不是替代子系统,也不是隐藏所有能力,而是为常见使用场景提供一个更友好的入口。


二、标准体系结构图

角色说明:

  • Client:客户端,只关心业务目标,不想理解子系统内部细节。
  • Facade:外观类,对外提供简洁接口,对内编排复杂调用。
  • Subsystem:子系统类,负责具体能力,通常数量较多、调用顺序也有要求。

外观模式并不禁止客户端直接访问子系统。它只是额外提供了一条更简单、更稳定的调用路径。


三、场景推演

三、场景推演

生活中有很多“外观模式”的例子。

比如我们去餐厅吃饭。顾客真正想做的事情很简单:点一份套餐。但餐厅内部实际要做很多事:前台接单、后厨备菜、厨师炒菜、饮品区做饮料、服务员上菜、收银系统结账。

如果没有“前台”这个统一入口,顾客可能要自己去找厨师、找饮品区、找收银员,整个过程就会非常混乱。

前台就像外观类:

  • 顾客只需要对前台说:“我要一份套餐。”
  • 前台负责把请求分发给后厨、饮品区、收银系统。
  • 顾客不需要知道餐厅内部怎么协作。

用代码简单模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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();
}
}

客户端代码非常简单:

1
restaurant.orderSetMeal();

它不需要知道主菜怎么做、饮料谁负责、账单怎么处理。

回到软件开发中也是一样。很多业务功能表面上只是一个动作,但内部需要多个类协作完成。如果这些细节全部暴露给调用者,客户端代码就会越来越重。外观模式的作用,就是给复杂系统套上一层“服务窗口”,让调用者通过一个简单入口完成复杂操作。


四、实战案例

4.1 需求分析

三、场景推演

生活中有很多“外观模式”的例子。

比如我们去餐厅吃饭。顾客真正想做的事情很简单:点一份套餐。但餐厅内部实际要做很多事:前台接单、后厨备菜、厨师炒菜、饮品区做饮料、服务员上菜、收银系统结账。

如果没有“前台”这个统一入口,顾客可能要自己去找厨师、找饮品区、找收银员,整个过程就会非常混乱。

前台就像外观类:

  • 顾客只需要对前台说:“我要一份套餐。”
  • 前台负责把请求分发给后厨、饮品区、收银系统。
  • 顾客不需要知道餐厅内部怎么协作。

用代码简单模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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();
}
}

客户端代码非常简单:

1
restaurant.orderSetMeal();

它不需要知道主菜怎么做、饮料谁负责、账单怎么处理。

回到软件开发中也是一样。很多业务功能表面上只是一个动作,但内部需要多个类协作完成。如果这些细节全部暴露给调用者,客户端代码就会越来越重。外观模式的作用,就是给复杂系统套上一层“服务窗口”,让调用者通过一个简单入口完成复杂操作。


4.1 需求分析

本项目案例是模拟的是一个视频格式转换功能。

客户端的目标很明确:把一个视频文件转换成指定格式,例如把 youtubevideo.ogg 转换成 mp4

但从代码可以看出,真正完成这个功能并不只是调用一个类,而是需要多个底层类协作

  • VideoFile:根据文件名创建视频文件对象,并解析文件格式。
  • CodecFactory:根据源视频文件提取对应的源编码器。
  • MPEG4CompressionCodec:表示 MP4 目标编码器。
  • OggCompressionCodec:表示 OGG 目标编码器。
  • BitrateReader:负责读取视频内容并执行格式转换。
  • AudioMixer:负责修复转换后的视频音频。
  • Codec:编码器统一接口。

如果客户端直接调用这些类,就必须自己维护完整流程:

  1. 创建视频文件对象。
  2. 提取源文件编码器。
  3. 根据目标格式判断使用哪种编码器。
  4. 读取视频内容。
  5. 执行格式转换。
  6. 修复音频。
  7. 获取最终文件。

这会带来几个问题:

  • 客户端依赖太多底层类。
  • 调用顺序必须由客户端自己保证。
  • 格式判断逻辑暴露在客户端。
  • 多个地方需要视频转换时,流程代码会重复出现。
  • 底层流程一旦变化,多个客户端都可能跟着修改。

因此,本案例需要一个统一的视频转换入口,把这些复杂步骤封装起来

VideoConversionFacade 就承担了这个角色。它对外只提供:

1
public File convertVideo(String fileName, String format)

客户端只需要传入文件名和目标格式,剩下的编码器选择、读取、转换、音频修复,都交给外观类内部完成。

所以本案例的核心需求可以总结为:

为复杂的视频转换子系统提供一个简单入口,让客户端只关心“转换视频”这个业务动作,而不关心底层转换流程。

4.2 架构图

4.2.1 普通方式架构图

普通方式下,客户端直接依赖多个底层类。

这种写法的问题在于:客户端既要知道“做什么”,也要知道“怎么做”。调用者承担了流程编排责任。

4.2.2 外观模式架构图

引入外观模式后,客户端只依赖 VideoConversionFacade

此时,客户端的依赖关系明显变简单了。底层复杂度没有消失,但被集中到了外观类内部。

4.3 时序图

4.3.1 普通代码时序图

普通写法中,所有步骤都由客户端主动发起。流程越长,客户端越容易变得臃肿。

4.3.2 外观模式时序图

外观写法中,客户端只发起一次调用。具体执行步骤由外观类内部完成。

4.4 代码分析

4.4.1 普通代码(if-else/硬编码)

ApiTest.test_no_design() 展示了不使用设计模式时的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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 是本案例的外观类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
}

客户端调用代码变成:

1
2
3
4
5
@Test
public void test_facade(){
VideoConversionFacade converter = new VideoConversionFacade();
File mp4Video = converter.convertVideo("youtubevideo.ogg", "mp4");
}

外观类承担了三件事:

  • 封装流程:把多个子系统调用组合成一个完整的视频转换流程。
  • 屏蔽细节:客户端不再关心编码器选择、码率读取、音频修复等步骤。
  • 降低依赖:客户端只依赖 VideoConversionFacade,而不是直接依赖一组媒体库类。

需要注意的是,外观模式并没有让底层代码消失。它只是把复杂度从客户端移动到了更合适的位置。

这样的设计让子系统类继续保持自己的职责,同时让客户端获得更简单的调用体验。


总结

外观模式的核心价值,就是给复杂子系统提供一个简单入口

它并不是把复杂逻辑消灭了,而是把复杂逻辑收拢到更合适的位置。

客户端不再直接面对一堆底层类,也不需要关心调用顺序、对象创建、流程编排这些细节,只需要调用外观类提供的高层方法。

外观模式带来的变化:把复杂留给内部,把简单交给外部。

外观模式适合用在这些场景:

  • 子系统类很多,调用链比较长。
  • 客户端只需要使用某个高层功能。
  • 多个地方重复编写同一套流程代码。
  • 希望降低客户端和底层系统之间的耦合。
  • 希望给复杂模块提供一个统一访问入口。

但使用时也要注意:外观类不要变成“万能类”。它应该封装的是一组明确的业务流程,而不是把所有功能都堆进去。

否则,外观类本身也会变成新的复杂点。