真实业务里,桥接比继承强在哪里?适配器又如何护住“反腐层”

双十一夜里,支付系统为适配“新通道+新风控”临时加了几组子类,结果回归漏了边界条件,线上大面积超时。问题根源:把多维变化写成继承金字塔,又让核心业务直接依赖外部 SDK。本文以桥接拉平特征维度,以适配器隔离外部异味,并给出 Spring/Netty/MyBatis/Dubbo 等源码佐证企业级代码模板,帮你在扩展性与稳定性之间,到位落地。

在这里插入图片描述

一晚上的补丁,半年的技术债

电商支付团队为了赶“新渠道 + 限频风控”上线,做了几件看似合理但注定翻车的事:

  • 新增 AlipayWithLimitPayServiceWxWithLimitPayService 等派生类;
  • 业务直接依赖三方 SDK;
  • 规则切换靠 if-else;

结果:新通道回归遗漏、风控组合不可测、回滚困难。坏味道很清晰:维度耦合、继承滥用、对外部系统无“反腐层”

子类爆炸问题

继承 if-else 的“子类爆炸”

// 反例:继承叠加维度(通道×风控),组合越多,类越多
abstract class BasePayService {
    abstract PayResult pay(PayReq req);
}

class AlipayWithNoRiskService extends BasePayService {
    @Override
    PayResult pay(PayReq req) {
        // 支付宝裸奔支付逻辑...
        System.out.println("使用支付宝支付,无风控");
        return PayResult.ok("ALI_" + System.currentTimeMillis());
    }
}

class AlipayWithLimitService extends BasePayService {
    @Override
    PayResult pay(PayReq req) {
        // 限频风控逻辑
        if (req.amountInCents > 10000) {
            System.out.println("支付宝支付,金额超限");
            return PayResult.fail("AMOUNT_LIMIT");
        }
        // 支付宝支付逻辑...
        System.out.println("使用支付宝支付,带限额风控");
        return PayResult.ok("ALI_" + System.currentTimeMillis());
    }
}

class WxWithNoRiskService extends BasePayService {
    @Override
    PayResult pay(PayReq req) {
        // 微信裸奔支付逻辑...
        System.out.println("使用微信支付,无风控");
        return PayResult.ok("WX_" + System.currentTimeMillis());
    }
}

class WxWithLimitService extends BasePayService {
    @Override
    PayResult pay(PayReq req) {
        // 限频风控逻辑
        if (req.amountInCents > 10000) {
            System.out.println("微信支付,金额超限");
            return PayResult.fail("AMOUNT_LIMIT");
        }
        // 微信支付逻辑...
        System.out.println("使用微信支付,带限额风控");
        return PayResult.ok("WX_" + System.currentTimeMillis());
    }
}

// 调用侧还需要写 if-else 选择具体实现...

病因分析:这种结构违反了单一职责原则开闭原则。类的职责过多(既管支付通道,又管风控策略),导致任何一个维度的变化都可能需要修改多个类。新增任意一维(如增加一个“银联通道”或一种“黑名单风控”)都要改 N 个类,回归范围指数级膨胀;且业务直接依赖三方 SDK 的异常、错误码和重试语义,极为脆弱。

桥接 VS 适配器,用在不同“火场”

桥接vs适配器模式

桥接(Bridge):造车,还是配发动机?

什么是桥接:将抽象部分与它的实现部分分离,使它们都可以独立地变化

一个生动的比喻:想象一下你是一家汽车制造商。你的产品线有多种车型(轿车、SUV、跑车),这是你的**“抽象”。同时,你又有多种发动机技术(汽油、电动、混合动力),这是你的“实现”**。

如果用继承,你可能得设计出 汽油版轿车电动版轿车汽油版SUV… 新增一种车型或一种发动机,都会导致子类数量爆炸。

桥接模式就像是制定了一套标准化的发动机接口。你的车型设计(抽象)只关心“如何安装和使用发动机”,而不关心这台发动机内部是烧油还是用电。这样,车型发动机这两个维度就可以独立发展了。你可以随时给任何一款新车型,装上任何一款新研发的发动机。这就是“桥接”——在“车型”和“发动机”之间架起一座灵活组合的桥梁。

适配器(Adapter):万能充电头与同声传译

什么是适配器:将一个类的接口转换成客户希望的另外一个接口,让不兼容的接口能够协同工作。

一个生动的比喻:你的系统需要发送短信验证码。市面上有阿里云、腾讯云等多家服务商,但它们的SDK接口五花八门:方法名、参数列表、返回的数据结构全都不一样。如果你的业务代码直接依赖这些SDK,那么每次更换服务商,或者增加一个备用渠道,都将是一场灾难。

适配器模式就是你的统一短信网关。它帮你屏蔽了底层所有服务商的差异。

  • 你的内部接口 (Target): 这是你系统内部定义的、稳定且统一的短信发送标准。它非常简单,只关心“给谁发”和“发什么”。
  • 外部待适配的类 (Adaptee): 这就是阿里云、腾讯云各自的SDK。

桥接模式:拆分支付与风控维度

首先,我们用桥接模式解决“通道 × 风控”的维度爆炸问题。

1. 用自定义注解为“维度实现”打上标记

渠道和风控的类型枚举

// 渠道类型枚举
public enum PaymentChannelType {
    ALIPAY, WECHAT
}

// 风控类型枚举
public enum RiskPolicyType {
    NO_RISK, AMOUNT_LIMIT
}
// 渠道类型
@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChannelType {
    PaymentChannelType value();
}

// 风控类型
@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PolicyType {
    RiskPolicyType value();
}

2.识别并拆分独立变化的维度

先拆分渠道。

public interface PaymentChannel {
    
    /** 支付 */
    PayResult doPay(PayReq req);
}

@ChannelType(PaymentChannelType.ALIPAY)
public class AlipayChannel implements PaymentChannel {
    public PayResult doPay(PayReq req) {
        // 模拟调用支付宝SDK/HTTP接口
        System.out.println("调用支付宝接口支付: " + req.amountInCents / 100.0 + "元");
        return PayResult.ok("ALI_" + System.currentTimeMillis());
    }
}

@ChannelType(PaymentChannelType.WECHAT)
public class WechatPayChannel implements PaymentChannel {
    public PayResult doPay(PayReq req) {
        // 模拟调用微信支付SDK/HTTP接口
        System.out.println("调用微信支付接口支付: " + req.amountInCents / 100.0 + "元");
        return PayResult.ok("WX_" + System.currentTimeMillis());
    }
}

再拆分风控策略

public interface RiskPolicy {

    /** 风控校验 */
    void check(PayReq req);
}

@PolicyType(RiskPolicyType.NO_RISK)
public class NoRiskPolicy implements RiskPolicy {
    public void check(PayReq req) {
        System.out.println("无风控策略,直接放行");
    }
}

@PolicyType(RiskPolicyType.AMOUNT_LIMIT)
public class AmountLimitPolicy implements RiskPolicy {
    public void check(PayReq req) {
        if (req.amountInCents > maxCents) {
            throw new RuntimeException("支付金额超过风控阈值: " + maxCents / 100.0 + "元");
        }
        System.out.println("执行金额限额风控,阈值: " + maxCents / 100.0 + "元");
    }
}

3.建立桥梁,连接抽象与实现

PaymentService 类就是这座桥梁。它不关心具体的渠道和风控逻辑,它只持有这两个维度的接口引用。它的职责是在运行时,将客户端指定的“渠道实现”和“风控实现”组合起来,完成一次完整的支付操作。

这种“组合优于继承”的方式,将“M x N”的实现爆炸问题,降维成了“M + N”的线性增长问题。

public abstract class PaymentService {

    protected PaymentChannel channel;
    protected RiskPolicy riskPolicy;

    public PaymentService(PaymentChannel channel, RiskPolicy riskPolicy) {
        this.channel = channel;
        this.riskPolicy = riskPolicy;
    }

    public abstract PayResult pay(PayReq req);
}

// 具体的支付服务实现
public class ConcretePaymentService extends PaymentService {

    public ConcretePaymentService(PaymentChannel channel, RiskPolicy riskPolicy) {
        super(channel, riskPolicy);
    }

    @Override
    public PayResult pay(PayReq req) {
        try {
            System.out.println("开始支付流程: " + describe());
            riskPolicy.check(req);
            return channel.doPay(req);
        } catch (Exception e) {
            System.err.println("支付失败: " + e.getMessage());
            return PayResult.fail(e.getMessage());
        }
    }
}

4.构建核心工厂,自动扫描并注册所有维度

这是最关键的一步。我们创建一个PaymentServiceFactory,让它同时实现ApplicationContextAwareInitializingBean接口。

  1. setApplicationContext方法先被调用,让工厂获得 ApplicationContext 的引用。
  2. afterPropertiesSet 方法在所有 Bean 属性设置完毕后被调用,我们在这里使用已获得的 ApplicationContext 来主动扫描容器,完成初始化。
@Component
public class PaymentServiceFactory implements ApplicationContextAware, InitializingBean {

    private ApplicationContext applicationContext;
    private Map<PaymentChannelType, PaymentChannel> channelMap;
    private Map<RiskPolicyType, RiskPolicy> policyMap;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.channelMap = new EnumMap<>(PaymentChannelType.class);
        this.policyMap = new EnumMap<>(RiskPolicyType.class);

        // 主动从容器中获取所有被@ChannelType注解的Bean
        applicationContext.getBeansWithAnnotation(ChannelType.class).values()
            .forEach(channel -> {
                ChannelType type = channel.getClass().getAnnotation(ChannelType.class);
                channelMap.put(type.value(), (PaymentChannel) channel);
            });

        // 主动从容器中获取所有被@PolicyType注解的Bean
        applicationContext.getBeansWithAnnotation(PolicyType.class).values()
            .forEach(policy -> {
                PolicyType type = policy.getClass().getAnnotation(PolicyType.class);
                policyMap.put(type.value(), (RiskPolicy) policy);
            });
    }

    // 对外提供服务获取方法
    public PaymentService getService(PaymentChannelType channelType, RiskPolicyType policyType) {
        PaymentChannel channel = channelMap.get(channelType);
        RiskPolicy policy = policyMap.get(policyType);

        if (channel == null || policy == null) {
            throw new IllegalArgumentException("Unsupported payment combination: " + channelType + ", " + policyType);
        }

        // 桥接模式的核心:在运行时动态组合
        return new ConcretePaymentService(channel, policy);
    }
}

5.客户端动态组合,按需装配

现在,客户端代码变得极其简单。它不再关心如何创建和组合对象,只需从 Spring 容器中获取 PaymentServiceFactory,然后按需索取服务即可。

@Service
public class PaymentClient {

    @Autowired
    private PaymentServiceFactory paymentServiceFactory;

    public void doSomePayment() {
        // --- 场景一:获取支付宝+无风控的服务 ---
        PaymentService service1 = paymentServiceFactory.getService(PaymentChannelType.ALIPAY, RiskPolicyType.NO_RISK);
        service1.pay(new PayReq("order1", 5000, "CNY"));

        // --- 场景二:获取微信+限额风控的服务 ---
        PaymentService service2 = paymentServiceFactory.getService(PaymentChannelType.WECHAT, RiskPolicyType.AMOUNT_LIMIT);
        service2.pay(new PayReq("order2", 12000, "CNY"));
    }
}

桥接的收益:通过与 Spring IoC 结合,我们的桥接模式实现了完全的自动化装配。新增一个渠道或风控策略,只需增加一个带注解的实现类,无需修改任何工厂或调用方代码,真正做到了对扩展开放,对修改关闭。

适配器模式: 用统一短信网关隔离阿里云与腾讯云

那是一个普通的周一,CTO在早会上宣布:“出于成本考虑,我们的短信服务,本周内必须从‘阿里云’切换到‘腾讯云’!”

会议室里一片寂静,只有项目经理的冷汗往下淌。因为他知道,我们的系统中,调用阿里云短信SDK的代码,像一群没有纪律的雇佣兵,散落在代码库的各个角落。

四处裸奔的AliyunSmsClient

来看下这段腐烂的代码

// 用户注册服务
@Service
public class UserRegistrationService {
    @Autowired
    private AliyunSmsClient aliyunClient; // 直接依赖阿里云SDK

    public void register(String phone, String code) {
        // ... 业务逻辑 ...
        aliyunClient.sendVerificationCode(phone, code); // 直接调用
    }
}

// 订单通知服务
@Service
public class OrderNotificationService {
    @Autowired
    private AliyunSmsClient aliyunClient; // 又一次直接依赖

    public void notifyShipment(Order order) {
        String message = "您的订单已发货...";
        aliyunClient.sendTransactionalMessage(order.getPhoneNumber(), message);
    }
}

“罪状”分析:

  1. 高度耦合,无法替换:我们的核心业务逻辑,直接依赖于AliyunSmsClient这个具体实现。现在要换成TencentSmsClient,意味着我们需要找到所有调用点,一个一个地修改。
  2. “方言”污染:aliyunClient.sendVerificationCode()和tencentClient.sendSms(),它们的方法名、参数、异常类型都可能完全不同。这种来自外部系统的“方言”,直接污染了我们的核心领域代码。
  3. 维护灾难:这是一场“代码瘟疫”。更换供应商,无异于一场全系统范围的“外科手术”,风险极高。

要治愈这场瘟疫,我们需要建立一道“防火墙”,将外部世界的混乱与我们内部系统的稳定隔离开。这道墙,就是适配器模式,在领域驱动设计(DDD)中,它被称为反腐层(Anti-Corruption Layer, ACL)。

1. 定义系统内部的顶层抽象——SmsProvider接口

public interface SmsProvider {
    boolean send(String phone, String message);
}

2.创建实现类——调用具体接口

// @Primary //把原来的注释掉,系统默认就不会使用aliyun
@Component("aliyunSmsProvider")
public class AliyunSmsAdapter implements SmsProvider {
    private final AliyunSmsClient aliyunClient = new AliyunSmsClient();

    @Override
    public boolean send(String phone, String message) {
        // “翻译”:调用阿里云SDK的真实方法
        return aliyunClient.sendTransactionalMessage(phone, message).isSuccess();
    }
}

@Primary //增加@Primary,系统默认使用这个bean
@Component("tencentSmsProvider")
public class TencentSmsAdapter implements SmsProvider {
    private final TencentSmsAdapter tencentClient = new TencentSmsAdapter();

    @Override
    public boolean send(String phone, String message) {
        // “翻译”:调用腾讯云SDK的真实方法
        return aliyunClient.sendTransactionalMessage(phone, message).isSuccess();
    }
}

业务代码直接调用

@Service
public class UserRegistrationService {

    @Autowired
    // @Qualifier("aliyunSmsProvider") // 如果需要使用aliyun的地方可以特殊制定使用哪个bean的实现
    private SmsProvider smsProvider;

    public void register(String phone, String code) {
        smsProvider.send(phone, "您的验证码是:" + code);
    }
}

适配器的价值:在这个例子中,我们的核心业务代码只需要依赖稳定、统一的 SmsProvider 接口,完全无需关心底层用的是阿里云还是腾讯云。AliyunSmsAdapter 和 TencentSmsAdapter 作为反腐层,完美地封装了所有与外部SDK相关的“脏活累活”:方法签名转换、请求参数适配、响应结果统一。未来如果需要接入第三家服务商,只需新增一个适配器即可,对现有业务代码零侵入。

四、实战对比:到底什么时候用桥接,什么时候用适配器?

桥接vs适配器使用场景

决策问题选桥接(Bridge)选适配器(Adapter)为什么?
问题本质是“组合爆炸”吗?桥接的核心目标就是解决多个独立变化维度的组合问题,如“M种渠道 × N种风控”。
问题本质是“接口不兼容”吗?适配器的核心目标是弥补接口差异,让原本无法合作的组件能协同工作,如接入多家SDK。
需要运行时“自由装配”维度吗?一般不涉及桥接通过组合,让你可以像搭积木一样在运行时动态创建对象(如从配置中心读取策略)。
目标是“隔离外部系统”吗?间接实现适配器是构建“反腐层”的天然选择,它在系统边界上进行翻译和保护。
代码长什么样?new Service(new DimensionAImpl(), new DimensionBImpl())new MyApiAdapter(new ThirdPartyApi())桥接通常在构造时注入多个“实现”;适配器通常注入一个“被适配”的对象。

一句话总结判断依据

  • 当你发现一个类需要向两个或更多方向独立扩展时,请立刻使用桥接模式
  • 当你需要集成一个接口不匹配的外部或遗留系统时,请果断使用适配器模式

一个常见的错误:试图用桥接模式去接入不同厂商的SDK。虽然也能实现,但属于“杀鸡用牛刀”。因为“厂商”通常只是一个维度,用适配器模式定义统一接口,然后为每个厂商写一个Adapter,是更清晰、更直接的做法。

反之,如果每个厂商的SDK还需要支持多种加密算法,这就变成了“厂商 × 加密算法”两个维度,此时桥接模式就派上用场了。

看看大师们在顶级框架中如何应用

理论是灰色的,生命之树常青。让我们看看顶级开源框架是如何在实战中运用这两个模式的。

适配器:Spring MVC 的 HandlerAdapter

  • 位置: org.springframework.web.servlet.HandlerAdapter
  • 场景: DispatcherServlet 作为前端控制器,需要调用各种类型的处理器(@Controller方法、HttpRequestHandler等)来处理请求。但它不想知道每种处理器的具体调用方式。
  • 巧思: HandlerAdapter 应运而生。每种处理器都有一个对应的适配器(如 RequestMappingHandlerAdapter)。DispatcherServlet 只需找到合适的适配器,然后调用其统一的 handle 方法即可。这完美地将**“变化的处理器类型”“不变的调度逻辑”**解耦。

适配器:MyBatis 的 TypeHandler

  • 位置: org.apache.ibatis.type.TypeHandler
  • 场景: 在 Java 对象的属性和数据库表的列之间进行类型转换。例如,将 Java 的 LocalDateTime 类型存入数据库的 TIMESTAMP 列。
  • 巧思: TypeHandler 负责将 JDBC 类型Java 类型 这两个不兼容的“接口”进行适配。MyBatis 为大多数常见类型提供了内置的 TypeHandler,同时也允许你自定义,以适配特殊的类型映射需求。

桥接模式的变体:JDBC 驱动

  • 场景: Java 应用程序需要与各种不同的数据库(MySQL, Oracle, PostgreSQL等)进行通信。
  • 巧思: JDBC API 本身可以看作是桥接模式中的“抽象”部分。它定义了一套标准的数据库操作接口(Connection, Statement, ResultSet)。而各个数据库厂商提供的驱动(Driver),则是**“实现”部分**。应用程序面向 JDBC API 编程,而具体的驱动可以在运行时动态加载和切换。这使得**“数据库操作的抽象”“具体数据库的实现”**得以分离,可以独立变化。

这些场景请慎用或寻找替代方案

  • 过度抽象:如果一个类只有一个变化维度,强行使用桥接模式会引入不必要的复杂性。此时,策略模式通常是更好的选择。
  • 适配器变成“万能胶”:不要在适配器里堆积过多的业务逻辑。适配器的职责是“转换”,而不是“创造”。复杂的业务逻辑应该属于领域服务。
  • 混淆桥接与策略:桥接处理的是结构上的多维度变化,而策略处理的是算法或行为上的单维度替换。如果你的维度之间没有本质区别,只是算法不同,用策略模式。

用桥接拉平维度,用适配器守住边界

  1. 识别维度是第一步:面对复杂性,先问自己:“这里有几个独立变化的方向?” 两个及以上,考虑桥接;一个,且是为兼容外部,考虑适配器。
  2. 适配器是你的“反腐层”:任何对外部系统的调用,都应该隔着一层适配器。它负责翻译数据、统一异常、甚至可以集成熔断和重试。
  3. 桥接是应对“组合爆炸”的银弹:它将继承的“树状结构”拍平为“网状组合”,让系统扩展性和可维护性呈线性增长,而非指数级。
  4. 设计模式不是非黑即白的,是权衡的艺术:永远根据问题的真实上下文来选择最合适的工具,避免为了模式而模式。

好的架构,总是在变化的方向上建立稳定的边界,在组合的地方提供灵活的桥梁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CV大魔王

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值