Appearance
外观模式 (Facade Pattern)
一、定义
一句话概括:为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。
官方定义(GoF):Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
外观模式的核心思想是:引入一个外观类(Facade),封装子系统的复杂性,对外提供简单、清晰的接口。客户端只需要与外观类交互,而不需要了解子系统内部的复杂细节。
二、解决的问题
2.1 什么场景下需要外观模式?
- 简化复杂系统:一个子系统包含多个类,客户端需要依次调用多个方法才能完成一个业务操作。
- 解耦客户端与子系统:降低客户端与子系统之间的耦合度,子系统的变化不影响客户端。
- 分层架构:为每一层提供统一的入口,如 Service 层调用多个 DAO、Gateway 调用多个微服务。
- 遗留系统封装:将复杂的遗留系统包装成简单接口,方便新系统调用。
2.2 不用外观模式会有什么问题?
假设一个"订单下单"流程需要依次调用:库存服务、支付服务、物流服务、通知服务。
java
// 不用外观模式 —— 客户端需要了解所有子系统
public void placeOrder(Order order) {
InventoryService inventory = new InventoryService();
PaymentService payment = new PaymentService();
LogisticsService logistics = new LogisticsService();
NotificationService notification = new NotificationService();
boolean inStock = inventory.checkStock(order.getProductId());
if (!inStock) throw new RuntimeException("库存不足");
boolean paid = payment.pay(order.getUserId(), order.getAmount());
if (!paid) throw new RuntimeException("支付失败");
String trackingNo = logistics.createShipment(order.getId());
notification.send(order.getUserId(), "订单已发货,运单号: " + trackingNo);
}问题1:客户端耦合严重 —— 依赖 4 个服务,任一服务接口变化都要修改客户端
问题2:调用顺序复杂 —— 客户端需要知道正确的调用顺序,容易出错
问题3:代码重复 —— 每个下单的地方都要写同样的调用逻辑
问题4:难以测试 —— 客户端测试需要 mock 4 个服务三、结构
3.1 文字描述
外观模式包含以下角色:
- Facade(外观类):提供统一的高层接口,内部调用子系统各组件。
- Subsystem Classes(子系统类):实现子系统的具体功能,处理 Facade 的委托。
- Client(客户端):通过 Facade 与子系统交互,不直接调用子系统类。
3.2 ASCII 类图
┌──────────────────────┐
│ Facade │
├──────────────────────┤
│ + simpleOperation() │
│ subA.op1() │
│ subB.op2() │
│ subC.op3() │
└──┬──────┬──────┬──────┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Subsystem│ │ Subsystem│ │ Subsystem│
│ A │ │ B │ │ C │
├──────────┤ ├──────────┤ ├──────────┤
│ + op1() │ │ + op2() │ │ + op3() │
└──────────┘ └──────────┘ └──────────┘
Client → Facade → Subsystem A/B/C
(客户端不直接访问子系统)四、代码实现
4.1 基础实现
场景:智能家居控制系统
java
// ============ 子系统类 ============
class Light {
public void on() { System.out.println("开灯"); }
public void off() { System.out.println("关灯"); }
public void dim(int level) { System.out.println("灯光调至 " + level + "%"); }
}
class AirConditioner {
public void on() { System.out.println("空调开启"); }
public void off() { System.out.println("空调关闭"); }
public void setTemperature(int temp) { System.out.println("空调温度设为 " + temp + "°C"); }
}
class Curtain {
public void open() { System.out.println("窗帘打开"); }
public void close() { System.out.println("窗帘关闭"); }
}
class TV {
public void on() { System.out.println("电视开启"); }
public void off() { System.out.println("电视关闭"); }
public void setChannel(int channel) { System.out.println("调到频道 " + channel); }
}
// ============ Facade:智能家居外观 ============
class SmartHomeFacade {
private Light light;
private AirConditioner ac;
private Curtain curtain;
private TV tv;
public SmartHomeFacade() {
this.light = new Light();
this.ac = new AirConditioner();
this.curtain = new Curtain();
this.tv = new TV();
}
// 回家模式:一键开启所有设备
public void comeHome() {
System.out.println("===== 回家模式 =====");
curtain.open();
light.on();
light.dim(80);
ac.on();
ac.setTemperature(26);
tv.on();
tv.setChannel(1);
}
// 离家模式:一键关闭所有设备
public void leaveHome() {
System.out.println("===== 离家模式 =====");
tv.off();
ac.off();
light.off();
curtain.close();
}
// 睡眠模式:只保留空调
public void sleepMode() {
System.out.println("===== 睡眠模式 =====");
tv.off();
light.dim(10);
curtain.close();
ac.setTemperature(25);
}
}
// ============ 客户端测试 ============
public class FacadeDemo {
public static void main(String[] args) {
SmartHomeFacade facade = new SmartHomeFacade();
facade.comeHome(); // 一键回家
// ... 用户活动 ...
facade.sleepMode(); // 一键睡眠
// ... 第二天 ...
facade.leaveHome(); // 一键离家
}
}4.2 进阶实现
4.2.1 微服务网关外观
java
// ============ 微服务子系统 ============
class UserService {
public UserDTO getUser(Long id) {
return new UserDTO(id, "张三", "zhangsan@example.com");
}
}
class OrderService {
public List<OrderDTO> getOrdersByUserId(Long userId) {
return Arrays.asList(
new OrderDTO(1L, userId, 199.00),
new OrderDTO(2L, userId, 299.00));
}
}
class PaymentService {
public PaymentInfo getPaymentInfo(Long orderId) {
return new PaymentInfo(orderId, "PAID");
}
}
class LogisticsService {
public LogisticsInfo getLogistics(Long orderId) {
return new LogisticsInfo(orderId, "IN_TRANSIT", "SF1234567890");
}
}
// ============ DTO ============
class UserDTO {
Long id; String name; String email;
public UserDTO(Long id, String name, String email) {
this.id = id; this.name = name; this.email = email;
}
}
class OrderDTO {
Long id; Long userId; Double amount;
public OrderDTO(Long id, Long userId, Double amount) {
this.id = id; this.userId = userId; this.amount = amount;
}
}
class PaymentInfo {
Long orderId; String status;
public PaymentInfo(Long orderId, String status) {
this.orderId = orderId; this.status = status;
}
}
class LogisticsInfo {
Long orderId; String status; String trackingNo;
public LogisticsInfo(Long orderId, String status, String trackingNo) {
this.orderId = orderId; this.status = status; this.trackingNo = trackingNo;
}
}
// ============ Facade:API 网关 ============
class ApiGatewayFacade {
private UserService userService;
private OrderService orderService;
private PaymentService paymentService;
private LogisticsService logisticsService;
public ApiGatewayFacade() {
this.userService = new UserService();
this.orderService = new OrderService();
this.paymentService = new PaymentService();
this.logisticsService = new LogisticsService();
}
// 用户详情页:聚合多个服务的数据
public UserProfileDTO getUserProfile(Long userId) {
// 1. 获取用户信息
UserDTO user = userService.getUser(userId);
// 2. 获取用户订单
List<OrderDTO> orders = orderService.getOrdersByUserId(userId);
// 3. 为每个订单获取支付和物流信息
List<OrderDetailDTO> orderDetails = new ArrayList<>();
for (OrderDTO order : orders) {
PaymentInfo payment = paymentService.getPaymentInfo(order.id);
LogisticsInfo logistics = logisticsService.getLogistics(order.id);
orderDetails.add(new OrderDetailDTO(order, payment, logistics));
}
return new UserProfileDTO(user, orderDetails);
}
}
// 聚合结果
class UserProfileDTO {
UserDTO user;
List<OrderDetailDTO> orders;
public UserProfileDTO(UserDTO user, List<OrderDetailDTO> orders) {
this.user = user; this.orders = orders;
}
}
class OrderDetailDTO {
OrderDTO order;
PaymentInfo payment;
LogisticsInfo logistics;
public OrderDetailDTO(OrderDTO order, PaymentInfo payment, LogisticsInfo logistics) {
this.order = order; this.payment = payment; this.logistics = logistics;
}
}4.2.2 复杂子系统简化
java
// ============ 视频转换子系统(复杂)============
class VideoFile { /* 视频文件 */ }
class AudioMixer { public AudioFile fix(AudioFile audio) { /* 修复音频 */ return new AudioFile(); } }
class AudioFile { /* 音频文件 */ }
class CodecFactory {
public Codec extract(VideoFile file) { return new Codec(); }
}
class Codec { /* 编解码器 */ }
class BitrateReader {
public static VideoFile read(VideoFile file, Codec codec) { return file; }
public static VideoFile convert(VideoFile buffer, Codec codec) { return buffer; }
}
class OggCompressionCodec extends Codec { /* OGG 压缩 */ }
class MPEG4CompressionCodec extends Codec { /* MP4 压缩 */ }
// ============ Facade:视频转换外观 ============
class VideoConverter {
public VideoFile convert(String fileName, String format) {
VideoFile file = new VideoFile();
Codec sourceCodec = new CodecFactory().extract(file);
Codec destinationCodec;
if ("mp4".equals(format)) {
destinationCodec = new MPEG4CompressionCodec();
} else {
destinationCodec = new OggCompressionCodec();
}
VideoFile buffer = BitrateReader.read(file, sourceCodec);
VideoFile result = BitrateReader.convert(buffer, destinationCodec);
// 修复音频
AudioMixer mixer = new AudioMixer();
mixer.fix(new AudioFile());
return result;
}
}
// 客户端只需一行代码:
// VideoConverter converter = new VideoConverter();
// VideoFile mp4 = converter.convert("funny-cats.ogg", "mp4");4.3 生产级实现
Spring Boot 订单服务外观
java
// ============ 订单提交外观 ============
@Service
class OrderFacade {
@Autowired
private InventoryService inventoryService;
@Autowired
private CouponService couponService;
@Autowired
private PaymentService paymentService;
@Autowired
private OrderRepository orderRepository;
@Autowired
private MessageProducer messageProducer;
@Transactional(rollbackFor = Exception.class)
public OrderResult submitOrder(OrderRequest request) {
// 1. 参数校验
validateRequest(request);
// 2. 库存扣减(预占)
boolean locked = inventoryService.lockStock(
request.getSkuId(), request.getQuantity());
if (!locked) {
throw new BusinessException("库存不足");
}
// 3. 优惠券核销
BigDecimal discount = BigDecimal.ZERO;
if (request.getCouponId() != null) {
discount = couponService.useCoupon(
request.getUserId(), request.getCouponId());
}
// 4. 计算金额
BigDecimal totalAmount = request.getUnitPrice()
.multiply(BigDecimal.valueOf(request.getQuantity()))
.subtract(discount);
// 5. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setSkuId(request.getSkuId());
order.setQuantity(request.getQuantity());
order.setTotalAmount(totalAmount);
order.setStatus(OrderStatus.PENDING_PAYMENT);
orderRepository.save(order);
// 6. 发起支付
PaymentResult payment = paymentService.pay(
order.getId(), totalAmount, request.getPayChannel());
// 7. 发送消息通知
messageProducer.sendOrderCreated(order);
return new OrderResult(order.getId(), payment.getPayUrl());
}
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
// 1. 释放库存
inventoryService.releaseStock(order.getSkuId(), order.getQuantity());
// 2. 退还优惠券
if (order.getCouponId() != null) {
couponService.returnCoupon(order.getUserId(), order.getCouponId());
}
// 3. 退款
paymentService.refund(order.getId());
// 4. 更新状态
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
// 5. 发送通知
messageProducer.sendOrderCancelled(order);
}
}五、优缺点
优点
| 优点 | 说明 |
|---|---|
| 简化接口 | 将复杂的子系统调用简化为一个高层接口 |
| 降低耦合 | 客户端与子系统解耦,子系统变化不影响客户端 |
| 分层清晰 | 符合分层架构思想,Facade 成为子系统入口 |
| 提高安全性 | 可以屏蔽子系统中不安全的操作 |
缺点
| 缺点 | 说明 |
|---|---|
| 不符合开闭原则 | 新增子系统功能需要修改 Facade 类 |
| 可能成为上帝类 | Facade 承担过多职责,变成"上帝对象" |
| 灵活性降低 | 客户端只能通过 Facade 访问子系统,无法直接使用高级功能 |
六、适用场景
- 复杂子系统简化:为复杂的子系统提供简单入口,如第三方 SDK 封装。
- 分层架构:为每一层提供统一入口,Service 层封装多个 DAO 操作。
- 微服务聚合:API 网关聚合多个微服务的响应。
- 遗留系统封装:将复杂的遗留系统包装成简单接口。
- 工具类库:为复杂的工具类库提供便捷方法。
- 第三方集成:封装第三方 SDK 的复杂调用链。
七、JDK / Spring 框架中的实际应用
7.1 JDK 中的外观模式
java
// 1. java.net.URL —— 外观模式封装了复杂的网络操作
URL url = new URL("https://www.example.com");
URLConnection conn = url.openConnection();
// 内部封装了 DNS 解析、Socket 建立、协议处理等
// 2. javax.faces.context.FacesContext —— JSF 的外观
// 封装了 ServletContext、HttpSession、HttpServletRequest 等
// 3. Class.forName() —— 外观模式封装了类加载机制
Class<?> clazz = Class.forName("com.example.MyClass");7.2 Spring 中的外观模式
java
// 1. JdbcTemplate —— 封装了 JDBC 的复杂操作
// 不用手动管理 Connection、Statement、ResultSet
jdbcTemplate.query("SELECT * FROM users", rowMapper);
// 2. RestTemplate —— 封装了 HTTP 请求的复杂操作
restTemplate.getForObject("http://api.example.com/users/1", User.class);
// 3. Spring MVC 的 DispatcherServlet
// 封装了请求分发、处理器映射、视图解析等复杂流程
// 4. Spring Data JPA 的 Repository
// 封装了 EntityManager、Transaction 等复杂操作八、与其他模式的关系
| 模式 | 关系 |
|---|---|
| 适配器模式 | 适配器改变接口以适配不同系统,外观简化接口以方便使用。适配器通常是事后补救,外观通常是事前设计。 |
| 抽象工厂模式 | 抽象工厂可以和外观模式配合,外观通过抽象工厂创建子系统对象。 |
| 中介者模式 | 外观和中介者都封装了多个对象的交互,但外观是单向调用(Client→Facade→Subsystem),中介者是双向协调(同事之间互相通信)。 |
| 单例模式 | 外观类通常设计为单例,因为只需要一个入口。 |
| 代理模式 | 外观可以为子系统添加额外的代理逻辑,如缓存、权限校验。 |
九、面试常见问题
Q1:外观模式和适配器模式有什么区别?
答:
- 意图不同:外观模式简化接口,为复杂子系统提供统一入口;适配器模式转换接口,让不兼容的接口协同工作。
- 使用时机不同:外观模式通常在设计阶段主动规划;适配器模式通常在维护阶段事后补救。
- 方向不同:外观提供新接口来包装旧接口;适配器复用已有接口进行转换。
Q2:外观模式会违反开闭原则,如何解决?
答:可以通过引入抽象外观类来解决。定义抽象外观接口,每个子系统版本对应一个具体外观实现。新增子系统时,创建新的外观实现类而不是修改原有外观类。
Q3:外观模式和中介者模式有什么区别?
答:
- 外观模式是单向的:Client → Facade → Subsystems,子系统之间不通过各种 Facade 通信。
- 中介者模式是双向的:Colleague ↔ Mediator ↔ Colleague,同事对象通过中介者互相通信。
- 外观是对外暴露的简化接口,中介者是内部协调的中心。
Q4:微服务架构中外观模式如何应用?
答:API 网关(如 Spring Cloud Gateway)就是外观模式在微服务中的典型应用。网关将多个微服务的接口聚合为一个统一的对外 API,客户端不需要知道微服务的具体地址和调用方式。
Q5:外观模式是否意味着客户端不能直接访问子系统?
答:不是。外观模式只是提供一个简化的入口,但并不禁止客户端直接访问子系统。如果客户端需要子系统的更高级功能,仍然可以直接调用子系统类。外观模式是可选的便利层。
