【软考设计模式】外观模式:复杂子系统的“门面“封装与代码填空精讲 系列定位软考软件设计师 / 系统架构设计师 — 结构型模式专题第 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(音响关闭); } } // SubsystemDVD 播放器 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, userexample.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、ResultSetFacade 职责封装固定调用顺序封装业务流程 异常回滚封装复杂 API简化调用子系统创建方式外部注入构造器参数Facade 内部创建Facade 内部创建是否有返回值无void有boolean 成功/失败有ResultSet、int异常处理无有回滚库存、退款有事务回滚Client 复杂度极低只需 1 个方法极低只需 1 个方法极低只需 1 个方法软考考法下午大题补全调用顺序下午大题补全业务流程上午选择题识别简化 API七、外观模式 vs 适配器模式 vs 代理模式核心对比对比项外观模式适配器模式代理模式设计目的简化子系统的使用提供高层接口转换接口兼容不兼容的类控制对象的访问延迟加载、权限等接口变化提供新的高层接口封装多个子接口转换已有接口为另一个接口保持原接口不变子系统数量通常涉及多个子系统类通常涉及一个Adaptee通常涉及一个RealSubjectClient 调用只调用 Facade调用 AdapterAdapter 内部调用 Adaptee调用 ProxyProxy 内部调用 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 依赖多个 SubsystemMediator 与多个 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 与子系统的组合而不是对象的创建问题。软考上午题如果问以下属于创建型模式的是选项里出现外观不能选。陷阱 2Facade 继承子系统类这是软考常见错误写法。Facade 应该通过组合持有子系统引用来使用子系统而不是继承子系统。如果 Facade 继承了子系统类那就破坏了子系统的独立性也违背了外观模式的初衷。软考代码填空里Facade 与子系统之间是关联/依赖关系Facade 类里有子系统的成员变量不是继承关系。陷阱 3认为外观阻止了直接访问子系统外观模式不阻止客户端直接访问子系统。Facade 只是提供了一个简化的入口如果客户端需要更细粒度的控制仍然可以直接调用子系统类。如果题干说外观模式强制所有调用必须通过 Facade那是错误的。陷阱 4与适配器模式混淆表格外观模式适配器模式封装多个子系统的复杂调用转换一个类的接口提供新的高层接口将已有接口转为另一个接口目的是简化使用目的是兼容接口如果类图里 Facade 连接多个子系统且 Client 只连 Facade → 外观。如果 Adapter 连接 Target 和 Adaptee → 适配器。陷阱 5与代理模式混淆外观模式代理模式封装多个子系统控制一个对象的访问Client 知道自己在调用 FacadeClient 通常不知道自己在调用 Proxy目的是简化目的是控制陷阱 6与中介者模式混淆这是软考最难的辨析题。外观Client → Facade → Subsystem单向Facade 封装子系统供客户端使用。中介者Colleague ↔ Mediator ↔ Colleague双向Mediator 协调多个对等对象之间的交互。快速判断如果类图里有一个中心类多个子系统类只被中心类调用且客户端只与中心类交互 → 外观。如果多个对等对象都通过中心类互相通信 → 中介者。陷阱 7Facade 的构造器参数Facade 的构造器有两种设计方式内部创建在 Facade 构造器里new Subsystem()如场景二。外部注入通过构造器参数传入子系统实例如场景一。两种方式都算对但软考代码填空时要注意如果题干说 Facade 自行管理子系统生命周期则内部创建如果题干说 子系统由外部配置则外部注入。十一、总结要点内容定义为子系统中的一组接口提供一个统一的高层接口使得子系统更加容易使用分类结构型模式GoF 23 正式成员核心角色Facade外观、Subsystem子系统多个、Client客户端核心思想封装子系统的复杂调用逻辑提供一个简洁的高层入口设计原则最少知识原则迪米特法则客户端只需与 Facade 交互是否阻止直接访问❌ 不阻止。Facade 只是简化入口客户端仍可直接访问子系统与单例结合外观类通常设计为单例整个系统只需要一个门面与适配器区别外观简化多个子系统的使用适配器转换一个类的接口与代理区别外观简化多个子系统代理控制一个对象的访问与中介者区别外观单向封装子系统中介者双向协调对等对象软考重点代码填空补全 Facade 对子系统的顺序调用类图识别Facade 连接多个 Subsystem与中介者的辨析答题技巧看到 封装复杂子系统、提供统一入口、简化调用 → 外观看到 Facade 类封装多个子系统方法 → 确认外观系列预告下一篇将讲享元模式—— 当系统中存在大量相似对象、内存开销巨大时如何通过共享技术有效地支持大量细粒度的对象。咱们下回见。