欢迎你来读这篇博客,这篇博客主要是关于外观模式。
其中包括外观模式的核心思想、适用场景、优缺点、与适配器模式、代理模式、中介者模式的区别,以及 Java 后端开发中下单流程聚合服务的完整案例。
序言
在真实项目中,业务系统往往不是一个类解决所有问题,而是由很多子系统协作完成。
例如用户下单这个动作,看起来只是一个按钮:
但后端可能要做很多事情:
- 校验用户;
- 查询商品;
- 校验库存;
- 计算价格;
- 使用优惠券;
- 创建订单;
- 锁定库存;
- 发起支付;
- 记录操作日志;
- 发送通知;
- 发布领域事件。
如果客户端直接调用这些子系统,代码可能会变成这样:
1 2 3 4 5 6 7 8 9 10
| userService.checkUser(userId); productService.getProduct(productId); inventoryService.checkStock(productId, quantity); priceService.calculatePrice(productId, quantity); couponService.useCoupon(userId, couponId); orderService.createOrder(...); inventoryService.lockStock(productId, quantity); paymentService.createPayment(...); notificationService.send(...); eventPublisher.publish(...);
|
这还只是一个下单流程。
如果每个 Controller、每个 RPC 接口、每个定时任务都要自己编排这些子系统,系统很快就会变成“调用链意大利面”。
外观模式就是为了解决这种问题:
为复杂子系统提供一个统一、简单、高层的访问入口,让客户端不用关心子系统内部细节。
简单说:
外观模式就是给复杂系统开一个“服务窗口”。
用户不需要知道后厨有几口锅、几个人切菜、谁负责洗碗,只需要到窗口点餐。
当然,如果这个窗口最后什么都管,连采购、做菜、收银、修空调都塞进去,那它就会从“门面”进化成“巨石”。这不是模式的问题,是我们手没管住。
正文
chapter 1:什么是外观模式
外观模式,英文是 Facade Pattern,也常被叫作门面模式,属于结构型设计模式。
它的定义是:
为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。
外观模式的核心是:
隐藏复杂子系统,对外提供简单入口。
它通常包含两个角色:
- Facade 外观类:对外提供统一入口,内部协调多个子系统。
- Subsystem 子系统类:真正完成具体功能的多个类或模块。
结构如下:
1 2 3 4 5 6 7 8
| Client │ ▼ Facade ├── Subsystem A ├── Subsystem B ├── Subsystem C └── Subsystem D
|
客户端只调用 Facade。
Facade 内部再调用多个子系统。
chapter 2:为什么需要外观模式
外观模式解决的不是“对象创建”问题,也不是“接口不兼容”问题,而是:
子系统太复杂,客户端不应该直接面对复杂调用。
如果没有外观模式,客户端可能会依赖大量子系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class OrderController {
private final UserService userService;
private final ProductService productService;
private final InventoryService inventoryService;
private final PriceService priceService;
private final CouponService couponService;
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService; }
|
这会带来几个问题:
- Controller 依赖太多服务;
- 业务流程散落在多个客户端;
- 子系统调用顺序被客户端掌握;
- 修改流程时要改很多地方;
- 客户端需要理解太多内部细节;
- 测试复杂度提高;
- 代码可读性下降。
外观模式的做法是定义一个统一入口:
客户端只调用:
1
| checkoutFacade.checkout(command);
|
复杂流程由 CheckoutFacade 统一编排。
这样 Controller 就可以保持很薄:
1 2 3 4
| @PostMapping("/checkout") public CheckoutResult checkout(@RequestBody CheckoutCommand command) { return checkoutFacade.checkout(command); }
|
这就是外观模式在后端服务中的真实价值。
chapter 3:外观模式的核心思想
外观模式的核心可以概括为三句话:
- 客户端不直接依赖复杂子系统;
- 外观类封装对子系统的调用顺序和协作逻辑;
- 对外暴露简单、稳定、高层的接口。
例如下单流程。
客户端关心的是:
而不是:
1 2 3 4 5 6 7 8 9
| checkUser() checkStock() calculatePrice() useCoupon() createOrder() lockStock() createPayment() sendNotify() publishEvent()
|
外观模式不是不让子系统存在。
它只是避免客户端直接和一堆子系统打交道。
chapter 4:外观模式适合解决什么复杂度
外观模式主要解决三类复杂度。
1. 调用复杂度
多个子系统需要按固定顺序协作。
例如:
1
| 校验用户 -> 校验库存 -> 计算价格 -> 创建订单 -> 发起支付
|
客户端不应该到处复制这套流程。
2. 依赖复杂度
客户端依赖太多服务,导致类构造器越来越长。
例如:
1 2 3 4 5 6 7 8
| public OrderController(UserService userService, ProductService productService, InventoryService inventoryService, PriceService priceService, CouponService couponService, OrderService orderService, PaymentService paymentService) { }
|
这时可以引入外观类,把依赖收敛起来。
3. 认知复杂度
客户端只想完成一个业务动作,但被迫理解很多内部细节。
例如前端调用“提交订单”,其实不需要知道后端怎么计算价格、怎么锁库存、怎么创建支付单。
外观模式就是降低使用方的理解成本。
chapter 5:最简单的外观模式示例
先看一个简单例子:启动一台电脑。
子系统包括:
1 2 3 4 5 6
| public class Cpu {
public void start() { System.out.println("CPU 启动"); } }
|
1 2 3 4 5 6
| public class Memory {
public void load() { System.out.println("内存加载数据"); } }
|
1 2 3 4 5 6
| public class Disk {
public void read() { System.out.println("硬盘读取系统文件"); } }
|
如果客户端直接调用:
1 2 3 4 5 6 7
| Cpu cpu = new Cpu(); Memory memory = new Memory(); Disk disk = new Disk();
cpu.start(); memory.load(); disk.read();
|
客户端就需要知道启动顺序。
使用外观模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class ComputerFacade {
private final Cpu cpu = new Cpu();
private final Memory memory = new Memory();
private final Disk disk = new Disk();
public void start() { cpu.start(); memory.load(); disk.read();
System.out.println("电脑启动完成"); } }
|
客户端调用:
1 2 3 4 5 6 7 8
| public class Client {
public static void main(String[] args) { ComputerFacade computerFacade = new ComputerFacade();
computerFacade.start(); } }
|
客户端只知道:
不需要知道内部启动细节。
这个例子很简单,但外观模式在后端系统中的思想也是一样的。
chapter 6:案例背景:下单结算外观服务
下面用一个 Java 后端真实一点的案例来讲外观模式。
假设我们正在开发电商系统的下单流程。
用户提交订单时,需要完成以下步骤:
- 校验用户状态;
- 查询商品信息;
- 校验库存;
- 计算订单价格;
- 使用优惠券;
- 创建订单;
- 锁定库存;
- 创建支付单;
- 发送通知。
如果 Controller 直接调用所有子服务,会非常臃肿。
所以我们设计一个外观类:
它对外只提供一个方法:
1
| CheckoutResult checkout(CheckoutCommand command);
|
内部负责协调多个子系统。
chapter 7:定义请求对象 CheckoutCommand
先定义下单请求对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import java.math.BigDecimal;
public class CheckoutCommand {
private final Long userId;
private final Long productId;
private final Integer quantity;
private final Long couponId;
public CheckoutCommand(Long userId, Long productId, Integer quantity, Long couponId) { this.userId = userId; this.productId = productId; this.quantity = quantity; this.couponId = couponId; }
public Long getUserId() { return userId; }
public Long getProductId() { return productId; }
public Integer getQuantity() { return quantity; }
public Long getCouponId() { return couponId; } }
|
这个对象是客户端传入外观类的统一请求。
客户端不需要分别给各个子系统传一堆参数。
chapter 8:定义返回对象 CheckoutResult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import java.math.BigDecimal;
public class CheckoutResult {
private final Long orderId;
private final Long paymentId;
private final BigDecimal payableAmount;
private final String message;
public CheckoutResult(Long orderId, Long paymentId, BigDecimal payableAmount, String message) { this.orderId = orderId; this.paymentId = paymentId; this.payableAmount = payableAmount; this.message = message; }
public Long getOrderId() { return orderId; }
public Long getPaymentId() { return paymentId; }
public BigDecimal getPayableAmount() { return payableAmount; }
public String getMessage() { return message; } }
|
外观类最终给客户端一个统一结果。
客户端不需要拼装多个子系统返回值。
chapter 9:定义商品模型 ProductInfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import java.math.BigDecimal;
public class ProductInfo {
private final Long productId;
private final String productName;
private final BigDecimal price;
public ProductInfo(Long productId, String productName, BigDecimal price) { this.productId = productId; this.productName = productName; this.price = price; }
public Long getProductId() { return productId; }
public String getProductName() { return productName; }
public BigDecimal getPrice() { return price; } }
|
chapter 10:定义价格结果 PriceResult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import java.math.BigDecimal;
public class PriceResult {
private final BigDecimal originalAmount;
private final BigDecimal discountAmount;
private final BigDecimal payableAmount;
public PriceResult(BigDecimal originalAmount, BigDecimal discountAmount, BigDecimal payableAmount) { this.originalAmount = originalAmount; this.discountAmount = discountAmount; this.payableAmount = payableAmount; }
public BigDecimal getOriginalAmount() { return originalAmount; }
public BigDecimal getDiscountAmount() { return discountAmount; }
public BigDecimal getPayableAmount() { return payableAmount; } }
|
chapter 11:定义子系统 UserService
用户服务负责校验用户状态。
1 2 3 4 5 6 7 8 9 10
| public class UserService {
public void checkUserAvailable(Long userId) { if (userId == null) { throw new IllegalArgumentException("userId can not be null"); }
System.out.println("校验用户状态,userId = " + userId); } }
|
真实项目中,这里可能会检查:
- 用户是否存在;
- 用户是否被冻结;
- 用户是否实名;
- 用户是否有下单权限;
- 用户是否命中风控。
chapter 12:定义子系统 ProductService
商品服务负责查询商品信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import java.math.BigDecimal;
public class ProductService {
public ProductInfo getProduct(Long productId) { if (productId == null) { throw new IllegalArgumentException("productId can not be null"); }
System.out.println("查询商品信息,productId = " + productId);
return new ProductInfo(productId, "设计模式课程", new BigDecimal("99.00")); } }
|
chapter 13:定义子系统 InventoryService
库存服务负责校验和锁定库存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class InventoryService {
public void checkStock(Long productId, Integer quantity) { if (quantity == null || quantity <= 0) { throw new IllegalArgumentException("quantity must be greater than 0"); }
System.out.println("校验库存,productId = " + productId + ",quantity = " + quantity); }
public void lockStock(Long productId, Integer quantity) { System.out.println("锁定库存,productId = " + productId + ",quantity = " + quantity); } }
|
真实项目中,库存锁定可能涉及:
- Redis;
- 数据库行锁;
- 库存中心;
- MQ 异步扣减;
- 分布式事务;
- TCC;
- Saga。
外观模式不负责解决这些底层复杂问题。
它负责把调用流程组织起来。
chapter 14:定义子系统 PriceService
价格服务负责计算金额。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.math.BigDecimal;
public class PriceService {
public PriceResult calculate(ProductInfo productInfo, Integer quantity, BigDecimal discountAmount) { BigDecimal originalAmount = productInfo.getPrice() .multiply(BigDecimal.valueOf(quantity));
BigDecimal payableAmount = originalAmount.subtract(discountAmount);
if (payableAmount.compareTo(BigDecimal.ZERO) < 0) { payableAmount = BigDecimal.ZERO; }
System.out.println("计算价格,originalAmount = " + originalAmount + ",discountAmount = " + discountAmount + ",payableAmount = " + payableAmount);
return new PriceResult(originalAmount, discountAmount, payableAmount); } }
|
chapter 15:定义子系统 CouponService
优惠券服务负责计算优惠金额和核销优惠券。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import java.math.BigDecimal;
public class CouponService {
public BigDecimal calculateDiscount(Long userId, Long couponId) { if (couponId == null) { return BigDecimal.ZERO; }
System.out.println("计算优惠券金额,userId = " + userId + ",couponId = " + couponId);
return new BigDecimal("10.00"); }
public void useCoupon(Long userId, Long couponId) { if (couponId == null) { return; }
System.out.println("核销优惠券,userId = " + userId + ",couponId = " + couponId); } }
|
chapter 16:定义子系统 OrderService
订单服务负责创建订单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class OrderService {
public Long createOrder(Long userId, ProductInfo productInfo, Integer quantity, PriceResult priceResult) { System.out.println("创建订单,userId = " + userId + ",productName = " + productInfo.getProductName() + ",quantity = " + quantity + ",payableAmount = " + priceResult.getPayableAmount());
return 10001L; } }
|
真实项目中,订单服务通常应该是领域核心。
它可能包含:
- 订单聚合创建;
- 订单明细创建;
- 订单状态初始化;
- 金额快照;
- 商品快照;
- 优惠快照;
- 订单号生成。
chapter 17:定义子系统 PaymentService
支付服务负责创建支付单。
1 2 3 4 5 6 7 8 9 10
| import java.math.BigDecimal;
public class PaymentService {
public Long createPayment(Long orderId, BigDecimal payableAmount) { System.out.println("创建支付单,orderId = " + orderId + ",amount = " + payableAmount);
return 20001L; } }
|
chapter 18:定义子系统 NotificationService
通知服务负责发送通知。
1 2 3 4 5 6
| public class NotificationService {
public void sendOrderCreatedMessage(Long userId, Long orderId) { System.out.println("发送下单成功通知,userId = " + userId + ",orderId = " + orderId); } }
|
chapter 19:定义外观类 CheckoutFacade
现在定义最关键的外观类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| import java.math.BigDecimal;
public class CheckoutFacade {
private final UserService userService;
private final ProductService productService;
private final InventoryService inventoryService;
private final PriceService priceService;
private final CouponService couponService;
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
public CheckoutFacade(UserService userService, ProductService productService, InventoryService inventoryService, PriceService priceService, CouponService couponService, OrderService orderService, PaymentService paymentService, NotificationService notificationService) { this.userService = userService; this.productService = productService; this.inventoryService = inventoryService; this.priceService = priceService; this.couponService = couponService; this.orderService = orderService; this.paymentService = paymentService; this.notificationService = notificationService; }
public CheckoutResult checkout(CheckoutCommand command) { validate(command);
userService.checkUserAvailable(command.getUserId());
ProductInfo productInfo = productService.getProduct(command.getProductId());
inventoryService.checkStock(command.getProductId(), command.getQuantity());
BigDecimal discountAmount = couponService.calculateDiscount( command.getUserId(), command.getCouponId() );
PriceResult priceResult = priceService.calculate( productInfo, command.getQuantity(), discountAmount );
Long orderId = orderService.createOrder( command.getUserId(), productInfo, command.getQuantity(), priceResult );
couponService.useCoupon(command.getUserId(), command.getCouponId());
inventoryService.lockStock(command.getProductId(), command.getQuantity());
Long paymentId = paymentService.createPayment(orderId, priceResult.getPayableAmount());
notificationService.sendOrderCreatedMessage(command.getUserId(), orderId);
return new CheckoutResult( orderId, paymentId, priceResult.getPayableAmount(), "下单成功" ); }
private void validate(CheckoutCommand command) { if (command == null) { throw new IllegalArgumentException("command can not be null"); }
if (command.getUserId() == null) { throw new IllegalArgumentException("userId can not be null"); }
if (command.getProductId() == null) { throw new IllegalArgumentException("productId can not be null"); }
if (command.getQuantity() == null || command.getQuantity() <= 0) { throw new IllegalArgumentException("quantity must be greater than 0"); } } }
|
这个类就是外观模式中的 Facade。
它对外只暴露一个简单方法:
内部协调多个子系统完成完整下单流程。
chapter 20:客户端调用
客户端不再直接调用所有子系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Client {
public static void main(String[] args) { CheckoutFacade checkoutFacade = new CheckoutFacade( new UserService(), new ProductService(), new InventoryService(), new PriceService(), new CouponService(), new OrderService(), new PaymentService(), new NotificationService() );
CheckoutCommand command = new CheckoutCommand( 1L, 100L, 2, 888L );
CheckoutResult result = checkoutFacade.checkout(command);
System.out.println("订单 ID:" + result.getOrderId()); System.out.println("支付单 ID:" + result.getPaymentId()); System.out.println("应付金额:" + result.getPayableAmount()); System.out.println("结果消息:" + result.getMessage()); } }
|
客户端只需要知道:
1
| checkoutFacade.checkout(command)
|
这就是外观模式最直观的价值。
chapter 21:如果不用外观模式会怎样
如果不用外观模式,Controller 可能会变成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @RestController public class OrderController {
private final UserService userService;
private final ProductService productService;
private final InventoryService inventoryService;
private final PriceService priceService;
private final CouponService couponService;
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
@PostMapping("/checkout") public CheckoutResult checkout(@RequestBody CheckoutCommand command) { userService.checkUserAvailable(command.getUserId());
ProductInfo productInfo = productService.getProduct(command.getProductId());
inventoryService.checkStock(command.getProductId(), command.getQuantity());
BigDecimal discountAmount = couponService.calculateDiscount( command.getUserId(), command.getCouponId() );
PriceResult priceResult = priceService.calculate( productInfo, command.getQuantity(), discountAmount );
Long orderId = orderService.createOrder( command.getUserId(), productInfo, command.getQuantity(), priceResult );
couponService.useCoupon(command.getUserId(), command.getCouponId());
inventoryService.lockStock(command.getProductId(), command.getQuantity());
Long paymentId = paymentService.createPayment(orderId, priceResult.getPayableAmount());
notificationService.sendOrderCreatedMessage(command.getUserId(), orderId);
return new CheckoutResult(orderId, paymentId, priceResult.getPayableAmount(), "下单成功"); } }
|
这会导致 Controller 变厚。
Controller 本来应该负责:
- 接收请求;
- 参数转换;
- 调用应用服务;
- 返回响应。
但现在它开始编排复杂业务流程。
这不是 Controller,是“披着 Controller 外衣的业务流程编排器”。
外观模式可以把复杂流程收敛到 CheckoutFacade 或应用服务中。
chapter 22:在 Spring Boot 中落地
在 Spring Boot 中,外观类通常会被定义成一个 @Service。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
@Service public class CheckoutFacade {
private final UserService userService;
private final ProductService productService;
private final InventoryService inventoryService;
private final PriceService priceService;
private final CouponService couponService;
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
public CheckoutFacade(UserService userService, ProductService productService, InventoryService inventoryService, PriceService priceService, CouponService couponService, OrderService orderService, PaymentService paymentService, NotificationService notificationService) { this.userService = userService; this.productService = productService; this.inventoryService = inventoryService; this.priceService = priceService; this.couponService = couponService; this.orderService = orderService; this.paymentService = paymentService; this.notificationService = notificationService; }
@Transactional(rollbackFor = Exception.class) public CheckoutResult checkout(CheckoutCommand command) { validate(command);
userService.checkUserAvailable(command.getUserId());
ProductInfo productInfo = productService.getProduct(command.getProductId());
inventoryService.checkStock(command.getProductId(), command.getQuantity());
BigDecimal discountAmount = couponService.calculateDiscount( command.getUserId(), command.getCouponId() );
PriceResult priceResult = priceService.calculate( productInfo, command.getQuantity(), discountAmount );
Long orderId = orderService.createOrder( command.getUserId(), productInfo, command.getQuantity(), priceResult );
couponService.useCoupon(command.getUserId(), command.getCouponId());
inventoryService.lockStock(command.getProductId(), command.getQuantity());
Long paymentId = paymentService.createPayment(orderId, priceResult.getPayableAmount());
notificationService.sendOrderCreatedMessage(command.getUserId(), orderId);
return new CheckoutResult( orderId, paymentId, priceResult.getPayableAmount(), "下单成功" ); }
private void validate(CheckoutCommand command) { if (command == null) { throw new IllegalArgumentException("command can not be null"); }
if (command.getUserId() == null) { throw new IllegalArgumentException("userId can not be null"); }
if (command.getProductId() == null) { throw new IllegalArgumentException("productId can not be null"); }
if (command.getQuantity() == null || command.getQuantity() <= 0) { throw new IllegalArgumentException("quantity must be greater than 0"); } } }
|
Controller 就可以非常薄:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import org.springframework.web.bind.annotation.*;
@RestController @RequestMapping("/orders") public class OrderController {
private final CheckoutFacade checkoutFacade;
public OrderController(CheckoutFacade checkoutFacade) { this.checkoutFacade = checkoutFacade; }
@PostMapping("/checkout") public CheckoutResult checkout(@RequestBody CheckoutCommand command) { return checkoutFacade.checkout(command); } }
|
这样 Controller 的职责就清晰了。
业务流程编排由 Facade 负责。
chapter 23:Facade 和 Application Service 的关系
在 Java 后端尤其是 DDD 分层中,外观模式经常和 Application Service 很像。
例如:
1 2 3
| CheckoutFacade OrderApplicationService OrderCommandService
|
它们都可能做流程编排。
区别不必过度教条。
可以这样理解:
- 外观模式是设计模式角度的说法,强调简化复杂子系统访问;
- Application Service是分层架构角度的说法,强调编排用例、事务边界、调用领域模型和基础设施;
- Facade Service在工程里常常就是一个对外简化接口的应用服务。
所以在 Spring Boot 项目中,你可能不会真的把类命名为:
也可能会命名为:
1 2 3
| OrderApplicationService CheckoutService OrderCommandService
|
只要它承担的是“统一入口 + 编排子系统 + 简化客户端调用”的职责,本质上就有外观模式的思想。
模式不是为了给类起名,而是为了让职责边界更清楚。
chapter 24:外观模式和适配器模式的区别
外观模式和适配器模式都可能包一层,但目的不同。
| 对比项 |
外观模式 |
适配器模式 |
| 目的 |
简化复杂子系统访问 |
转换不兼容接口 |
| 关注点 |
降低调用复杂度 |
解决接口不兼容 |
| 包装对象 |
通常是一组子系统 |
通常是一个已有类或外部 SDK |
| 接口是否兼容 |
不一定涉及兼容问题 |
重点就是接口不兼容 |
| 示例 |
CheckoutFacade 统一下单流程 |
AliPayClientAdapter 适配支付宝 SDK |
一句话区分:
外观模式是“太复杂,我帮你简化”。
适配器模式是“接口不一样,我帮你转换”。
例如:
1
| checkoutFacade.checkout(command)
|
是外观模式。
1
| paymentClient.pay(request)
|
内部适配支付宝 SDK,是适配器模式。
chapter 25:外观模式和代理模式的区别
外观模式和代理模式也容易混。
| 对比项 |
外观模式 |
代理模式 |
| 目的 |
简化对子系统的访问 |
控制对目标对象的访问 |
| 包装对象数量 |
通常多个子系统 |
通常一个目标对象 |
| 接口关系 |
外观接口通常更高层 |
代理通常和目标对象接口一致 |
| 关注点 |
简化调用 |
权限、远程、懒加载、增强 |
| 示例 |
下单门面封装多个服务 |
Spring AOP 代理 Service |
代理模式像“代办人”。
外观模式像“服务大厅”。
服务大厅背后可能有很多窗口和部门,而代办人通常代表某一个对象。
chapter 26:外观模式和中介者模式的区别
外观模式和中介者模式都能协调多个对象,但关注点不同。
| 对比项 |
外观模式 |
中介者模式 |
| 目的 |
简化外部访问 |
降低多个对象之间的网状耦合 |
| 客户端角色 |
客户端通过外观访问子系统 |
同事对象通过中介者通信 |
| 子系统是否知道外观 |
通常不知道 |
同事对象通常知道中介者 |
| 关注点 |
对外提供简单入口 |
对内协调对象交互 |
| 示例 |
CheckoutFacade 封装下单流程 |
聊天室协调用户消息 |
外观模式强调:
外部调用更简单。
中介者模式强调:
内部对象不要互相乱连。
chapter 27:外观模式和装饰器模式的区别
外观模式和装饰器模式都可能加一层对象,但目的不同。
| 对比项 |
外观模式 |
装饰器模式 |
| 目的 |
简化复杂子系统 |
动态增强对象功能 |
| 接口关系 |
外观接口通常更简单 |
装饰器和被装饰对象接口一致 |
| 包装对象数量 |
通常多个子系统 |
通常一个同接口对象 |
| 关注点 |
降低使用复杂度 |
添加额外职责 |
| 示例 |
统一下单接口 |
给 MessageSender 加日志和重试 |
装饰器是给原对象加功能。
外观是给复杂系统开入口。
一个是“增强”,一个是“简化”。
chapter 28:外观模式的优点
1. 降低客户端复杂度
客户端只需要调用高层接口,不需要理解子系统细节。
2. 降低客户端和子系统耦合
客户端不直接依赖大量子系统。
3. 统一复杂流程入口
下单、退款、结算、导出、审批这类流程可以统一封装。
4. 提高代码可读性
客户端代码从一堆调用变成一个语义明确的方法:
1 2 3
| checkout(command) refund(command) exportReport(command)
|
5. 便于维护流程
如果流程顺序变化,只需要修改外观类,而不是所有客户端。
6. 便于事务控制
在 Spring 中,外观类或应用服务经常作为事务边界。
1 2 3
| @Transactional public CheckoutResult checkout(CheckoutCommand command) { }
|
chapter 29:外观模式的缺点
1. 外观类可能变成上帝类
如果所有逻辑都往 Facade 里塞,Facade 会越来越大。
这就是外观模式最常见的坑。
2. 可能隐藏子系统能力
外观接口为了简化,可能只暴露常用能力。
高级调用方如果需要子系统细节,可能不够灵活。
3. 可能增加一层间接调用
对于非常简单的场景,加外观类可能只是多一层包装。
4. 设计不好会变成过程脚本
Facade 如果只是把所有业务逻辑按流程写在一起,而领域模型很薄,就容易变成贫血模型里的大脚本。
5. 容易和 Service 滥用混在一起
很多项目里 XxxService 既做外观,又做领域逻辑,又做数据访问。
最后 Service 就成了“万物归一”的大锅。
chapter 30:适用场景
外观模式适合以下场景。
1. 子系统复杂,客户端不应该直接访问
例如:
- 下单流程;
- 退款流程;
- 报表导出;
- 文件上传;
- 用户注册;
- 数据同步;
- 审批提交;
- 结算生成。
2. 需要为一组服务提供统一入口
例如:
1 2 3 4 5
| CheckoutFacade RefundFacade ReportExportFacade UserRegisterFacade SettlementFacade
|
3. 需要隔离客户端和子系统变化
子系统内部怎么变,尽量不影响外部调用方。
4. 需要定义清晰的业务用例入口
例如 DDD 中的 Application Service。
5. 需要统一事务边界
一个业务用例中涉及多个操作时,Facade 可以承担事务边界。
chapter 31:不适合使用的场景
以下场景不建议使用外观模式。
1. 子系统本身很简单
如果只有一个服务方法,没必要再包一层 Facade。
2. 客户端需要高度定制子系统调用
如果每个调用方都需要不同流程,统一外观可能反而限制灵活性。
3. 外观类已经过度膨胀
如果 Facade 文件几千行,方法几十个,依赖几十个服务,就要拆分。
4. 为了掩盖混乱设计而加门面
如果子系统本身职责混乱,外观模式只能遮住表面,不能解决根因。
门面可以挡风,但不能修房子地基。
5. 把所有业务逻辑都写进 Facade
Facade 应该编排子系统,而不是吞掉所有业务规则。
业务规则应该尽量放在领域服务、领域模型或具体业务服务中。
chapter 32:真实项目中的实践建议
1. Facade 方法要有明确用例语义
好的方法名:
1 2 3 4 5
| checkout(command) refund(command) registerUser(command) exportReport(command) generateSettlement(command)
|
不好的方法名:
1 2 3 4
| handle(command) process(data) doBusiness(params) execute(request)
|
方法名越泛,职责越危险。
2. Facade 主要做编排,不要堆积细节
Facade 适合做:
- 参数校验;
- 调用顺序编排;
- 事务边界;
- 子系统协调;
- 结果组装;
- 异常转换。
不适合做:
- 大量价格计算细节;
- 复杂库存算法;
- 领域状态机;
- SQL 拼接;
- 第三方 SDK 参数细节。
3. 子系统仍然可以被高级客户端直接使用
外观模式不是禁止访问子系统。
它只是给普通客户端提供更简单入口。
如果某些高级调用方确实需要子系统能力,可以直接依赖子系统。
4. Facade 不要无限增大
可以按业务用例拆分:
1 2 3 4 5
| CheckoutFacade RefundFacade PaymentFacade SettlementFacade ReportExportFacade
|
不要做一个:
然后把订单相关所有用例都塞进去。
5. Controller 应该薄,Facade/Application Service 应该承接用例
推荐结构:
1 2 3 4 5 6
| Controller -> Facade / ApplicationService -> DomainService -> Repository -> Adapter -> Gateway
|
Controller 不应该编排复杂业务流程。
6. 注意事务和外部调用边界
如果 Facade 中同时有数据库事务和外部调用,要小心。
例如:
1
| 创建订单 -> 锁库存 -> 调支付 -> 发通知
|
支付、通知这类外部调用不一定适合放在同一个本地事务里。
真实项目中可以考虑:
- 本地事务;
- 事务消息;
- outbox;
- Saga;
- TCC;
- MQ 异步事件;
- 最终一致性。
外观模式负责简化入口,但不自动解决分布式一致性。
别让 Facade 背上它背不动的锅。
chapter 33:完整案例代码汇总
CheckoutCommand
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class CheckoutCommand {
private final Long userId;
private final Long productId;
private final Integer quantity;
private final Long couponId;
public CheckoutCommand(Long userId, Long productId, Integer quantity, Long couponId) { this.userId = userId; this.productId = productId; this.quantity = quantity; this.couponId = couponId; }
public Long getUserId() { return userId; }
public Long getProductId() { return productId; }
public Integer getQuantity() { return quantity; }
public Long getCouponId() { return couponId; } }
|
CheckoutResult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import java.math.BigDecimal;
public class CheckoutResult {
private final Long orderId;
private final Long paymentId;
private final BigDecimal payableAmount;
private final String message;
public CheckoutResult(Long orderId, Long paymentId, BigDecimal payableAmount, String message) { this.orderId = orderId; this.paymentId = paymentId; this.payableAmount = payableAmount; this.message = message; }
public Long getOrderId() { return orderId; }
public Long getPaymentId() { return paymentId; }
public BigDecimal getPayableAmount() { return payableAmount; }
public String getMessage() { return message; } }
|
ProductInfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import java.math.BigDecimal;
public class ProductInfo {
private final Long productId;
private final String productName;
private final BigDecimal price;
public ProductInfo(Long productId, String productName, BigDecimal price) { this.productId = productId; this.productName = productName; this.price = price; }
public Long getProductId() { return productId; }
public String getProductName() { return productName; }
public BigDecimal getPrice() { return price; } }
|
PriceResult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import java.math.BigDecimal;
public class PriceResult {
private final BigDecimal originalAmount;
private final BigDecimal discountAmount;
private final BigDecimal payableAmount;
public PriceResult(BigDecimal originalAmount, BigDecimal discountAmount, BigDecimal payableAmount) { this.originalAmount = originalAmount; this.discountAmount = discountAmount; this.payableAmount = payableAmount; }
public BigDecimal getOriginalAmount() { return originalAmount; }
public BigDecimal getDiscountAmount() { return discountAmount; }
public BigDecimal getPayableAmount() { return payableAmount; } }
|
子系统服务
1 2 3 4 5 6
| public class UserService {
public void checkUserAvailable(Long userId) { System.out.println("校验用户状态,userId = " + userId); } }
|
1 2 3 4 5 6 7 8 9 10
| import java.math.BigDecimal;
public class ProductService {
public ProductInfo getProduct(Long productId) { System.out.println("查询商品信息,productId = " + productId);
return new ProductInfo(productId, "设计模式课程", new BigDecimal("99.00")); } }
|
1 2 3 4 5 6 7 8 9 10
| public class InventoryService {
public void checkStock(Long productId, Integer quantity) { System.out.println("校验库存,productId = " + productId + ",quantity = " + quantity); }
public void lockStock(Long productId, Integer quantity) { System.out.println("锁定库存,productId = " + productId + ",quantity = " + quantity); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import java.math.BigDecimal;
public class CouponService {
public BigDecimal calculateDiscount(Long userId, Long couponId) { if (couponId == null) { return BigDecimal.ZERO; }
System.out.println("计算优惠券金额,userId = " + userId + ",couponId = " + couponId);
return new BigDecimal("10.00"); }
public void useCoupon(Long userId, Long couponId) { if (couponId == null) { return; }
System.out.println("核销优惠券,userId = " + userId + ",couponId = " + couponId); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import java.math.BigDecimal;
public class PriceService {
public PriceResult calculate(ProductInfo productInfo, Integer quantity, BigDecimal discountAmount) { BigDecimal originalAmount = productInfo.getPrice() .multiply(BigDecimal.valueOf(quantity));
BigDecimal payableAmount = originalAmount.subtract(discountAmount);
if (payableAmount.compareTo(BigDecimal.ZERO) < 0) { payableAmount = BigDecimal.ZERO; }
return new PriceResult(originalAmount, discountAmount, payableAmount); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class OrderService {
public Long createOrder(Long userId, ProductInfo productInfo, Integer quantity, PriceResult priceResult) { System.out.println("创建订单,userId = " + userId + ",productName = " + productInfo.getProductName() + ",quantity = " + quantity + ",payableAmount = " + priceResult.getPayableAmount());
return 10001L; } }
|
1 2 3 4 5 6 7 8 9 10
| import java.math.BigDecimal;
public class PaymentService {
public Long createPayment(Long orderId, BigDecimal payableAmount) { System.out.println("创建支付单,orderId = " + orderId + ",amount = " + payableAmount);
return 20001L; } }
|
1 2 3 4 5 6
| public class NotificationService {
public void sendOrderCreatedMessage(Long userId, Long orderId) { System.out.println("发送下单成功通知,userId = " + userId + ",orderId = " + orderId); } }
|
外观类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| import java.math.BigDecimal;
public class CheckoutFacade {
private final UserService userService;
private final ProductService productService;
private final InventoryService inventoryService;
private final PriceService priceService;
private final CouponService couponService;
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
public CheckoutFacade(UserService userService, ProductService productService, InventoryService inventoryService, PriceService priceService, CouponService couponService, OrderService orderService, PaymentService paymentService, NotificationService notificationService) { this.userService = userService; this.productService = productService; this.inventoryService = inventoryService; this.priceService = priceService; this.couponService = couponService; this.orderService = orderService; this.paymentService = paymentService; this.notificationService = notificationService; }
public CheckoutResult checkout(CheckoutCommand command) { validate(command);
userService.checkUserAvailable(command.getUserId());
ProductInfo productInfo = productService.getProduct(command.getProductId());
inventoryService.checkStock(command.getProductId(), command.getQuantity());
BigDecimal discountAmount = couponService.calculateDiscount( command.getUserId(), command.getCouponId() );
PriceResult priceResult = priceService.calculate( productInfo, command.getQuantity(), discountAmount );
Long orderId = orderService.createOrder( command.getUserId(), productInfo, command.getQuantity(), priceResult );
couponService.useCoupon(command.getUserId(), command.getCouponId());
inventoryService.lockStock(command.getProductId(), command.getQuantity());
Long paymentId = paymentService.createPayment(orderId, priceResult.getPayableAmount());
notificationService.sendOrderCreatedMessage(command.getUserId(), orderId);
return new CheckoutResult( orderId, paymentId, priceResult.getPayableAmount(), "下单成功" ); }
private void validate(CheckoutCommand command) { if (command == null) { throw new IllegalArgumentException("command can not be null"); }
if (command.getUserId() == null) { throw new IllegalArgumentException("userId can not be null"); }
if (command.getProductId() == null) { throw new IllegalArgumentException("productId can not be null"); }
if (command.getQuantity() == null || command.getQuantity() <= 0) { throw new IllegalArgumentException("quantity must be greater than 0"); } } }
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Client {
public static void main(String[] args) { CheckoutFacade checkoutFacade = new CheckoutFacade( new UserService(), new ProductService(), new InventoryService(), new PriceService(), new CouponService(), new OrderService(), new PaymentService(), new NotificationService() );
CheckoutCommand command = new CheckoutCommand( 1L, 100L, 2, 888L );
CheckoutResult result = checkoutFacade.checkout(command);
System.out.println("订单 ID:" + result.getOrderId()); System.out.println("支付单 ID:" + result.getPaymentId()); System.out.println("应付金额:" + result.getPayableAmount()); System.out.println("结果消息:" + result.getMessage()); } }
|
chapter 34:一句话总结
外观模式的本质是:
为复杂子系统提供一个简单统一的高层入口,让客户端不用直接面对复杂的内部调用。
它适合:
- 子系统复杂;
- 调用流程固定;
- 客户端不应该感知太多细节;
- 需要统一用例入口;
- 需要收敛依赖关系;
- 需要定义事务边界。
在 Java 后端开发中,外观模式经常体现为:
- Facade;
- Application Service;
- Command Service;
- 聚合服务;
- 统一业务入口。
它不是让你把所有代码都塞进一个大 Service。
好的外观模式是“简化入口”。
坏的外观模式是“制造巨类”。
真正的工程判断在于:把复杂度藏起来,但不要把复杂度堆烂。
参考资料
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
- Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
- Martin Fowler. Patterns of Enterprise Application Architecture.
- Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software.
- Spring Framework Documentation: Declarative Transaction Management.
- Refactoring Guru: Facade Pattern.
- SourceMaking: Facade Design Pattern.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。