Skip to content

适配器、外观与桥接:结构型模式的解耦手法

本文是设计模式系统学习系列的 L2 核心篇。前置:[8. 责任链与命令]。 学完可以配合面试题食用:服务通信 REST/gRPC

为什么需要这三种"包一层"的模式

结构型模式解决的都是"怎么把我已有的代码组织得更好",而不是"怎么写新代码"。适配器、外观、桥接——名字不同,结构上都是"在中间加一层"。区别在于这一层解决了三种完全不同的问题:

  • 适配器:接口不兼容,加一层转换
  • 外观:子系统太复杂,加一层简化入口
  • 桥接:两个维度都在变化,加一层解耦

下面逐个拆开,每节都带一个工程里你肯定见过的实例。

适配器:它不兼容,我转一下

适配器想解决的问题很直接:你有一个接口 A,但手里只有实现了接口 B 的类,不想改已有的代码,那就写一个适配器把 B 包装成 A。

两种实现方式:

  • 类适配器:继承适配者 + 实现目标接口(Java 单继承限制,很少用)
  • 对象适配器:持有适配者引用 + 实现目标接口(组合方式,更灵活)

工程里最经典的例子是 SLF4J。SLF4J 定义了统一的日志门面接口,各个日志框架(Log4j、Logback、java.util.logging)各自的 API 不同。SLF4J 给每个框架配了一个适配器 jar(slf4j-log4j12slf4j-jdk14),运行期 classpath 上有哪个就适配哪个。你代码里只写 LoggerFactory.getLogger(xxx),底层具体是哪个框架,适配器帮你转。

java
// SLF4J 的门面接口(目标接口)
public interface Logger {
    void info(String msg);
    void error(String msg, Throwable t);
}

// Log4j 的适配器:对象适配器
public class Log4j12Adapter implements Logger {
    private final org.apache.log4j.Logger logger;
    
    public Log4j12Adapter(String name) {
        this.logger = org.apache.log4j.Logger.getLogger(name);
    }
    
    @Override
    public void info(String msg) {
        logger.info(msg);  // 调用 Log4j 自己的 API
    }
    
    @Override
    public void error(String msg, Throwable t) {
        logger.error(msg, t);
    }
}
java
// InputStreamReader:字节流 -> 字符流的适配器
// InputStream 是字节输入,Reader 是字符输入
// 中间加一层 InputStreamReader 做转换
InputStream in = new FileInputStream("data.txt");
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(reader);

Spring MVC 里的 HandlerAdapter 也是适配器模式。DispatcherServlet 拿到请求后,不知道当前 Handler 是 @Controller 方法、HttpRequestHandler 还是 SimpleControllerHandlerAdapter,它只依赖 HandlerAdapter 接口,每种 Handler 有自己的适配器去调。

外观:别管里面多复杂,就调一个方法

外观模式的作用是给一堆复杂子系统提供一个简单入口。不是"把子系统藏起来不让人用",而是"大部分人用这个入口就够了"。

java
// 下单一套流程:没外观时客户端要自己调3个服务
public class OrderServiceClient {
    public void placeOrder(OrderRequest req) {
        inventoryService.checkStock(req.getSkuId(), req.getQuantity());
        paymentService.charge(req.getUserId(), req.getTotalAmount());
        notificationService.sendOrderConfirmation(req.getUserId(), req.getOrderId());
    }
}
java
// 有外观后:客户端只调一个方法
public class OrderFacade {
    private final InventoryService inventory;
    private final PaymentService payment;
    private final NotificationService notification;
    
    public OrderResult placeOrder(OrderRequest req) {
        // 内部编排,甚至能处理回滚
        if (!inventory.checkStock(req.getSkuId(), req.getQuantity())) {
            return OrderResult.fail("库存不足");
        }
        PaymentResult pay = payment.charge(req.getUserId(), req.getTotalAmount());
        if (!pay.success()) {
            return OrderResult.fail("支付失败: " + pay.errorMsg());
        }
        notification.sendOrderConfirmation(req.getUserId(), req.getOrderId());
        return OrderResult.success(req.getOrderId());
    }
}

外观和网关/BFF(Backend For Frontend) 是同一思路在不同尺度的表现——Web 应用里网关给前端统一入口,微服务里 BFF 聚合多个下游接口。它们都是外观模式。

桥接:两个维度独立变化

桥接模式解决的是"一个类有两个独立变化的维度,直接用继承会导致类爆炸"的问题。

最经典的例子是 JDBCjava.sql.Driver 是接口,每个数据库厂商(MySQL、PostgreSQL、Oracle)各自实现。java.sql.ConnectionStatementResultSet 也是接口,同样由各厂商实现。这两个维度是:

  1. 数据库操作抽象(Connection/Statement/ResultSet)
  2. 具体数据库实现(MySQL 驱动、PG 驱动)

如果不用桥接,可能每个数据库的操作都要单独写一个类:MysqlConnectionPgConnectionMysqlStatementPgStatement……n 个数据库 × m 个操作 = n×m 个类。

java
// 桥接模式在 JDBC 中的体现
// 抽象维度:数据库操作接口
interface Connection {
    Statement createStatement();
}

// 实现维度:各数据库驱动
class MysqlDriver implements Driver {
    Connection connect(String url) { return new MysqlConnection(url); }
}

class PgDriver implements Driver {
    Connection connect(String url) { return new PgConnection(url); }
}

// 客户端代码从不关心哪个实现
// 全由 DriverManager 根据 URL 选驱动
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db");
Statement stmt = conn.createStatement();
mermaid
classDiagram
    class DriverManager {
        +getConnection(url) Connection
    }
    class Driver {
        <<interface>>
        +connect(url) Connection
    }
    class Connection {
        <<interface>>
        +createStatement() Statement
    }
    DriverManager --> Driver : 注册
    Driver --> Connection : 创建
    Connection --> Statement : 创建
    class MysqlDriver {
        +connect() MysqlConnection
    }
    class PgDriver {
        +connect() PgConnection
    }
    Driver <|.. MysqlDriver
    Driver <|.. PgDriver
    Connection <|.. MysqlConnection
    Connection <|.. PgConnection

抽象(Connection)和实现(MysqlDriver/PgDriver)可以各自独立扩展。新增一个数据库,只需要加一个 Driver 实现和对应的 Connection/Statement 实现,不需要改现有代码。反过来,新增一个数据库操作抽象(比如 BatchConnection),也只需要加接口和实现,不需要改各厂商的驱动逻辑。

三种模式的辨析:都是"包一层",包给谁看

模式问题解法实例
适配器接口不兼容转换层,把 A 转成 B 能调的形式SLF4J、InputStreamReader
外观子系统太复杂简化入口,封装调用链网关/BFF、下单 Facade
桥接两个维度独立变化抽象和实现分离,各自继承JDBC Driver、日志框架适配+门面

适配器和外观的区别——结构上它们都是"中间加一层",但适配器是别人调的接口和你不一样,外观是别人调的过程太复杂你给简化。桥接则更不同:它不是为了兼容或简化,而是为了让两个维度都能独立扩展,避免继承爆炸。

动手实操:第三方支付 SDK 适配 + 外观封装

假设项目接了三个支付渠道——微信、支付宝、银联——每个渠道的 SDK 接口不同。你想让业务代码统一调一个 pay() 方法,同时下单流程里还要校验、库存、通知,不能分散在各处。

java
// 1. 定义统一支付接口(目标接口)
public interface PaymentAdapter {
    PayResult pay(String orderId, BigDecimal amount);
    PayResult refund(String orderId, BigDecimal amount);
}

// 2. 微信 SDK 适配器(对象适配器)
public class WechatPayAdapter implements PaymentAdapter {
    private final WechatSdk wechatSdk = new WechatSdk();
    
    @Override
    public PayResult pay(String orderId, BigDecimal amount) {
        // 微信 SDK 要求传分(单位转换)
        long fen = amount.multiply(BigDecimal.valueOf(100)).longValue();
        WechatResponse resp = wechatSdk.unifiedOrder(orderId, fen);
        return new PayResult(resp.isSuccess(), resp.getMsg());
    }
    
    @Override
    public PayResult refund(String orderId, BigDecimal amount) {
        long fen = amount.multiply(BigDecimal.valueOf(100)).longValue();
        WechatResponse resp = wechatSdk.refund(orderId, fen);
        return new PayResult(resp.isSuccess(), resp.getMsg());
    }
}

// 3. 支付宝适配器(SDK 接口不同)
public class AlipayAdapter implements PaymentAdapter {
    private final AlipaySdk alipaySdk = new AlipaySdk();
    
    @Override
    public PayResult pay(String orderId, BigDecimal amount) {
        // 支付宝 SDK 传元
        AlipayResponse resp = alipaySdk.tradeCreate(orderId, amount.toString());
        return new PayResult(resp.isSuccess(), resp.getMsg());
    }
    
    @Override
    public PayResult refund(String orderId, BigDecimal amount) {
        AlipayResponse resp = alipaySdk.tradeRefund(orderId, amount.toString());
        return new PayResult(resp.isSuccess(), resp.getMsg());
    }
}

// 4. 外观封装:下单全流程,业务代码只调这一个方法
@Component
public class CheckoutFacade {
    private final Map<String, PaymentAdapter> paymentAdapters; // Spring 自动注入
    private final InventoryService inventoryService;
    private final OrderRepository orderRepo;
    private final NotificationService notificationService;
    
    public CheckoutResult checkout(CheckoutRequest request) {
        // 校验库存
        if (!inventoryService.checkStock(request.getSkuId(), request.getQuantity())) {
            return CheckoutResult.fail("库存不足");
        }
        // 创建订单
        Order order = orderRepo.save(request.toOrder());
        // 支付(通过适配器统一接口)
        PaymentAdapter adapter = paymentAdapters.get(request.getChannel());
        PayResult payResult = adapter.pay(order.getId(), order.getTotalAmount());
        if (!payResult.isSuccess()) {
            orderRepo.markFailed(order.getId(), payResult.getMsg());
            return CheckoutResult.fail("支付失败: " + payResult.getMsg());
        }
        // 通知
        notificationService.sendOrderConfirmation(order.getUserId(), order.getId());
        return CheckoutResult.success(order.getId());
    }
}

业务代码永远只调 CheckoutFacade.checkout(),里面适配器做渠道差异转换,外观做流程编排。新增渠道只需要写一个适配器类注入 Spring,不动既有代码。

常见误区与小结

  • 适配器只在"接口不兼容"时用,不要因为"我想换个返回值"就上适配器,那是包装器,不是适配器
  • 外观不是"把所有方法再暴露一遍",外观是"封装一个常用场景流程",不是把子系统的方法原样转发
  • 桥接和策略长得像,但意图不同:策略是"算法可替换",桥接是"抽象和实现均可独立变化"
  • 适配器 vs 装饰器结构几乎一样,但适配器解决兼容问题,装饰器叠加功能——看意图,不看代码结构
  • 外观层不要写业务逻辑,只做编排和路由。业务逻辑写在子系统中,外观只是"按顺序调用",否则外观会变成上帝类

小结:适配器、外观、桥接是三种最常见的"包一层"结构型模式,各自解决不同的问题。适配器让不兼容的代码能一起工作,外观降低复杂子系统的使用成本,桥接让两个变化维度解耦。工程里它们常常组合出现——SLF4J 既是门面(外观)又靠适配器接实际日志框架。理解"它解决什么问题"比记住类图重要得多。

下一篇进入状态机和迭代器、享元等实用模式,把更多戈夫 23 种里常用的收尾。

参考

参考:GoF《设计模式》第 4 章结构型模式、《Effective Java》第 3 版第 1 条用静态工厂方法替代构造器、Spring MVC HandlerAdapter 源码

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1