你定义的门面接口其实在用外观模式——但99%的人把它用成了垃圾堆

📅 2026/6/20 5:45:54
你定义的门面接口其实在用外观模式——但99%的人把它用成了垃圾堆
见过太多这种代码了Servicepublic class OrderFacade {Autowiredprivate OrderDao orderDao;Autowiredprivate InventoryService inventoryService;Autowiredprivate PaymentService paymentService;Autowiredprivate LogisticsService logisticsService;Autowiredprivate CouponService couponService;Autowiredprivate NotificationService notificationService;Autowiredprivate RiskControlService riskControlService;Autowiredprivate AccountingService accountingService;// 九个依赖还觉得自己写得挺对public OrderResult placeOrder(OrderRequest request) {// 300 行代码揉在一起// 校验、风控、库存、优惠券、支付、记账、物流、通知...}}这不是门面模式。这叫把所有垃圾倒在一个桶里。门面Facade本来想干嘛外观模式的定义出奇地朴素**为子系统中的一组接口提供一个统一的入口**。它不添加新功能只是把复杂的内部细节藏起来给外部一个简单的界面。你打开电脑按一下开机键主板通电、BIOS 自检、操作系统加载、驱动初始化——所有这些对你来说就是一个按钮。这就是门面。在代码里最直观的例子是编译器的前端// 没有门面——调用方需要了解编译的每一步内部细节Lexer lexer new Lexer(sourceCode);List tokens lexer.tokenize();Parser parser new Parser(tokens);AST ast parser.parse();SemanticAnalyzer analyzer new SemanticAnalyzer(ast);analyzer.analyze();CodeGenerator generator new CodeGenerator(ast);Bytecode bytecode generator.generate();// 有了门面——调用方只需要关心输入和输出Compiler compiler new Compiler();Bytecode bytecode compiler.compile(sourceCode);内部四步流程一个都不少但调用者不需要知道 Lexer、Parser、SemanticAnalyzer 的存在。这就是门面做的事**降低复杂度暴露面**。Spring 里把门面用得最好的地方JdbcTemplate 就是一个门面。JDBC 的原始操作有多繁琐不用多说了吧// 原始 JDBC —— 8 步操作每个都得手动处理Connection conn null;PreparedStatement stmt null;ResultSet rs null;try {conn dataSource.getConnection();stmt conn.prepareStatement(SELECT * FROM users WHERE id ?);stmt.setLong(1, userId);rs stmt.executeQuery();if (rs.next()) {User user new User();user.setId(rs.getLong(id));user.setName(rs.getString(name));return user;}return null;} catch (SQLException e) {throw new RuntimeException(e);} finally {if (rs ! null) try { rs.close(); } catch (SQLException ignored) {}if (stmt ! null) try { stmt.close(); } catch (SQLException ignored) {}if (conn ! null) try { conn.close(); } catch (SQLException ignored) {}}// JdbcTemplate —— 一行User user jdbcTemplate.queryForObject(SELECT * FROM users WHERE id ?,new BeanPropertyRowMapper(User.class),userId);JdbcTemplate的门面之下是DataSourceUtils管理连接、StatementCallback管理生命周期、SQLExceptionTranslator翻译异常——但使用者不需要知道这些。门面把复杂性封装在内部暴露一个干净的接口。另一个例子是 Spring MVC 的DispatcherServlet它是整个 Web 层的门面- 请求来了 → DispatcherServlet- HandlerMapping 找到哪个 Controller- HandlerAdapter 调用它- ViewResolver 渲染结果这一整套流程对外部Servlet 容器只有一个入口DispatcherServlet.service(request, response)。门面变垃圾堆的三个原因**第一个原因把门面当业务逻辑层。**这是最常见的错误。很多人觉得「门面层」就是「把所有逻辑堆到一个类里」。门面应该组织调用不应该包含具体的业务规则。// 错误门面里揉进了业务逻辑public class OrderFacade {public void cancelOrder(Long orderId) {Order order orderDao.findById(orderId);// 这些是业务规则不该在门面里if (order.getStatus() OrderStatus.SHIPPED) {throw new BusinessException(已发货订单不可取消);}if (order.getCreateTime().plusHours(2).isAfter(Instant.now())) {throw new BusinessException(下单超过2小时不可取消);}if (order.getAmount().compareTo(new BigDecimal(10000)) 0) {// 大额订单取消需要审批approvalService.submit(orderId);return;}orderDao.updateStatus(orderId, OrderStatus.CANCELLED);inventoryService.restore(order.getItems());paymentService.refund(orderId);notificationService.notifyCancellation(orderId);}}// 正确门面只做调度业务规则在下层public class OrderFacade {public void cancelOrder(Long orderId) {// 规则下沉到领域服务orderService.cancel(orderId); // 封装了所有业务规则// 基础设施调度inventoryService.restoreForOrder(orderId);paymentService.refundForOrder(orderId);notificationService.notify(OrderEvent.CANCELLED, orderId);}}**第二个原因把门面当上帝对象。**一个门面引用 9 个依赖处理 5 种不同类型的业务——拆分信号已经很明显了- 业务流程完全不同下单 vs 退款 vs 查询- 每次改一个功能都要动同一个类- 单元测试的 mock 对象比测试代码还多这时候应该按业务流程拆门面public class OrderCreationFacade {// 只管下单流程}public class OrderCancellationFacade {// 只管取消流程}public class OrderQueryFacade {// 只管查询}**第三个原因把异常处理堆在门面里。**public OrderResult placeOrder(OrderRequest req) {try {riskControlService.check(req.getUserId());} catch (RiskRejectedException e) {return OrderResult.reject(风控拒绝);}try {inventoryService.lock(req.getItems());} catch (InsufficientInventoryException e) {return OrderResult.fail(库存不足);}try {paymentService.charge(req.getUserId(), req.getAmount());} catch (PaymentFailedException e) {inventoryService.unlock(req.getItems()); // 回滚库存return OrderResult.fail(支付失败);}// ...}门面不应该知道每个子系统的异常类型。正确的方式是统一异常处理public OrderResult placeOrder(OrderRequest req) {try {riskControlService.check(req.getUserId());inventoryService.lock(req.getItems());paymentService.charge(req.getUserId(), req.getAmount());return OrderResult.success();} catch (BusinessException e) {compensationService.compensate(req); // 统一补偿return OrderResult.fail(e.getMessage());}}API 网关——门面模式的分布式版本如果你在做微服务Kong、Spring Cloud Gateway、Nginx 反代——这些都是门面模式在架构层面的体现。客户端只跟网关打交道客户端 → 网关 → [用户服务, 订单服务, 库存服务, 支付服务, ...]网关做的事跟代码层的门面一样隐藏内部复杂性。客户端不需要知道后端有多少个服务每个服务的地址是什么它们之间怎么通信。认证、限流、日志、路由——全在网关层面解决。但网关也有同样的陷阱**不要把业务逻辑写进网关**。网关负责路由和横切关注点不应该知道「取消订单前需要判断是否超过 2 小时」这种业务规则。门面 vs 适配器 vs 中介者这三个经常被搞混- **门面**简化复杂子系统的接口。你主动设计了一个新接口目的是让外部更容易使用。- **适配器**让不兼容的接口能一起工作。你被动地做了一个包装因为两边已经存在且没法改。- **中介者**协调多个对象之间的交互。你不只是转发你在管理对象之间的通信。简单记门面是一对多一个入口多个子系统适配器是一对一一个接口适配另一个中介者是多对多多个对象互相通信。门面最实用的自检问题下次写一个 Facade 类之前问自己1. 这个类里的 if-else 是不是跟业务规则有关是的话搬出去。2. 这个类的构造函数里注入了超过 5 个依赖考虑按流程拆。3. 如果去掉这个类调用方需要多写多少行代码如果不到 10 行这个门面价值不大。门面应该是薄薄的一层调度器不是把所有东西搅在一起的大杂烩。---我们团队在做一个叫「爪爪代码冒险记」的微信小程序用卡皮巴拉漫画讲设计模式。外观模式那关被设计成「遥控器」主题——卡皮巴拉用一个遥控器控制整个智能家居系统。如果感兴趣可以搜一下或者等我后面的文章。