外观模式
你接到一个任务:开发一个新订单系统,需要调用库存服务扣减库存、调用支付服务完成扣款、调用物流服务创建发货单。这三个服务各有十几个 API,参数校验规则不同,返回格式各异。你需要写多少代码?
没有外观模式的时代,你可能这样写:
// 订单创建
public void createOrder(OrderDTO order) {
// 1. 校验库存
InventoryRequest req1 = new InventoryRequest();
req1.setProductId(order.getProductId());
req1.setQuantity(order.getQuantity());
req1.setWarehouseCode("WH001");
InventoryResponse resp1 = inventoryService.reserve(req1);
if (resp1.getCode() != 200) {
throw new BusinessException("库存不足");
}
// 2. 扣款
PaymentRequest req2 = new PaymentRequest();
req2.setUserId(order.getUserId());
req2.setAmount(order.getAmount());
req2.setPaymentMethod(order.getPaymentMethod());
PaymentResponse resp2 = paymentService.deduct(req2);
if (resp2.getCode() != 200) {
// 回滚库存
inventoryService.release(req1);
throw new BusinessException("支付失败");
}
// 3. 创建物流
LogisticsRequest req3 = new LogisticsRequest();
req3.setOrderId(generateOrderId());
req3.setReceiverAddress(order.getAddress());
LogisticsResponse resp3 = logisticsService.create(req3);
// 4. 发送通知
notificationService.send(order.getUserId(), "订单创建成功");
}
每个服务都有自己的请求类、响应类、错误码体系。当需要创建订单时,调用方需要了解所有这些细节。有没有办法把这种复杂性封装起来,让调用方只关心「创建订单」这一个动作?
这就是外观模式的价值。
外观模式的核心思想
外观模式(Facade Pattern)为复杂的子系统提供一个统一的接口,使子系统更易于使用。外观对象将客户端请求委派给相应的子系统对象,处理实际的工作。
flowchart TB
Client[客户端] --> Facade[外观对象\nOrderFacade]
Facade --> SubSystem1[库存子系统\nInventoryService]
Facade --> SubSystem2[支付子系统\nPaymentService]
Facade --> SubSystem3[物流子系统\nLogisticsService]
Facade --> SubSystem4[通知子系统\nNotificationService]
SubSystem1 --> Lib1[InventoryRequest\nInventoryResponse]
SubSystem2 --> Lib2[PaymentRequest\nPaymentResponse]
SubSystem3 --> Lib3[LogisticsRequest\nLogisticsResponse]
外观模式的核心目标不是「添加新功能」,而是「简化现有接口」。它不取代子系统,只是包装了一层更友好的接口。
外观模式的实现
// 统一的订单请求
public class CreateOrderRequest {
private String userId;
private String productId;
private int quantity;
private BigDecimal amount;
private String paymentMethod;
private String address;
// getters and setters
}
// 统一的订单响应
public class CreateOrderResponse {
private boolean success;
private String orderId;
private String message;
// getters and setters
}
// 订单外观类
public class OrderFacade {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final LogisticsService logisticsService;
private final NotificationService notificationService;
public OrderFacade() {
this.inventoryService = new InventoryService();
this.paymentService = new PaymentService();
this.logisticsService = new LogisticsService();
this.notificationService = new NotificationService();
}
// 简化的统一接口
public CreateOrderResponse createOrder(CreateOrderRequest request) {
CreateOrderResponse response = new CreateOrderResponse();
try {
// 1. 扣减库存
InventoryResponse invResp = inventoryService.reserve(
request.getProductId(),
request.getQuantity()
);
if (!invResp.isSuccess()) {
return response.fail("库存不足");
}
// 2. 支付扣款
PaymentResponse payResp = paymentService.deduct(
request.getUserId(),
request.getAmount(),
request.getPaymentMethod()
);
if (!payResp.isSuccess()) {
// 回滚库存
inventoryService.release(request.getProductId(), request.getQuantity());
return response.fail("支付失败");
}
// 3. 创建物流
String orderId = generateOrderId();
LogisticsResponse logResp = logisticsService.create(
orderId,
request.getAddress()
);
// 4. 发送通知
notificationService.send(request.getUserId(), "订单创建成功");
return response.success(orderId);
} catch (Exception e) {
// 统一异常处理
return response.fail("系统异常: " + e.getMessage());
}
}
private String generateOrderId() {
return "ORD" + System.currentTimeMillis();
}
}
调用方代码变得极其简洁:
public class OrderController {
private final OrderFacade orderFacade = new OrderFacade();
public void createOrder(HttpRequest request) {
CreateOrderRequest req = new CreateOrderRequest();
req.setUserId(request.getUserId());
req.setProductId(request.getProductId());
req.setQuantity(request.getQuantity());
req.setAmount(request.getAmount());
req.setPaymentMethod(request.getPaymentMethod());
req.setAddress(request.getAddress());
CreateOrderResponse resp = orderFacade.createOrder(req);
if (resp.isSuccess()) {
return ok(resp.getOrderId());
} else {
return badRequest(resp.getMessage());
}
}
}
外观模式 vs 适配器模式
两者看起来都是「包装」,但意图完全不同:
flowchart LR
subgraph 外观模式["外观模式"]
C1[客户端] --> F1[外观]
F1 --> S1[子系统A]
F1 --> S2[子系统B]
F1 --> S3[子系统C]
end
subgraph 适配器模式["适配器模式"]
C2[客户端] --> A[适配器]
A --> S4[已有的\n不兼容类]
end
外观模式不改变子系统的接口,只是把多个接口打包成一个。适配器模式则是「翻译」——把 A 接口转换成客户端期望的 B 接口。
双向外观
标准的外观模式是单向的:客户端通过外观访问子系统。但在某些场景下,需要双向访问——子系统也需要知道客户端的存在。
flowchart TB
Client[客户端] <--> BidirectionalFacade[双向外观]
BidirectionalFacade --> SubSystem1[子系统A]
BidirectionalFacade --> SubSystem2[子系统B]
SubSystem1 --> BidirectionalFacade
SubSystem2 --> BidirectionalFacade
双向外观的典型应用是事件总线。子系统发布事件,客户端订阅事件,外观负责路由:
public class EventBusFacade {
private final Map<Class<?>, List<Consumer<?>>> subscribers = new ConcurrentHashMap<>();
// 子系统发布事件
public <T> void publish(T event) {
List<Consumer<?>> handlers = subscribers.get(event.getClass());
if (handlers != null) {
for (Consumer<?> handler : handlers) {
((Consumer<T>) handler).accept(event);
}
}
}
// 客户端订阅事件
public <T> void subscribe(Class<T> eventType, Consumer<T> handler) {
subscribers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
.add(handler);
}
}
Spring 中的外观模式
RequestContextHolder
Spring MVC 中的 RequestContextHolder 是一个典型的外观模式实现。它封装了 HttpServletRequest 的获取逻辑,让开发者可以随时随地获取当前请求上下文:
// 原始方式:依赖注入
@Controller
public class UserController {
@RequestMapping("/user")
public String getUser(HttpServletRequest request) {
String userId = request.getParameter("id");
// ...
return "user";
}
}
// 外观模式:通过静态方法随时获取
@Service
public class UserService {
public User getCurrentUser() {
HttpServletRequest request = RequestContextHolder.getRequestAttributes().getRequest();
String userId = request.getParameter("id");
// ...
return userRepository.findById(userId);
}
}
RequestContextHolder 封装了获取 RequestAttributes 的复杂性,包括线程绑定、请求作用域管理等细节。
TransactionTemplate
Spring 的 TransactionTemplate 简化了事务管理:
// 原始方式:全手工事务
public void transfer(Account from, Account to, BigDecimal amount) {
Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
// 转出
accountDao.decrease(from, amount);
// 转入
accountDao.increase(to, amount);
conn.commit();
} catch (Exception e) {
conn.rollback();
} finally {
conn.close();
}
}
// 外观模式:封装事务逻辑
public void transfer(Account from, Account to, BigDecimal amount) {
transactionTemplate.execute(status -> {
accountDao.decrease(from, amount);
accountDao.increase(to, amount);
return null;
});
}
TransactionTemplate 把事务的开启、提交、回滚、异常处理全部封装起来,调用方只需要关注业务逻辑。
外观模式的适用场景
适用场景
- 为复杂的子系统提供简单接口
- 客户端与子系统之间存在耦合,需要解耦
- 需要分层架构,在各层之间建立外观
- 子系统可能变化,但客户端只需要知道外观接口
不适用场景
- 客户端需要直接访问子系统的全部功能
- 子系统本身已经足够简单,不需要再包装
- 性能敏感场景,外观可能增加不必要的调用开销
外观模式的「守门人」角色
外观模式不仅是简化接口,还承担了统一入口的角色:
- 参数校验:在外观层统一校验请求参数
- 权限检查:在外观层检查用户权限
- 日志记录:在外观层记录调用日志
- 异常转换:把子系统的异常转换成统一的业务异常
这些横切关注点集中在外观层处理,可以让子系统更纯粹。
思考题
问题 1:外观模式和迪米特法则(最少知识原则)有什么关系?
参考答案
迪米特法则(Law of Demeter)要求一个对象对其他对象有最少的了解——只和「直接朋友」交流。「直接朋友」包括:
- 对象本身
- 对象的成员对象
- 方法参数
- 方法内部创建的对象
外观模式是实现迪米特法则的典型手段。如果不用外观,客户端需要了解子系统中多个类的接口,这违反了迪米特法则。通过外观,客户端只和一个类(外观)交互,子系统的复杂性被隐藏。
但要注意:外观不是万能的。如果客户端确实需要访问子系统的某个特定功能,不应该强行通过外观,而是应该重新设计接口或直接暴露该功能。
问题 2:如果子系统需要演进,新功能要加到外观接口中,应该怎么做?
参考答案
这涉及到开闭原则和外观模式的平衡:
-
如果新功能是子系统的增强,应该在外观中直接添加方法,客户端自动获得这个能力。
-
如果新功能是可选的复杂功能,可以:
- 在外观中添加
getAdvancedFacade() 方法,返回更细粒度的接口
- 使用「外观 + 策略模式」,让调用方按需注入
-
如果新功能只是部分客户端需要,可以:
- 添加新的专用外观类
- 把原有外观保持简单,新功能放到新外观中
关键原则:不要因为扩展而破坏已有客户端。如果现有客户端不需要新功能,不应该被迫了解它。
问题 3:如何在微服务架构中应用外观模式?
参考答案
在微服务架构中,外观模式通常体现在 BFF(Backend For Frontend)层:
flowchart TB
MobileApp[移动端] --> BFFMobile[BFF Mobile]
WebApp[Web端] --> BFFWeb[BFF Web]
PCApp[PC端] --> BFFPC[BFF PC]
BFFMobile --> Facade[聚合服务]
BFFWeb --> Facade
BFFPC --> Facade
Facade --> UserService[用户服务]
Facade --> OrderService[订单服务]
Facade --> ProductService[商品服务]
Facade --> PaymentService[支付服务]
每个 BFF 为特定客户端聚合多个微服务,返回适合该客户端的数据格式。例如:
- 移动端 BFF 聚合用户信息 + 订单列表 + 商品简略信息
- Web 端 BFF 聚合相同数据,但返回更详细的商品描述和图片 URL
这样,各端可以独立演进,不需要为所有端维护统一的 API。