你定义的门面接口其实在用外观模式——但99%的人把它用成了垃圾堆
见过太多这种代码了:
@Servicepublic class OrderFacade {
@Autowired
private OrderDao orderDao;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
@Autowired
private LogisticsService logisticsService;
@Autowired
private CouponService couponService;
@Autowired
private NotificationService notificationService;
@Autowired
private RiskControlService riskControlService;
@Autowired
private 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 行,这个门面价值不大。
门面应该是薄薄的一层调度器,不是把所有东西搅在一起的大杂烩。
---
我们团队在做一个叫「爪爪代码冒险记」的微信小程序,用卡皮巴拉漫画讲设计模式。外观模式那关被设计成「遥控器」主题——卡皮巴拉用一个遥控器控制整个智能家居系统。如果感兴趣可以搜一下,或者等我后面的文章。
