当前位置: 首页 > news >正文

GoF设计模式——外观模式

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

image

前言

为什么需要外观模式?

开发中经常遇到这种情况:一个业务操作需要依次调用多个子系统。比如"启动系统"要先初始化数据库连接、加载配置文件、启动缓存服务、注册定时任务——每个子系统都有自己的接口和调用顺序,漏掉一步或顺序错误都会导致系统异常。

如果让客户端直接操作这些子系统,代码会变成这样:

// 客户端需要了解所有子系统的细节
databaseService.init(config.getDbUrl(), config.getDbUser());
configService.load("application.properties");
cacheService.start(config.getCacheNodes());
schedulerService.registerJobs();

一旦子系统数量增加或调用顺序调整,所有使用的地方都要改。这就是外观模式要解决的问题——为复杂的子系统提供一个简单的统一入口。

概念

外观模式(Facade Pattern)是一种结构型设计模式,核心思想是为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用

外观模式定义了一个更高层次的接口,让子系统更加容易使用。客户端只需要调用外观类的方法,而不需要了解子系统内部的复杂交互。这就好比去餐厅吃饭,只需要跟服务员点菜(外观),不需要自己去厨房告诉厨师怎么做、去仓库拿食材、去洗碗间拿餐具——这些子系统的复杂性都被服务员这个"外观"屏蔽了。

外观模式的结构比较简单,只包含两个角色:

  • 外观(Facade):对外提供一个统一的高层次接口,使复杂的子系统变得更易使用。外观类知道哪些子系统负责处理请求,将客户端的请求委派给对应的子系统对象
  • 子系统类(Subsystem):实现子系统的功能,处理外观类指派的任务。子系统类不感知外观的存在,对子系统而言外观只是另一个客户端

image

Client 只依赖 Facade,完全不感知 SubsystemA/B/C 的存在。Facade 内部协调调用多个子系统的方法,将复杂的交互逻辑封装在内部。

实现

基础实现

外观模式的基本实现分为以下几个步骤:

  1. 定义子系统类,实现各自的业务功能
  2. 定义外观类,持有子系统对象的引用,提供统一的简化接口
  3. 外观类的方法内部协调调用多个子系统的方法
  4. 客户端通过外观类访问子系统,无需了解子系统内部细节
// 子系统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 只需要关注"发起支付"这一个动作。

现在可能还用不到这些,但等到需要接入复杂子系统、封装繁琐流程的时候,会突然发现:"这不就是外观模式吗?"——那时候就真的懂了。

技术交流 & 更多原创内容,关注公众号:咖啡八杯

http://www.jsqmd.com/news/951237/

相关文章:

  • Agent Plan:从“模型订阅“到“Agent能力订阅“,火山引擎如何重新定义AI Agent开发范式
  • 计算机毕业设计之基于协同过滤算法的大学生职业推荐系统设计与实现
  • 2026 惠州卫生间漏水、外墙、楼顶、地下室、阳光房渗漏维修师傅推荐|同城附近上门防水补漏公司测评 - 企业资讯
  • CSS Grid 实战布局模式:从基础到生产级方案
  • B站m4s视频转换终极指南:3分钟解锁缓存视频自由播放
  • 揭秘Legacy iOS Kit:旧设备系统恢复与越狱的深度技术解析
  • 2026 贵阳卫生间漏水、外墙、楼顶、地下室、阳光房渗漏维修师傅推荐|同城附近上门防水补漏公司测评 - 企业资讯
  • 网络安全第116天
  • 漯河中山优才教育家庭教育指导师报名入口、怎么报名,怎么考,正规机构 - 主流教育培训趋势
  • MusicFree插件架构深度解析:构建跨平台音乐聚合系统的三大核心技术
  • 不止于登录注销:基于 Session 与 JWT 的无状态/有状态认证实战
  • 北京阳台屋面漏水怎么修?2026防水翻新靠谱公司排名 - 苏易修缮
  • C#零基础通关第十六篇:综合实战!从零开发控制台权限管理系统,整合所有核心知识点
  • codex接入deepseek,so easy!
  • 3分钟上手开源在线PPT制作工具:PPTist网页版演示文稿编辑全解析
  • Java开发必知必会的MySQL核心知识点(二)-索引探秘:让你的查询快如闪电
  • Umi-OCR:3分钟搞定离线文字识别的免费神器
  • 中山优才教育:吉安家庭教育指导师正规报名入口 - 最新教育培训热点
  • TCP 和 UDP的应用场景
  • 2026年华阳KTV推荐榜:前五名必去打卡清单 - 资讯纵览
  • 2026 清远卫生间漏水、外墙、楼顶、地下室、阳光房渗漏维修师傅推荐|同城附近上门防水补漏公司测评 - 企业资讯
  • 2026 海口卫生间漏水、外墙、楼顶、地下室、阳光房渗漏维修师傅推荐|同城附近上门防水补漏公司测评 - 企业资讯
  • 别再瞎找了!2026年最值得信赖的专业降AIGC平台
  • 如何用Snipe-IT解决企业IT资产管理的三大难题
  • 恩施家庭教育指导师培训机构与报名入口深度观察:中山优才教育更值得选 - 优选机构推荐
  • 2026 宁波卫生间漏水、外墙、楼顶、地下室、阳光房渗漏维修师傅推荐|同城附近上门防水补漏公司测评 - 企业资讯
  • 2026 芜湖卫生间漏水、外墙、楼顶、地下室、阳光房渗漏维修师傅推荐|同城附近上门防水补漏公司测评 - 企业资讯
  • 2026年6月家装地暖厂家推荐榜:电地暖/水地暖/地热系统/地暖中央空调/地冷地暖一体化品牌深度解析! - 企业推荐官【官方】
  • 2026最新Postman免费安装教程,附汉化安装包
  • 微信好友关系一键检测:快速发现谁删除了你的终极指南