【软考设计模式】外观模式:复杂子系统的“门面“封装与代码填空精讲
系列定位:软考软件设计师 / 系统架构设计师 — 结构型模式专题第 4 讲
考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现
难度等级:⭐⭐☆☆☆(概念最直观,但与其他模式的辨析是失分点)
一、考纲定位与模式定义
1.1 考纲要求
外观模式在软考中属于结构型模式的入门内容。考察形式包括:
上午选择题:判断场景描述所属模式;识别外观模式与适配器模式、代理模式的区别;判断外观类是否屏蔽了子系统的内部细节
下午设计题:补全 Facade 类中对多个子系统方法的调用顺序;补全客户端通过 Facade 调用而非直接调用子系统的代码;识别类图中 Facade 与子系统之间的依赖/关联关系
1.2 模式定义
外观模式:为子系统中的一组接口提供一个统一的高层接口,这个接口使得子系统更加容易使用。
核心意图:当一个子系统由多个类组成,且这些类的调用顺序复杂、相互依赖时,客户端直接与子系统交互的成本很高(需要了解每个子系统类的接口、调用顺序、依赖关系)。外观模式引入一个"门面"(Facade)类,将子系统的复杂调用逻辑封装在 Facade 内部,客户端只需与 Facade 打交道,无需关心子系统的内部细节。
通俗理解:
你去餐厅吃饭,后厨有切配、炒菜、装盘、传菜等多个岗位(子系统)。作为顾客(客户端),你不需要跑到后厨指挥每个岗位 "先切配、再炒菜、再装盘",你只需要对服务员(Facade)说 "来一份宫保鸡丁",服务员 internally 协调后厨完成所有步骤,最终把菜端给你。服务员就是后厨子系统的 "门面"。
关键原则:外观模式不阻止客户端直接访问子系统,只是提供了一个更简单的入口。如果客户端需要更细粒度的控制,仍然可以直接调用子系统类。
二、UML 类图与角色划分
┌─────────────────────────────────────────────────────────┐ │ Client │ │ + main() │ │ ├── Facade facade = new Facade(); │ │ └── facade.doSomething(); // 一键调用,无需关心内部 │ └───────────────────────┬─────────────────────────────────┘ │ uses ▼ ┌─────────────────────────────────────────────────────────┐ │ Facade │ │ ── 门面类:封装子系统的复杂调用逻辑 ── │ │ - subA: SubsystemA │ │ - subB: SubsystemB │ │ - subC: SubsystemC │ │ + doSomething() { │ │ subA.operationA(); │ │ subB.operationB(); │ │ subC.operationC(); │ │ } │ └───────────────────────┬─────────────────────────────────┘ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ SubsystemA │ │ SubsystemB │ │ SubsystemC │ │ + operationA() │ │ + operationB() │ │ + operationC() │ │ (子系统A) │ │ (子系统B) │ │ (子系统C) │ └─────────────────┘ └─────────────────┘ └─────────────────┘| 角色 | 职责 | 软考填空关键词 |
|---|---|---|
| Facade(外观) | 封装子系统的复杂调用逻辑,提供一个简洁的高层接口 | class+ 组合多个子系统引用 + 统一方法 |
| Subsystem(子系统) | 子系统内部的各个类,完成具体的子功能 | 多个独立的类,各自有特定方法 |
| Client(客户端) | 通过 Facade 访问子系统,不直接与子系统类交互 | new Facade()+facade.method() |
类图识别要点:软考类图中,外观模式的核心特征是存在一个Facade 类,它同时与多个子系统类有关联/依赖关系(箭头从 Facade 指向多个 Subsystem),而 Client 只与 Facade 交互。如果类图里只有一个类连接多个子系统,且 Client 不直接连子系统 → 外观。如果 Client 直接连子系统,且有一个类做接口转换 → 适配器。
三、场景一:家庭影院一键观影(最经典的外观模式)
业务背景:家庭影院子系统包含多个设备:投影仪(Projector)、音响(SoundSystem)、播放器(DVDPlayer)、灯光(Light)。要看一场电影,需要按顺序操作:关灯 → 放下幕布 → 打开投影仪 → 打开音响 → 放入光盘 → 播放。如果客户端直接操作每个设备,调用顺序复杂且容易出错。使用外观模式封装 "一键观影" 流程。
说明:这是外观模式最经典的例子,也是软考下午题最爱考的 "将多个子系统调用封装为一个方法" 场景。
3.1 代码实现
// Subsystem:投影仪 public class Projector { public void on() { System.out.println("投影仪:开启"); } public void setInput(String input) { System.out.println("投影仪:切换到 " + input + " 输入源"); } public void off() { System.out.println("投影仪:关闭"); } } // Subsystem:音响系统 public class SoundSystem { public void on() { System.out.println("音响:开启"); } public void setVolume(int level) { System.out.println("音响:音量调至 " + level); } public void off() { System.out.println("音响:关闭"); } } // Subsystem:DVD 播放器 public class DVDPlayer { public void on() { System.out.println("DVD 播放器:开启"); } public void play(String movie) { System.out.println("DVD 播放器:开始播放《" + movie + "》"); } public void off() { System.out.println("DVD 播放器:关闭"); } } // Subsystem:灯光控制 public class Light { public void dim() { System.out.println("灯光:调暗"); } public void bright() { System.out.println("灯光:调亮"); } } // Subsystem:幕布 public class Screen { public void down() { System.out.println("幕布:放下"); } public void up() { System.out.println("幕布:收起"); } } // Facade:家庭影院外观 public class HomeTheaterFacade { private Projector projector; private SoundSystem sound; private DVDPlayer dvd; private Light light; private Screen screen; public HomeTheaterFacade(Projector projector, SoundSystem sound, DVDPlayer dvd, Light light, Screen screen) { this.projector = projector; this.sound = sound; this.dvd = dvd; this.light = light; this.screen = screen; } public void watchMovie(String movie) { System.out.println("===== 准备观影 ====="); light.dim(); screen.down(); projector.on(); projector.setInput("DVD"); sound.on(); sound.setVolume(20); dvd.on(); dvd.play(movie); System.out.println("===== 开始观影 =====\n"); } public void endMovie() { System.out.println("===== 结束观影 ====="); dvd.off(); sound.off(); projector.off(); screen.up(); light.bright(); System.out.println("===== 已恢复 =====\n"); } } // Client:客户端 public class Client { public static void main(String[] args) { HomeTheaterFacade theater = new HomeTheaterFacade( new Projector(), new SoundSystem(), new DVDPlayer(), new Light(), new Screen() ); theater.watchMovie("星际穿越"); theater.endMovie(); } }关键体会:watchMovie()方法内部封装了 6 个子系统调用,按固定顺序执行。客户端只需调用watchMovie("星际穿越"),完全不需要知道投影仪、音响、DVD 等子系统的存在。这就是 "门面" 的价值 ——简化接口,降低耦合。
四、场景二:电商订单处理系统(业务子系统封装)
业务背景:电商系统下单流程涉及多个子系统:库存检查(Inventory)、支付处理(Payment)、物流下单(Shipping)、邮件通知(Notification)。一个完整的下单流程需要:检查库存 → 扣减库存 → 处理支付 → 创建物流单 → 发送确认邮件。使用外观模式封装 "一键下单" 流程。
说明:这是软考下午题中常见的 "业务流程封装" 场景,Facade 的方法对应一个完整的业务流程。
4.1 代码实现
java
// Subsystem:库存系统 public class InventoryService { public boolean checkStock(String productId, int quantity) { System.out.println("[库存] 检查商品 " + productId + " 库存,数量: " + quantity); return true; } public void deductStock(String productId, int quantity) { System.out.println("[库存] 扣减商品 " + productId + " 库存 " + quantity + " 件"); } public void restoreStock(String productId, int quantity) { System.out.println("[库存] 恢复商品 " + productId + " 库存 " + quantity + " 件"); } } // Subsystem:支付系统 public class PaymentService { public boolean processPayment(String orderId, double amount) { System.out.println("[支付] 处理订单 " + orderId + " 支付,金额: ¥" + amount); return true; } public void refund(String orderId) { System.out.println("[支付] 订单 " + orderId + " 退款完成"); } } // Subsystem:物流系统 public class ShippingService { public String createShippingOrder(String orderId, String address) { String trackingNo = "SF" + System.currentTimeMillis(); System.out.println("[物流] 创建运单,订单: " + orderId + ", 单号: " + trackingNo); return trackingNo; } } // Subsystem:通知系统 public class NotificationService { public void sendOrderConfirmation(String email, String orderId) { System.out.println("[通知] 发送订单确认邮件到 " + email + ",订单号: " + orderId); } } // Facade:订单处理外观 public class OrderFacade { private InventoryService inventory; private PaymentService payment; private ShippingService shipping; private NotificationService notification; public OrderFacade() { this.inventory = new InventoryService(); this.payment = new PaymentService(); this.shipping = new ShippingService(); this.notification = new NotificationService(); } public boolean placeOrder(String productId, int quantity, double amount, String email, String address) { String orderId = "ORD" + System.currentTimeMillis(); try { if (!inventory.checkStock(productId, quantity)) { System.out.println("下单失败:库存不足"); return false; } inventory.deductStock(productId, quantity); if (!payment.processPayment(orderId, amount)) { inventory.restoreStock(productId, quantity); System.out.println("下单失败:支付未成功"); return false; } String trackingNo = shipping.createShippingOrder(orderId, address); notification.sendOrderConfirmation(email, orderId); System.out.println("下单成功!订单号: " + orderId + ",运单号: " + trackingNo); return true; } catch (Exception e) { inventory.restoreStock(productId, quantity); System.out.println("下单失败:系统异常"); return false; } } } // Client public class Client { public static void main(String[] args) { OrderFacade orderFacade = new OrderFacade(); boolean success = orderFacade.placeOrder( "SKU1001", 2, 599.0, "user@example.com", "北京市海淀区" ); } }关键体会:Facade 不仅封装了调用顺序,还处理了异常回滚(支付失败时恢复库存)。这是实际开发中外观模式的常见用法 —— Facade 作为一个 "事务协调者",保证子系统调用的一致性。
五、场景三:数据库操作外观(简化复杂 API)
业务背景:JDBC 操作数据库需要多个步骤:加载驱动 → 获取连接 → 创建语句 → 执行 SQL → 处理结果集 → 关闭资源。使用外观模式封装常用的 CRUD 操作,让客户端只需调用query()或update()即可。
说明:这是实际开发中外观模式的简化应用,也是软考上午题中 "简化 API" 场景的典型代表。
5.1 代码实现
import java.sql.*; // Facade:数据库操作外观 public class DatabaseFacade { private String url; private String user; private String password; public DatabaseFacade(String url, String user, String password) { this.url = url; this.user = user; this.password = password; } public ResultSet query(String sql) { Connection conn = null; Statement stmt = null; ResultSet rs = null; try { conn = DriverManager.getConnection(url, user, password); stmt = conn.createStatement(); rs = stmt.executeQuery(sql); return rs; } catch (SQLException e) { throw new RuntimeException("查询失败", e); } } public int update(String sql) { try (Connection conn = DriverManager.getConnection(url, user, password); Statement stmt = conn.createStatement()) { return stmt.executeUpdate(sql); } catch (SQLException e) { throw new RuntimeException("更新失败", e); } } public boolean executeTransaction(String... sqls) { try (Connection conn = DriverManager.getConnection(url, user, password)) { conn.setAutoCommit(false); try (Statement stmt = conn.createStatement()) { for (String sql : sqls) { stmt.executeUpdate(sql); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); return false; } } catch (SQLException e) { throw new RuntimeException("事务执行失败", e); } } } // Client public class Client { public static void main(String[] args) { DatabaseFacade db = new DatabaseFacade( "jdbc:mysql://localhost:3306/test", "root", "123456" ); int rows = db.update("UPDATE users SET status = 1 WHERE id = 1001"); System.out.println("影响行数: " + rows); boolean ok = db.executeTransaction( "UPDATE account SET balance = balance - 100 WHERE id = 1", "UPDATE account SET balance = balance + 100 WHERE id = 2" ); System.out.println("转账结果: " + ok); } }六、三种场景对比与演进思路
| 维度 | 场景一:家庭影院 | 场景二:电商下单 | 场景三:数据库操作 |
|---|---|---|---|
| 子系统数量 | 5 个(投影仪、音响、DVD、灯光、幕布) | 4 个(库存、支付、物流、通知) | 多个 JDBC 类(Connection、Statement、ResultSet) |
| Facade 职责 | 封装固定调用顺序 | 封装业务流程 + 异常回滚 | 封装复杂 API,简化调用 |
| 子系统创建方式 | 外部注入(构造器参数) | Facade 内部创建 | Facade 内部创建 |
| 是否有返回值 | 无(void) | 有(boolean 成功/失败) | 有(ResultSet、int) |
| 异常处理 | 无 | 有(回滚库存、退款) | 有(事务回滚) |
| Client 复杂度 | 极低(只需 1 个方法) | 极低(只需 1 个方法) | 极低(只需 1 个方法) |
| 软考考法 | 下午大题(补全调用顺序) | 下午大题(补全业务流程) | 上午选择题(识别简化 API) |
七、外观模式 vs 适配器模式 vs 代理模式:核心对比
| 对比项 | 外观模式 | 适配器模式 | 代理模式 |
|---|---|---|---|
| 设计目的 | 简化子系统的使用,提供高层接口 | 转换接口,兼容不兼容的类 | 控制对象的访问(延迟加载、权限等) |
| 接口变化 | 提供新的高层接口(封装多个子接口) | 转换已有接口为另一个接口 | 保持原接口不变 |
| 子系统数量 | 通常涉及多个子系统类 | 通常涉及一个Adaptee | 通常涉及一个RealSubject |
| Client 调用 | 只调用 Facade | 调用 Adapter(Adapter 内部调用 Adaptee) | 调用 Proxy(Proxy 内部调用 RealSubject) |
| 是否隐藏子系统 | 部分隐藏(Client 可以选择直接访问子系统) | 完全隐藏(Client 不知道 Adaptee) | 完全透明(Client 以为在调用 RealSubject) |
| 软考判断 | 一个类封装多个子系统的调用 | 一个类转换两个不同接口 | 一个类控制对另一个类的访问 |
记忆口诀:外观是 "简化入口"(封装复杂子系统),适配器是 "翻译官"(接口转换),代理是 "中间人"(控制访问)。
八、软考高频考点与易混淆辨析
8.1 高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 结构型模式(GoF 23 正式成员) |
| 核心角色 | Facade(外观)、Subsystem(子系统,多个)、Client(客户端) |
| 核心思想 | 为子系统提供统一的高层接口,降低客户端与子系统的耦合 |
| 设计原则 | 最少知识原则(迪米特法则):客户端只需与 Facade 交互,不需要了解子系统内部 |
| 是否阻止直接访问 | ❌ 不阻止。外观只是提供一个简化入口,客户端仍可直接访问子系统 |
| 与单例结合 | 外观类通常设计为单例(整个系统只需要一个门面) |
| 适用场景 | ① 子系统复杂,客户端调用成本高 ② 需要分层架构(层与层之间通过外观交互)③ 需要解耦客户端与子系统 |
| 与中介者区别 | 外观:Client → Facade → Subsystem(单向,Facade 封装子系统);中介者:Colleague ↔ Mediator ↔ Colleague(双向,协调多个对等对象) |
8.2 易混淆辨析:外观 vs 中介者
| 对比项 | 外观模式 | 中介者模式 |
|---|---|---|
| 交互方向 | 单向:Client → Facade → Subsystem | 双向:Colleague ↔ Mediator ↔ Colleague |
| 对象关系 | Facade 封装子系统(子系统不知道 Facade 的存在) | Mediator 协调对等对象(同事对象知道 Mediator 的存在) |
| 目的 | 简化客户端对子系统的使用 | 解耦多个对象之间的相互引用 |
| 类图结构 | Facade 依赖多个 Subsystem | Mediator 与多个 Colleague 双向关联 |
| 软考判断 | 一个类封装多个子系统供客户端使用 | 一个类协调多个对等对象之间的交互 |
九、真题风格模拟与代码填空
模拟题 1(上午选择题)
以下关于外观模式的叙述中,正确的是()。
A. 外观模式属于创建型模式,主要用于创建子系统的统一入口
B. 在外观模式中,Facade 类通过继承子系统类来简化客户端调用
C. 外观模式为子系统中的一组接口提供一个统一的高层接口,使得子系统更加容易使用
D. 外观模式会阻止客户端直接访问子系统的内部类,强制所有调用必须通过 Facade
答案:C
解析:
A 错误:外观属于结构型模式,不是创建型。
B 错误:Facade 通过组合(持有子系统引用)来调用子系统,不是继承。外观模式不修改子系统的继承结构。
C 正确:这是外观模式的核心定义。
D 错误:外观模式不阻止客户端直接访问子系统。Facade 只是提供一个简化入口,如果客户端需要更细粒度的控制,仍然可以直接调用子系统类。
模拟题 2(下午代码填空 — 补全外观类封装逻辑)
某系统使用外观模式封装编译子系统。编译流程包括:词法分析(Lexer)、语法分析(Parser)、代码生成(CodeGenerator)。请补全(1)~(4)。
// Subsystem:词法分析 class Lexer { public String tokenize(String source) { System.out.println("词法分析完成"); return "tokens"; } } // Subsystem:语法分析 class Parser { public String parse(String tokens) { System.out.println("语法分析完成"); return "ast"; } } // Subsystem:代码生成 class CodeGenerator { public String generate(String ast) { System.out.println("代码生成完成"); return "machine_code"; } } // Facade:编译外观 class CompilerFacade { private Lexer lexer; private Parser parser; private CodeGenerator generator; public CompilerFacade() { this.lexer = new Lexer(); this.parser = new Parser(); this.generator = new CodeGenerator(); } // 一键编译 public String compile(String sourceCode) { String tokens = (1)______; String ast = (2)______; String code = (3)______; return (4)______; } } public class Client { public static void main(String[] args) { CompilerFacade compiler = new CompilerFacade(); String result = compiler.compile("int main() { return 0; }"); System.out.println("编译结果: " + result); } }答案:
(1)
lexer.tokenize(sourceCode)(2)
parser.parse(tokens)(3)
generator.generate(ast)(4)
code
阅卷要点:
(1)(2)(3) 必须体现 Facade 对子系统方法的顺序调用。顺序不能错(先词法、再语法、再生成),但方法名必须准确。
(4) 必须是
code(或generator.generate(ast)),返回最终的编译结果。如果写tokens或ast会扣分,因为那不是最终产物。注意:Facade 内部的方法调用是通过组合的成员变量(
lexer、parser、generator)调用,不是通过类名直接调用静态方法。
模拟题 3(下午代码填空 — 外观类与单例结合)
某系统的外观类
ServiceFacade需要设计为单例,封装UserService和OrderService两个子系统。请补全(1)~(3)。
class UserService { public String getUser(String id) { return "User:" + id; } } class OrderService { public String getOrder(String userId) { return "Order of " + userId; } } class ServiceFacade { private static final ServiceFacade (1)______ = new ServiceFacade(); private UserService userService; private OrderService orderService; private ServiceFacade() { this.userService = new UserService(); this.orderService = new OrderService(); } public static ServiceFacade (2)______() { return (3)______; } public String getUserOrderInfo(String userId) { String user = userService.getUser(userId); String order = orderService.getOrder(userId); return user + " -> " + order; } } public class Client { public static void main(String[] args) { String info = ServiceFacade.getInstance().getUserOrderInfo("1001"); System.out.println(info); } }答案:
(1)
INSTANCE(2)
getInstance(3)
INSTANCE
阅卷要点:
(1) 必须是
INSTANCE(或instance),这是单例的标准命名。static final修饰表示饿汉式单例。(2) 必须是
getInstance,这是单例的标准获取方法名。(3) 必须是
INSTANCE,返回单例实例。不能写new ServiceFacade()(构造器是 private)。此题结合了单例模式和外观模式,是软考下午题中常见的组合考法。
十、常见陷阱与注意事项
陷阱 1:误认为外观是创建型模式
外观属于结构型模式,因为它解决的是类与类之间的结构关系(Facade 与子系统的组合),而不是对象的创建问题。软考上午题如果问"以下属于创建型模式的是",选项里出现"外观"不能选。
陷阱 2:Facade 继承子系统类
这是软考常见错误写法。Facade 应该通过组合(持有子系统引用)来使用子系统,而不是继承子系统。如果 Facade 继承了子系统类,那就破坏了子系统的独立性,也违背了外观模式的初衷。软考代码填空里,Facade 与子系统之间是关联/依赖关系(Facade 类里有子系统的成员变量),不是继承关系。
陷阱 3:认为外观阻止了直接访问子系统
外观模式不阻止客户端直接访问子系统。Facade 只是提供了一个简化的入口,如果客户端需要更细粒度的控制,仍然可以直接调用子系统类。如果题干说"外观模式强制所有调用必须通过 Facade",那是错误的。
陷阱 4:与适配器模式混淆
表格
| 外观模式 | 适配器模式 |
|---|---|
| 封装多个子系统的复杂调用 | 转换一个类的接口 |
| 提供新的高层接口 | 将已有接口转为另一个接口 |
| 目的是简化使用 | 目的是兼容接口 |
如果类图里 Facade 连接多个子系统,且 Client 只连 Facade → 外观。如果 Adapter 连接 Target 和 Adaptee → 适配器。
陷阱 5:与代理模式混淆
| 外观模式 | 代理模式 |
|---|---|
| 封装多个子系统 | 控制一个对象的访问 |
| Client 知道自己在调用 Facade | Client 通常不知道自己在调用 Proxy |
| 目的是简化 | 目的是控制 |
陷阱 6:与中介者模式混淆
这是软考最难的辨析题。
外观:Client → Facade → Subsystem(单向,Facade 封装子系统供客户端使用)。
中介者:Colleague ↔ Mediator ↔ Colleague(双向,Mediator 协调多个对等对象之间的交互)。
快速判断:如果类图里有一个中心类,多个子系统类只被中心类调用,且客户端只与中心类交互 → 外观。如果多个对等对象都通过中心类互相通信 → 中介者。
陷阱 7:Facade 的构造器参数
Facade 的构造器有两种设计方式:
内部创建:在 Facade 构造器里
new Subsystem()(如场景二)。外部注入:通过构造器参数传入子系统实例(如场景一)。
两种方式都算对,但软考代码填空时要注意:如果题干说 "Facade 自行管理子系统生命周期",则内部创建;如果题干说 "子系统由外部配置",则外部注入。
十一、总结
| 要点 | 内容 |
|---|---|
| 定义 | 为子系统中的一组接口提供一个统一的高层接口,使得子系统更加容易使用 |
| 分类 | 结构型模式(GoF 23 正式成员) |
| 核心角色 | Facade(外观)、Subsystem(子系统,多个)、Client(客户端) |
| 核心思想 | 封装子系统的复杂调用逻辑,提供一个简洁的高层入口 |
| 设计原则 | 最少知识原则(迪米特法则):客户端只需与 Facade 交互 |
| 是否阻止直接访问 | ❌ 不阻止。Facade 只是简化入口,客户端仍可直接访问子系统 |
| 与单例结合 | 外观类通常设计为单例(整个系统只需要一个门面) |
| 与适配器区别 | 外观:简化多个子系统的使用;适配器:转换一个类的接口 |
| 与代理区别 | 外观:简化多个子系统;代理:控制一个对象的访问 |
| 与中介者区别 | 外观:单向封装子系统;中介者:双向协调对等对象 |
| 软考重点 | 代码填空(补全 Facade 对子系统的顺序调用);类图识别(Facade 连接多个 Subsystem);与中介者的辨析 |
| 答题技巧 | 看到 "封装复杂子系统"、"提供统一入口"、"简化调用" → 外观;看到 Facade 类封装多个子系统方法 → 确认外观 |
系列预告:下一篇将讲享元模式—— 当系统中存在大量相似对象、内存开销巨大时,如何通过共享技术有效地支持大量细粒度的对象。咱们下回见。
