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

文章目录

    • 前言
    • 一、核心定义
    • 二、标准体系结构图
    • 三、场景推演
    • 三、场景推演
    • 四、实战案例
      • [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:编码器统一接口。

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

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

这会带来几个问题:

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

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

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,而不是直接依赖一组媒体库类。

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

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


总结

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

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

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

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

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

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

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

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

相关推荐
++==4 小时前
设计模式:单例模式和观察者模式实现方式以及优化
观察者模式·单例模式·设计模式
doubledong19944 小时前
分形世界与设计模式
设计模式
多加点辣也没关系4 小时前
设计模式-访问者模式
设计模式·访问者模式
咖啡八杯4 小时前
GoF设计模式——原型模式
java·后端·设计模式·原型模式
多加点辣也没关系4 小时前
设计模式-状态模式
设计模式·状态模式
多加点辣也没关系5 小时前
设计模式-备忘录模式
设计模式·备忘录模式
雪度娃娃5 小时前
行为型设计模式——中介者模式
microsoft·设计模式·中介者模式
多加点辣也没关系5 小时前
设计模式-中介者模式
设计模式·中介者模式
c++之路5 小时前
外观模式(Facade Pattern)
算法·外观模式