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

一晚上的补丁,半年的技术债
电商支付团队为了赶“新渠道 + 限频风控”上线,做了几件看似合理但注定翻车的事:
- 新增
AlipayWithLimitPayService、WxWithLimitPayService等派生类; - 业务直接依赖三方 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 适配器,用在不同“火场”

桥接(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,让它同时实现ApplicationContextAware和InitializingBean接口。
setApplicationContext方法先被调用,让工厂获得 ApplicationContext 的引用。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);
}
}
“罪状”分析:
- 高度耦合,无法替换:我们的核心业务逻辑,直接依赖于AliyunSmsClient这个具体实现。现在要换成TencentSmsClient,意味着我们需要找到所有调用点,一个一个地修改。
- “方言”污染:aliyunClient.sendVerificationCode()和tencentClient.sendSms(),它们的方法名、参数、异常类型都可能完全不同。这种来自外部系统的“方言”,直接污染了我们的核心领域代码。
- 维护灾难:这是一场“代码瘟疫”。更换供应商,无异于一场全系统范围的“外科手术”,风险极高。
要治愈这场瘟疫,我们需要建立一道“防火墙”,将外部世界的混乱与我们内部系统的稳定隔离开。这道墙,就是适配器模式,在领域驱动设计(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相关的“脏活累活”:方法签名转换、请求参数适配、响应结果统一。未来如果需要接入第三家服务商,只需新增一个适配器即可,对现有业务代码零侵入。
四、实战对比:到底什么时候用桥接,什么时候用适配器?

| 决策问题 | 选桥接(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 编程,而具体的驱动可以在运行时动态加载和切换。这使得**“数据库操作的抽象”与“具体数据库的实现”**得以分离,可以独立变化。
这些场景请慎用或寻找替代方案
- 过度抽象:如果一个类只有一个变化维度,强行使用桥接模式会引入不必要的复杂性。此时,策略模式通常是更好的选择。
- 适配器变成“万能胶”:不要在适配器里堆积过多的业务逻辑。适配器的职责是“转换”,而不是“创造”。复杂的业务逻辑应该属于领域服务。
- 混淆桥接与策略:桥接处理的是结构上的多维度变化,而策略处理的是算法或行为上的单维度替换。如果你的维度之间没有本质区别,只是算法不同,用策略模式。
用桥接拉平维度,用适配器守住边界
- 识别维度是第一步:面对复杂性,先问自己:“这里有几个独立变化的方向?” 两个及以上,考虑桥接;一个,且是为兼容外部,考虑适配器。
- 适配器是你的“反腐层”:任何对外部系统的调用,都应该隔着一层适配器。它负责翻译数据、统一异常、甚至可以集成熔断和重试。
- 桥接是应对“组合爆炸”的银弹:它将继承的“树状结构”拍平为“网状组合”,让系统扩展性和可维护性呈线性增长,而非指数级。
- 设计模式不是非黑即白的,是权衡的艺术:永远根据问题的真实上下文来选择最合适的工具,避免为了模式而模式。
好的架构,总是在变化的方向上建立稳定的边界,在组合的地方提供灵活的桥梁。
179

被折叠的 条评论
为什么被折叠?



