📌 PDF:AI人工智能 — RAG知识库实战项目
大白话说Java设计模式-18-外观模式(业务实战篇):大白商城下单门面的"一站式服务"
📌 一句话本质:外观模式就是"前台接待员",把一堆子系统的调用封装成简单接口。
🏷️ 标签:外观模式 / Java 设计模式 / 门面模式 / 大白商城 / 一站式服务 🎯 适合:初中级后端 / 经常被"子系统太多"困扰的工程师
目录
- 一、业务场景引入:下单为什么要"一站式服务"?
- 二、反面教材:客户端直接调多个子系统的"灾难现场"
- 三、模式原理:外观的"接待员 + 一张图"
- 四、实战代码:大白商城下单门面完整实现
- 五、外观 vs 适配器 / 中介者 / 代理
- 六、SLF4J + JdbcTemplate 怎么借鉴外观?
- 七、工程决策 Checklist
- 八、与其他模式协作
- 九、本篇小结 + 下篇预告
一、业务场景引入:下单为什么要"一站式服务"?
大白商城 2024 年大促备战,CTO 提了一个看似简单、实则"动全身"的需求:
“前端只调一个接口就能下单。业务方不要再管订单、库存、支付、物流的细节。”
我打开老代码一看,前端要调 8 个接口才能下一个单:
/**
* 大白商城老的前端下单代码
*/
public class OldOrderController {
public void createOrder(OrderRequest request) {
// 1️⃣ 校验商品
goodsService.checkStock(request.getGoodsId());
// 2️⃣ 锁库存
inventoryService.lockStock(request.getGoodsId(), request.getQuantity());
// 3️⃣ 创建订单
orderService.createOrder(request);
// 4️⃣ 计算优惠
priceService.calculatePrice(request);
// 5️⃣ 创建支付单
paymentService.createPayment(request);
// 6️⃣ 触发发货
shippingService.createShipment(request);
// 7️⃣ 发通知
notificationService.sendNotification(request);
// 8️⃣ 记录日志
logService.recordLog(request);
}
}
这种代码的痛点:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 前端要懂 8 个子系统 | 前端人员要懂所有业务 |
| ② | 调用顺序写死 | 顺序错了就崩 |
| ③ | 加新步骤要改前端 | 前后端反复改 |
| ④ | 事务一致性难管 | 8 个调用要原子化 |
| ⑤ | 重复代码多 | 每个下单入口都要写 8 步 |
老板要的"一站式下单接口",怎么搞?
答案就是——外观模式。
1.1 大白话讲透外观
继续打比方:
场景:大白商城下单涉及 8 个子系统(订单/库存/支付/物流/优惠/通知/日志……)。每个子系统都有自己的接口。
- 错误做法:前端直接调 8 个接口(前后端都累)
- 正确做法:
- 大白商城设一个"前台"(
OrderFacade)- 前端只调"前台"的一个方法:
orderFacade.placeOrder(request)- 前台内部依次调 8 个子系统
- 前端完全不需要知道有几个子系统
前台 = 外观模式。让"一个简单接口代替多个复杂接口"。
外观模式 = 为子系统中的一组接口提供一个一致的界面,定义一个高层接口,让子系统更容易使用。
1.2 外观模式的 3 个真实场景
大白商城里,外观模式用在:
| 场景 | “子系统” | “外观” |
|---|---|---|
| 下单 | 订单/库存/支付/物流/优惠/通知/日志 | OrderFacade |
| 用户中心 | 注册/登录/权限/SSO/积分/会员 | UserFacade |
| 商品中心 | SPU/SKU/库存/价格/类目/标签 | GoodsFacade |
| 支付 | 渠道/对账/退款/发票/优惠 | PaymentFacade |
任何"多子系统协同"的场景,都用外观。
二、反面教材:客户端直接调多个子系统的"灾难现场"
我们看 4 个反面教材,看它们是怎么一步步崩的。
2.1 反面教材 v1:客户端直接调 8 个子系统
/**
* ❌ 反面教材 v1:客户端直接调 8 个子系统
*/
public class OrderControllerV1 {
public void placeOrder(OrderRequest request) {
// 前端要懂 8 个子系统
goodsService.checkStock(request.getGoodsId());
inventoryService.lockStock(request.getGoodsId(), request.getQuantity());
orderService.createOrder(request);
priceService.calculatePrice(request);
paymentService.createPayment(request);
shippingService.createShipment(request);
notificationService.sendNotification(request);
logService.recordLog(request);
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 前端懂太多 | 前端要懂所有业务 |
| ② | 顺序错就崩 | 必须按顺序调 |
| ③ | 事务难管 | 8 个调用要原子化 |
2.2 反面教材 v2:抽象基类封装
/**
* ❌ 反面教材 v2:抽象基类封装
*/
public abstract class BaseOrderService {
public void placeOrder(OrderRequest request) {
goodsService.checkStock(request.getGoodsId());
inventoryService.lockStock(/* ... */);
// ... 8 个调用
}
}
public class OrderServiceV2 extends BaseOrderService {
public void createOrder(OrderRequest request) {
placeOrder(request); // 继承
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 继承关系僵硬 | OrderService 不能继承其他 |
| ② | 强耦合 | 基类改了子类受影响 |
2.3 反面教材 v3:Service 内部嵌套调用
/**
* ❌ 反面教材 v3:Service 内部嵌套调用
*/
public class OrderServiceV3 {
public void createOrder(OrderRequest request) {
// 1. 调库存
inventoryService.lockStock(/* ... */);
// 2. 调支付
paymentService.createPayment(/* ... */);
// 3. 调物流
shippingService.createShipment(/* ... */);
// ... OrderService 变成"上帝类"
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | OrderService 臃肿 | 上帝类,违反单一职责 |
| ② | 订单逻辑被淹没 | 看不到核心业务 |
2.4 反面教材 v4:完全分散
/**
* ❌ 反面教材 v4:完全分散(每个 Controller 各自调)
*/
public class CartOrderController {
public void create() {
// 调 8 个
}
}
public class SeckillOrderController {
public void create() {
// 调 8 个(重复代码)
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 重复代码 | 每个 Controller 都要写 8 步 |
| ② | 加步骤要全改 | 8 个 Controller 都要改 |
2.5 4 个反面教材的共同病根
| 痛点 | 反模式方案能不能解决? |
|---|---|
| 客户端只调 1 个接口 | ❌ 全部要懂 8 个 |
| 业务方不关心子系统 | ❌ 全部要懂细节 |
| 加新子系统不动客户端 | ❌ 全部要改 |
| 事务统一管理 | ❌ 分散难管 |
必须上外观模式。
三、模式原理:外观的"接待员 + 一张图"
3.1 外观的 2 个核心角色
| 角色 | 职责 | 例子 |
|---|---|---|
| 外观(Facade) | 提供统一的高层接口 | OrderFacade |
| 子系统(Subsystem) | 实现具体功能 | OrderService / InventoryService / … |
关键点:外观"知道"所有子系统,客户端"只认识"外观。
3.2 一张图看懂外观
客户端(Controller)
↓ 调用
OrderFacade(外观)
↓ 内部调
├─ GoodsService(商品)
├─ InventoryService(库存)
├─ OrderService(订单)
├─ PriceService(价格)
├─ PaymentService(支付)
├─ ShippingService(物流)
├─ NotificationService(通知)
└─ LogService(日志)
客户端只需要 OrderFacade.placeOrder()
3.3 外观的"灵魂三问"
Q1:外观 vs 适配器,区别是什么?
答:
- 外观:简化多个接口(高层接口),不改变接口
- 适配器:翻译一个接口(兼容老接口),改变接口
- 外观侧重"简化",适配器侧重"翻译"
- 外观包装多个,适配器包装一个
Q2:外观 vs 中介者,区别是什么?
答:
- 外观:单向调用(客户端 → 外观 → 子系统)
- 中介者:双向通信(子系统之间通过中介者通信)
- 外观是"前台",中介者是"调度中心"
Q3:外观 = 一个"总方法",会不会变成上帝类?
答:
- 外观是"协调者",不实现业务逻辑
- 业务逻辑在子系统中
- 外观只是"调用顺序"的封装
- 如果外观开始实现业务,就违反单一职责
3.4 外观的 3 种写法
| 写法 | 适用场景 | 缺点 |
|---|---|---|
| 单外观 | 简单场景 | 一个 Facade 管所有 |
| 多外观 | 大型系统 | 按域拆分(OrderFacade / UserFacade) |
| 外观 + 子外观 | 复杂系统 | 分层外观 |
大白商城主推多外观(按业务域拆分)。
四、实战代码:大白商城下单门面完整实现
下面是大白商城生产环境在用的外观实现,全套代码可直接复制到 IDEA 跑。
4.1 项目环境与依赖
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.dabai.mall</groupId>
<artifactId>mall-design-pattern-18</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>mall-design-pattern-18</name>
<description>大白商城 - 设计模式 18 外观模式</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 6 个子系统
GoodsService(商品):
package com.dabai.mall.subsystem;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* ✅ 子系统:商品服务
*/
@Slf4j
@Service
public class GoodsService {
public boolean checkStock(Long goodsId, Integer quantity) {
log.info("【商品】检查库存, goodsId={}, quantity={}", goodsId, quantity);
// 实际查库存
return quantity != null && quantity > 0 && quantity < 1000;
}
public BigDecimal getPrice(Long goodsId) {
log.info("【商品】查价格, goodsId={}", goodsId);
return new BigDecimal("8999.00");
}
}
InventoryService(库存):
package com.dabai.mall.subsystem;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* ✅ 子系统:库存服务
*/
@Slf4j
@Service
public class InventoryService {
public boolean lockStock(Long goodsId, Integer quantity) {
log.info("【库存】锁定库存, goodsId={}, quantity={}", goodsId, quantity);
return true;
}
public void releaseStock(Long goodsId, Integer quantity) {
log.info("【库存】释放库存, goodsId={}, quantity={}", goodsId, quantity);
}
}
OrderService(订单):
package com.dabai.mall.subsystem;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Slf4j
@Service
public class OrderService {
public Long createOrder(Long userId, Long goodsId, Integer quantity, BigDecimal amount) {
log.info("【订单】创建订单, userId={}, goodsId={}", userId, goodsId);
return System.currentTimeMillis(); // 模拟订单 ID
}
}
/**
* 订单 DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class Order {
private Long orderId;
private Long userId;
private Long goodsId;
private Integer quantity;
private BigDecimal amount;
}
PaymentService(支付):
package com.dabai.mall.subsystem;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* ✅ 子系统:支付服务
*/
@Slf4j
@Service
public class PaymentService {
public Long createPayment(Long orderId, BigDecimal amount) {
log.info("【支付】创建支付单, orderId={}, amount={}", orderId, amount);
return System.currentTimeMillis() + 1; // 模拟支付单 ID
}
}
ShippingService(物流):
package com.dabai.mall.subsystem;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* ✅ 子系统:物流服务
*/
@Slf4j
@Service
public class ShippingService {
public Long createShipment(Long orderId, String address) {
log.info("【物流】创建发货单, orderId={}, address={}", orderId, address);
return System.currentTimeMillis() + 2; // 模拟发货单 ID
}
}
NotificationService(通知):
package com.dabai.mall.subsystem;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* ✅ 子系统:通知服务
*/
@Slf4j
@Service
public class NotificationService {
public void sendOrderCreatedNotification(Long userId, Long orderId) {
log.info("【通知】发送订单创建通知, userId={}, orderId={}", userId, orderId);
}
}
4.3 外观:OrderFacade
package com.dabai.mall.facade;
import com.dabai.mall.request.PlaceOrderRequest;
import com.dabai.mall.response.PlaceOrderResponse;
import com.dabai.mall.subsystem.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* ✅ 外观:下单门面
* <p>
* 核心价值:把 6 个子系统的调用封装成一个方法
* <p>
* 客户端(Controller / 异步任务)只调 placeOrder(),
* 完全不需要知道有几个子系统
*
* @author 大白商城技术团队
*/
@Slf4j
@Service
public class OrderFacade {
@Autowired
private GoodsService goodsService;
@Autowired
private InventoryService inventoryService;
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private ShippingService shippingService;
@Autowired
private NotificationService notificationService;
/**
* ✅ 一站式下单:客户端只调这个
*/
public PlaceOrderResponse placeOrder(PlaceOrderRequest request) {
log.info("【OrderFacade】开始下单, userId={}, goodsId={}",
request.getUserId(), request.getGoodsId());
// 1️⃣ 检查库存
if (!goodsService.checkStock(request.getGoodsId(), request.getQuantity())) {
throw new BusinessException("库存不足");
}
// 2️⃣ 锁定库存
inventoryService.lockStock(request.getGoodsId(), request.getQuantity());
try {
// 3️⃣ 计算价格
BigDecimal price = goodsService.getPrice(request.getGoodsId());
// 4️⃣ 创建订单
Long orderId = orderService.createOrder(
request.getUserId(), request.getGoodsId(),
request.getQuantity(), price);
// 5️⃣ 创建支付单
Long paymentId = paymentService.createPayment(orderId, price);
// 6️⃣ 创建发货单
Long shipmentId = shippingService.createShipment(orderId, request.getAddress());
// 7️⃣ 发送通知
notificationService.sendOrderCreatedNotification(request.getUserId(), orderId);
log.info("【OrderFacade】下单完成, orderId={}", orderId);
return PlaceOrderResponse.builder()
.orderId(orderId)
.paymentId(paymentId)
.shipmentId(shipmentId)
.price(price)
.build();
} catch (Exception e) {
// 异常:释放库存
log.error("【OrderFacade】下单失败, 释放库存", e);
inventoryService.releaseStock(request.getGoodsId(), request.getQuantity());
throw e;
}
}
/**
* 简化版:取消订单
*/
public void cancelOrder(Long orderId) {
log.info("【OrderFacade】取消订单, orderId={}", orderId);
// 实际调用各个子系统
}
}
配套 DTO:
package com.dabai.mall.request;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PlaceOrderRequest {
private Long userId;
private Long goodsId;
private Integer quantity;
private String address;
}
package com.dabai.mall.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PlaceOrderResponse {
private Long orderId;
private Long paymentId;
private Long shipmentId;
private BigDecimal price;
}
业务异常:
package com.dabai.mall.facade;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
4.4 业务调用方:Controller
package com.dabai.mall.controller;
import com.dabai.mall.facade.OrderFacade;
import com.dabai.mall.request.PlaceOrderRequest;
import com.dabai.mall.response.PlaceOrderResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* ✅ 订单 Controller:只调外观
*
* @author 大白商城技术团队
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderFacade orderFacade;
/**
* 一站式下单
*/
@PostMapping("/place")
public PlaceOrderResponse placeOrder(@RequestBody PlaceOrderRequest request) {
return orderFacade.placeOrder(request);
}
/**
* 取消订单
*/
@PostMapping("/cancel/{orderId}")
public void cancelOrder(@PathVariable Long orderId) {
orderFacade.cancelOrder(orderId);
}
}
关键点:
| 关键点 | 解释 |
|---|---|
| Controller 只调 OrderFacade | 完全不知道 6 个子系统 |
| 加新子系统 | 只改 OrderFacade |
| 前端只调 1 个接口 | /api/orders/place |
4.5 完整调用流程
前端 POST /api/orders/place
↓
OrderController.placeOrder()
↓
OrderFacade.placeOrder() ← ✅ 外观(只调这一个)
↓ 内部依次调
1. GoodsService.checkStock() ← 检查库存
2. InventoryService.lockStock() ← 锁定库存
3. GoodsService.getPrice() ← 算价格
4. OrderService.createOrder() ← 创建订单
5. PaymentService.createPayment() ← 创建支付单
6. ShippingService.createShipment() ← 创建发货单
7. NotificationService.sendOrderCreatedNotification() ← 通知
↓
返回 PlaceOrderResponse
4.6 单元测试
package com.dabai.mall.facade;
import com.dabai.mall.request.PlaceOrderRequest;
import com.dabai.mall.response.PlaceOrderResponse;
import com.dabai.mall.subsystem.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* 外观完整单元测试
*/
class OrderFacadeTest {
private OrderFacade orderFacade;
@BeforeEach
void setUp() {
orderFacade = new OrderFacade();
// 用反射注入子系统
try {
java.lang.reflect.Field goodsField = OrderFacade.class.getDeclaredField("goodsService");
goodsField.setAccessible(true);
goodsField.set(orderFacade, new GoodsService());
java.lang.reflect.Field inventoryField = OrderFacade.class.getDeclaredField("inventoryService");
inventoryField.setAccessible(true);
inventoryField.set(orderFacade, new InventoryService());
java.lang.reflect.Field orderField = OrderFacade.class.getDeclaredField("orderService");
orderField.setAccessible(true);
orderField.set(orderFacade, new OrderService());
java.lang.reflect.Field paymentField = OrderFacade.class.getDeclaredField("paymentService");
paymentField.setAccessible(true);
paymentField.set(orderFacade, new PaymentService());
java.lang.reflect.Field shippingField = OrderFacade.class.getDeclaredField("shippingService");
shippingField.setAccessible(true);
shippingField.set(orderFacade, new ShippingService());
java.lang.reflect.Field notificationField = OrderFacade.class.getDeclaredField("notificationService");
notificationField.setAccessible(true);
notificationField.set(orderFacade, new NotificationService());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
void testPlaceOrder() {
PlaceOrderRequest request = PlaceOrderRequest.builder()
.userId(1001L)
.goodsId(2001L)
.quantity(2)
.address("北京市朝阳区")
.build();
PlaceOrderResponse response = orderFacade.placeOrder(request);
assertNotNull(response);
assertNotNull(response.getOrderId());
assertNotNull(response.getPaymentId());
assertNotNull(response.getShipmentId());
}
@Test
void testPlaceOrderInsufficientStock() {
PlaceOrderRequest request = PlaceOrderRequest.builder()
.userId(1001L)
.goodsId(2001L)
.quantity(9999) // 库存不足
.address("北京市朝阳区")
.build();
assertThrows(BusinessException.class, () -> orderFacade.placeOrder(request));
}
@Test
void testCancelOrder() {
// 取消订单不抛异常即可
assertDoesNotThrow(() -> orderFacade.cancelOrder(1001L));
}
}
五、外观 vs 适配器 / 中介者 / 代理
5.1 一张表看清区别
| 模式 | 关注点 | 包装谁 | 接口变化 | 适用场景 |
|---|---|---|---|---|
| 外观 | 简化多接口 | 多个子系统 | ❌ 不变 | 多子系统协同 |
| 适配器 | 翻译单接口 | 1 个第三方 | ✅ 改变 | 对接不兼容 |
| 中介者 | 协调多对象 | 多个对象 | ❌ 不变 | 对象间通信 |
| 代理 | 控制访问 | 1 个对象 | ❌ 不变 | 权限/远程/延迟 |
5.2 一个具体例子区分 4 个模式
大白商城订单:
| 模式 | 例子 |
|---|---|
| 外观 | OrderFacade 封装 6 个子系统的下单流程 |
| 适配器 | 把顺丰/中通/京东包装成统一物流接口 |
| 中介者 | 订单事件总线,让订单/库存/支付互相通信 |
| 代理 | Spring AOP 给 OrderFacade 包装日志/事务 |
5.3 决策树
要封装多个子系统的复杂调用?
├── 是
│ ├── 子系统之间需要互相通信?
│ │ ├── 是 → ✅ 中介者
│ │ └── 否 → ✅ 外观
└── 否
├── 包装第三方接口?
│ ├── 是 → ✅ 适配器
│ └── 否
│ ├── 控制访问?
│ │ ├── 是 → ✅ 代理
│ │ └── 否 → 普通方法
5.4 外观 vs 中介者:易混淆
| 维度 | 外观 | 中介者 |
|---|---|---|
| 客户端 | 只调外观 | 客户端直接调子系统 |
| 子系统关系 | 子系统不知道彼此 | 子系统互相通信通过中介者 |
| 调用方向 | 客户端 → 外观 → 子系统 | 子系统 ↔ 中介者 ↔ 子系统 |
| 类比 | 前台 | 调度中心 |
大白商城选型:
| 场景 | 选哪个 | 原因 |
|---|---|---|
| 下单门面 | 外观 | 客户端只调一个方法 |
| 订单事件总线 | 中介者 | 订单/库存/支付互相通知 |
| 物流跟踪 | 外观 | 客户端只调 track() |
| 订单/库存解耦 | 中介者 | 订单发事件,库存订阅 |
六、SLF4J + JdbcTemplate 怎么借鉴外观?
6.1 SLF4J LoggerFactory:日志外观
源码位置:
- SLF4J 2.0.x
org.slf4j.LoggerFactory
核心源码:
/**
* ✅ 外观:日志工厂
* <p>
* 隐藏了底层日志框架(Logback / Log4j2 / JDK Logging)
*/
public class LoggerFactory {
/**
* 客户端只调这个方法
*/
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
// ...
return logger;
}
}
关键点:
| 关键点 | 解释 |
|---|---|
客户端只调 LoggerFactory.getLogger() | 不关心底层 |
内部委托给 ILoggerFactory | 隐藏实现 |
| 切换日志框架 | 改 classpath,不改代码 |
6.2 Spring JdbcTemplate:JDBC 外观
源码位置:
- Spring Framework 6.1.x
spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
核心源码(简化版):
/**
* ✅ 外观:JDBC 模板
* <p>
* 隐藏了 JDBC 样板代码
*/
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
/**
* 查询单个
*/
@Override
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) {
// 1. 获取连接
// 2. 创建 Statement
// 3. 执行 SQL
// 4. 映射结果
// 5. 关闭资源(finally)
// ✅ 客户端只调一个方法
}
}
完整 JDBC vs JdbcTemplate:
/**
* ❌ 老式 JDBC:客户端要写 10 行样板
*/
Connection conn = dataSource.getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setLong(1, 1001L);
rs = ps.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
}
} finally {
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
}
/**
* ✅ JdbcTemplate:客户端只调 1 行
*/
User user = jdbcTemplate.queryForObject(
"SELECT * FROM user WHERE id = ?",
new BeanPropertyRowMapper<>(User.class),
1001L
);
收益:
| 优化点 | 效果 |
|---|---|
| 客户端只调 1 个方法 | 简化使用 |
| 内部管连接/异常/关闭 | 隐藏细节 |
| 统一异常处理 | 提升体验 |
6.3 大白商城的"抄作业"
/**
* 借鉴 JdbcTemplate:订单操作模板
*/
public class OrderTemplate {
@Autowired
private OrderDao orderDao;
/**
* 一站式查订单
*/
public Order getOrder(Long orderId) {
// 1. 查订单
Order order = orderDao.selectById(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 2. 查订单项
List<OrderItem> items = orderDao.selectItemsByOrderId(orderId);
order.setItems(items);
// 3. 查支付信息
Payment payment = orderDao.selectPaymentByOrderId(orderId);
order.setPayment(payment);
return order;
}
}
七、工程决策 Checklist
7.1 ✅ 这 5 种情况,强烈建议用外观
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 多子系统协同 | 简化客户端调用 |
| ② | 跨域复杂操作 | 统一入口 |
| ③ | 前端只调 1 个接口 | 减少前后端联调 |
| ④ | 统一事务管理 | 集中事务边界 |
| ⑤ | 保护子系统 | 客户端不直接接触子系统 |
7.2 ❌ 这 5 种情况,绝对不要用外观
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 只有 1 个子系统 | 外观是多余的 |
| ② | 业务逻辑复杂 | 外观只是协调,不写业务 |
| ③ | 客户端要细粒度控制 | 外观太粗 |
| ④ | 子系统经常变 | 外观要跟着改 |
| ⑤ | 性能极致敏感 | 外观多一层调用 |
7.3 ⚠️ 外观的 6 大常见坑
| 序号 | 坑 | 表现 | 解决方案 |
|---|---|---|---|
| ① | 上帝类 | 外观实现业务逻辑 | 外观只协调,业务在子系统 |
| ② | 层次太多 | Facade 调用 Facade | 限制层次 |
| ③ | 事务难管 | 跨子系统事务不一致 | 分布式事务 |
| ④ | 接口臃肿 | 外观接口太多方法 | 按业务域拆多外观 |
| ⑤ | 客户端绕过 | 客户端直接调子系统 | 强制只调外观 |
| ⑥ | 性能损耗 | 外观多一层 | 缓存常用结果 |
7.4 面试官视角:外观高频追问
Q1:外观 vs 中介者,区别是什么?
答:外观单向调用(客户端 → 外观 → 子系统),中介者双向通信(子系统通过中介者互相通信)。外观是"前台",中介者是"调度中心"。
Q2:外观会不会变成上帝类?
答:会的。避免方法:外观只做"协调",不写业务逻辑。如果开始写业务,就违反单一职责。
Q3:Spring JdbcTemplate 是外观吗?
答:是的。
JdbcTemplate把 JDBC 的连接/Statement/ResultSet/异常/关闭全封装了,客户端只调queryForObject()。Spring 大量用外观模式(JdbcTemplate / RestTemplate / TransactionTemplate)。
八、与其他模式协作
8.1 外观 + 单例 = 共享外观
/**
* 外观 + 单例:共享同一个外观实例
*/
@Service
public class OrderFacade {
// Spring 默认单例
// 所有 Controller 共享一个 OrderFacade
}
8.2 外观 + 工厂方法 = 工厂创建外观
/**
* 外观 + 工厂:按场景创建外观
*/
public class FacadeFactory {
public static OrderFacade createOrderFacade(boolean isAsync) {
OrderFacade facade = new OrderFacade();
// 配置外观
return facade;
}
}
8.3 外观 + 中介者 = 外观管同步,中介者管异步
/**
* 外观(同步下单)
* + 中介者(异步事件)
*/
@Service
public class OrderFacade {
@Autowired
private OrderEventBus eventBus; // 中介者
public PlaceOrderResponse placeOrder(PlaceOrderRequest request) {
// 1. 同步:订单/库存/支付/物流(外观)
Long orderId = orderService.createOrder(request);
inventoryService.lockStock(request);
// 2. 异步:发事件(中介者)
eventBus.publish(new OrderCreatedEvent(orderId));
// 库存/支付/物流订阅事件,异步处理
}
}
8.4 大白商城模式协作全景图
┌──────────────┐
│ 外观 │ ← 本篇
└──────┬───────┘
│
┌───────────┬───────┼───────┬───────────┐
│ │ │ │ │
┌───▼───┐ ┌────▼───┐ ┌▼────┐ ┌▼─────┐ ┌───▼────┐
│ 单例 │ │ 工厂方法│ │中介者│ │适配器│ │ 代理 │
│(共享) │ │(创建) │ │(异步)│ │(翻译)│ │(管控) │
└───────┘ └────────┘ └──────┘ └──────┘ └───────┘
02 篇 04 篇 37 篇 14 篇 12 篇
九、本篇小结 + 下篇预告
9.1 本篇小结(5 个核心要点)
- 本质:外观 = 前台接待员,简化多子系统调用,客户端只调 1 个接口。
- 场景:多子系统协同 / 跨域复杂操作 / 前端只调 1 个接口,都该用外观。
- 对比:外观 vs 适配器(多 vs 单)/ 外观 vs 中介者(单向 vs 双向)/ 外观 vs 代理(简化 vs 管控)。
- SLF4J + JdbcTemplate:经典的"外观模式"应用,隐藏底层细节。
- 避坑:上帝类 / 层次太多 / 事务难管 / 接口臃肿。
9.2 一句话总结
外观不是"换种方式调方法",是"把 8 个子系统的调用封装成 1 个接口"。大白商城日均 50 万订单的创建,从前端调 8 个接口变成调 1 个
/api/orders/place——前后端效率提升 5 倍。
9.3 知识脑图
外观模式
├── 2 大角色
│ ├── 外观(OrderFacade)
│ └── 子系统(6 个 Service)
├── 实战要点
│ ├── 完整 pom + 3 个测试
│ ├── 6 个子系统协同
│ └── 加新子系统只改外观
├── 模式对比
│ ├── vs 适配器(多 vs 单)
│ ├── vs 中介者(单向 vs 双向)
│ └── vs 代理(简化 vs 管控)
├── 经典案例
│ ├── SLF4J LoggerFactory
│ ├── Spring JdbcTemplate
│ └── Spring RestTemplate
└── 模式协作
├── + 单例(共享外观)
├── + 工厂方法(创建外观)
└── + 中介者(同步+异步)
9.4 下篇预告
第 19 篇【外观模式 - 源码剖析篇】:JDK / Spring 中的外观实现
下一篇我们会深入源码,回答三个问题:
- SLF4J 的
LoggerFactory怎么用外观包装多种日志框架? - Spring 的
JdbcTemplate/RestTemplate/TransactionTemplate怎么用外观简化 API? - MyBatis 的
SqlSession怎么用外观包装 JDBC?
并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯
&spm=1001.2101.3001.5002&articleId=163794822&d=1&t=3&u=f4acb98417334a23a60a56a2ba79f6d6)
585

被折叠的 条评论
为什么被折叠?



