大白话说Java设计模式-18-外观模式(业务实战篇)

📌 PDF:AI人工智能 — RAG知识库实战项目

大白话说Java设计模式-18-外观模式(业务实战篇):大白商城下单门面的"一站式服务"

📌 一句话本质:外观模式就是"前台接待员",把一堆子系统的调用封装成简单接口。

🏷️ 标签:外观模式 / Java 设计模式 / 门面模式 / 大白商城 / 一站式服务   🎯 适合:初中级后端 / 经常被"子系统太多"困扰的工程师


目录


一、业务场景引入:下单为什么要"一站式服务"?

大白商城 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 个接口(前后端都累)
  • 正确做法
    1. 大白商城设一个"前台"(OrderFacade
    2. 前端只调"前台"的一个方法:orderFacade.placeOrder(request)
    3. 前台内部依次调 8 个子系统
    4. 前端完全不需要知道有几个子系统

前台 = 外观模式让"一个简单接口代替多个复杂接口"

外观模式 = 为子系统中的一组接口提供一个一致的界面,定义一个高层接口,让子系统更容易使用

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 个接口。
  2. 场景:多子系统协同 / 跨域复杂操作 / 前端只调 1 个接口,都该用外观
  3. 对比:外观 vs 适配器(多 vs 单)/ 外观 vs 中介者(单向 vs 双向)/ 外观 vs 代理(简化 vs 管控)。
  4. SLF4J + JdbcTemplate:经典的"外观模式"应用,隐藏底层细节
  5. 避坑:上帝类 / 层次太多 / 事务难管 / 接口臃肿。

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 中的外观实现

下一篇我们会深入源码,回答三个问题:

  1. SLF4J 的 LoggerFactory 怎么用外观包装多种日志框架?
  2. Spring 的 JdbcTemplate / RestTemplate / TransactionTemplate 怎么用外观简化 API?
  3. MyBatis 的 SqlSession 怎么用外观包装 JDBC?

并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

计算机小能手(AI+Java)

若对您有所帮助,请点点关注哟~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值