第13章:外观模式——给复杂系统一个“简化接口”

外观模式,说白了就是给一堆乱七八糟的子系统提供一个统一的“总开关”。你不需要知道内部怎么接线,按一下按钮,该亮的灯全亮,该转的风扇全转。我最早接触这个模式,是在做一个智能家居控制系统的项目。当时客户说:“我就想按一个‘离家模式’按钮,所有灯关掉、空调关闭、窗帘拉上、安防启动。”嗯,这不就是典型的外观模式吗?

13.1 什么是外观模式?

外观模式(Facade Pattern)属于结构型设计模式。它为一组复杂的子系统接口提供一个更高级的、统一的接口。这个高级接口让子系统更容易使用。

你想想看,我们平时用电脑,开机只需要按一下电源键。但背后发生了什么?电源自检、BIOS加载、操作系统启动、驱动加载、服务启动……这一堆流程,全被“开机键”这个外观给封装了。用户不需要知道这些,按就完了。

核心定义:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

13.2 外观模式的结构

外观模式的结构其实很简单,就三个角色:

  • 外观(Facade): 对外提供统一接口,内部调用各个子系统的方法。
  • 子系统(SubSystem): 一组完成特定功能的类,它们之间可能有复杂的依赖关系。
  • 客户端(Client): 通过外观类来访问子系统,不直接与子系统交互。

下面这张图,是我自己画的外观模式结构图,你看一眼就明白了:

客户端 Facade(外观) + operation() 子系统(SubSystem) 子系统A + methodA1() + methodA2() 子系统B + methodB1() + methodB2() 子系统C + methodC1() + methodC2()

13.3 代码实现:一个订单系统的外观

我记得有一次帮一个电商平台重构订单提交流程。原来客户端下单要调用库存、支付、物流、通知四个子系统,代码耦合得一塌糊涂。后来我用外观模式重构了一下,清爽多了。

先看子系统代码:

// 子系统1:库存服务
public class InventoryService {
    public boolean checkStock(String productId, int quantity) {
        System.out.println("检查库存: " + productId + ", 数量: " + quantity);
        return true; // 假设库存充足
    }
    
    public void reduceStock(String productId, int quantity) {
        System.out.println("扣减库存: " + productId + ", 数量: " + quantity);
    }
}

// 子系统2:支付服务
public class PaymentService {
    public boolean processPayment(String orderId, double amount) {
        System.out.println("处理支付: 订单 " + orderId + ", 金额 " + amount);
        return true;
    }
}

// 子系统3:物流服务
public class ShippingService {
    public void createShipping(String orderId, String address) {
        System.out.println("创建物流: 订单 " + orderId + ", 地址 " + address);
    }
}

// 子系统4:通知服务
public class NotificationService {
    public void sendOrderConfirmation(String email, String orderId) {
        System.out.println("发送确认邮件: " + email + ", 订单 " + orderId);
    }
}

然后是我们的外观类:

// 外观类:订单服务
public class OrderFacade {
    private InventoryService inventory;
    private PaymentService payment;
    private ShippingService shipping;
    private NotificationService notification;
    
    public OrderFacade() {
        this.inventory = new InventoryService();
        this.payment = new PaymentService();
        this.shipping = new ShippingService();
        this.notification = new NotificationService();
    }
    
    public void placeOrder(String productId, int quantity, 
                          double amount, String address, String email) {
        System.out.println("===== 开始下单 =====");
        
        // 1. 检查库存
        if (!inventory.checkStock(productId, quantity)) {
            System.out.println("库存不足,下单失败");
            return;
        }
        
        // 2. 扣减库存
        inventory.reduceStock(productId, quantity);
        
        // 3. 处理支付
        String orderId = "ORD" + System.currentTimeMillis();
        if (!payment.processPayment(orderId, amount)) {
            System.out.println("支付失败,下单失败");
            // 这里应该回滚库存,简化处理
            return;
        }
        
        // 4. 创建物流
        shipping.createShipping(orderId, address);
        
        // 5. 发送通知
        notification.sendOrderConfirmation(email, orderId);
        
        System.out.println("===== 下单成功 =====");
    }
}

客户端调用变得极其简单:

public class Client {
    public static void main(String[] args) {
        OrderFacade orderFacade = new OrderFacade();
        
        // 一行代码完成下单
        orderFacade.placeOrder("P1001", 2, 299.99, 
                              "北京市朝阳区", "user@example.com");
    }
}

个人经验:我在实际项目中,经常把外观类设计成单例模式。因为外观类本身是无状态的,多个客户端共享一个实例完全没问题。这样还能节省内存,避免重复创建子系统的实例。

13.4 迪米特法则——外观模式的理论基础

外观模式背后有一个重要的设计原则——迪米特法则(Law of Demeter),也叫最少知识原则。它的核心思想是:一个对象应该对其他对象有最少的了解。

说白了就是:不要和陌生人说话。你只需要跟你直接的朋友交互,不要绕来绕去调用别人的内部方法。

违反迪米特法则的代码长这样:

// 糟糕的设计:客户端直接操作子系统内部
public void badOrderProcess() {
    InventoryService inv = new InventoryService();
    PaymentService pay = new PaymentService();
    
    inv.checkStock("P1001", 2);
    inv.reduceStock("P1001", 2);
    pay.processPayment("ORD123", 299.99);
    // ... 客户端知道太多内部细节了
}

遵守迪米特法则的设计:

// 好的设计:客户端只跟外观交互
public void goodOrderProcess() {
    OrderFacade facade = new OrderFacade();
    facade.placeOrder("P1001", 2, 299.99, "北京", "user@example.com");
}

我曾经踩过的坑:有一次我过度追求迪米特法则,把外观类设计得特别大,里面塞了几十个方法。结果外观类变成了“上帝类”,耦合反而更高了。记住,外观模式是简化接口,不是把所有逻辑都塞进一个类里。如果一个外观类超过20个方法,就该考虑拆分了。

13.5 外观模式的优缺点

优点 缺点
  • 简化使用:客户端只需要跟外观交互,不用了解子系统内部
  • 降低耦合:客户端与子系统解耦,子系统变化不影响客户端
  • 分层清晰:外观可以作为系统分层入口,比如Service层
  • 编译依赖减少:客户端只需要依赖外观类,不需要依赖所有子系统
  • 不符合开闭原则:外观类修改可能影响客户端
  • 可能变成上帝类:如果外观类塞了太多功能,反而成为维护负担
  • 隐藏了子系统特性:有些高级功能可能被外观屏蔽了

我的建议:外观模式不是银弹。如果你的子系统本身就很稳定,或者客户端需要灵活调用子系统的各种组合,那就不适合用外观。我一般会在以下情况使用:子系统接口变化频繁、客户端只需要固定几种操作流程、或者想给第三方提供简化API。

13.6 应用场景

外观模式在实际项目中非常常见,我列举几个典型的场景:

  1. 复杂系统的简化入口:比如一个视频转换工具,内部涉及解码、滤镜、编码、封装等多个步骤。外观类提供一个 convert() 方法,客户端传个文件路径就完事了。
  2. 遗留系统接口封装:老系统的接口往往又乱又难用。用外观模式包一层,新系统只跟外观交互。我做过一个银行核心系统的接口封装,老系统有200多个方法,外观只暴露了20个。
  3. 分层架构中的服务层:在DDD架构中,Application Service 本质上就是一个外观。它协调领域对象、仓储、基础设施来完成一个用例。
  4. 第三方库的适配:有些第三方库API设计得很糟糕,用外观模式包一层,让你的业务代码跟第三方库解耦。哪天换库了,只改外观类就行。
  5. 单元测试的替身:外观类可以方便地做Mock。你不需要Mock所有子系统,只需要Mock外观类就行。

嗯,外观模式就讲到这里。它不是什么高深的技术,但用好了能让你的代码结构清晰很多。记住一句话:给复杂系统一个简单的门面,让调用者只关心“做什么”,不关心“怎么做”


公众号:蓝海资料掘金营,微信deep3321