从 if-else 到优雅解耦:策略 / 适配器 / 组合模式全流程实战

阅读提示:很多同学写代码看到 Map + Context 路由就全部当成策略模式,踩了大量坑。本文一次性讲清三者边界:纯策略、纯适配器、适配器+策略组合,全部配套 Spring 实战代码、场景区分、踩坑总结。

前言

业务开发里,多分支场景是家常便饭:多渠道上报、多文件存储、多种AI对话处理器、多种单据解析。

大家为了消灭 if-else,很容易写出长得几乎一模一样的代码:统一接口 + 多个实现 + Spring Map自动注入 + Context调度。

但是这里藏着一个高频面试+代码评审大坑:

  • ✅ 策略模式:实现类原生遵守统一契约,内部是不同算法
  • ✅ 适配器模式:实现类包装异构第三方/老组件,做接口翻译转换
  • ✅ 适配器+策略组合:先用适配器抹平异构SDK,产出统一接口;再用策略做可替换的路由选择规则

很多博客只单独讲其中一种,没有横向对比。本文三套代码一脉相承,业务域统一用【指标上报】,方便对照阅读。

一句话速记评审口诀:

看实现类内部:自己写算法 → 策略;包了别的异构对象做翻译 → 适配器;先翻译归一、再做多套路由选择算法 → 适配器+策略组合。


一、策略模式实战(模板方法增强版,你项目里 TaskSubmitStrategy / ChatStrategy 同类方案)

核心定义

策略模式(行为型):定义一套统一契约,多个实现类本身就是该能力的原生实现,内部是平等、可互相替换的不同业务算法;上下文负责路由选择,目的是运行时切换算法,消除大量if分支。

重点:策略实现类不包装任何外部不兼容的SDK,算法本体就在策略类内部。

适用场景

AI意图处理器、任务提交(同步/异步/延迟)、报表生成、标签查询,分支持续新增,公共逻辑多,适合搭配模板方法下沉通用逻辑。

完整代码

复制代码
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.Map;

// ===================== 1.抽象策略基类:策略 + 模板方法混合 =====================
public abstract class AbstractMetricStrategy {
    /** 策略唯一key,和@Component beanName对齐 */
    public abstract String getStrategyKey();

    /** 差异化核心逻辑,子类重写 */
    public abstract void push(String metricKey, Map<String, Object> data);

    // 模板方法:公共逻辑,所有策略默认复用
    public void preCheck(Map<String, Object> data) {
        if(data == null || data.isEmpty()){
            throw new IllegalArgumentException("指标数据不能为空");
        }
    }

    public void recordLog(String metricKey, Map<String, Object> data) {
        System.out.println("[策略公共日志] metricKey=" + metricKey);
    }
}

// ===================== 2.具体策略实现(原生实现,无外部Adaptee包装) =====================
@Component("sync")
public class SyncMetricStrategy extends AbstractMetricStrategy {
    @Override
    public String getStrategyKey() {
        return "sync";
    }

    @Override
    public void push(String metricKey, Map<String, Object> data) {
        preCheck(data);
        // 算法本体:同步直接本地打印上报
        System.out.println("[同步策略] 直接上报指标:" + metricKey + ",数据:" + data);
        recordLog(metricKey, data);
    }
}

@Component("async")
public class AsyncMetricStrategy extends AbstractMetricStrategy {
    @Override
    public String getStrategyKey() {
        return "async";
    }

    @Override
    public void push(String metricKey, Map<String, Object> data) {
        preCheck(data);
        // 算法本体:丢入MQ异步
        System.out.println("[异步策略] 投递MQ,指标:" + metricKey);
        recordLog(metricKey, data);
    }
}

@Component("defaultMetric")
public class DefaultMetricStrategy extends AbstractMetricStrategy {
    @Override
    public String getStrategyKey() {
        return "defaultMetric";
    }

    @Override
    public void push(String metricKey, Map<String, Object> data) {
        preCheck(data);
        System.out.println("[兜底策略] 默认日志落盘上报");
        recordLog(metricKey, data);
    }
}

// ===================== 3.上下文调度器 Context =====================
@Service
public class MetricStrategyContext {
    private final Map<String, AbstractMetricStrategy> strategyMap;

    public MetricStrategyContext(Map<String, AbstractMetricStrategy> strategyMap) {
        this.strategyMap = strategyMap;
    }

    public void pushByStrategy(String strategyKey, String metricKey, Map<String, Object> data){
        AbstractMetricStrategy strategy = strategyMap.getOrDefault(strategyKey, strategyMap.get("defaultMetric"));
        strategy.push(metricKey, data);
    }
}

调用示例

复制代码
@RestController
@RequestMapping("/metric/strategy")
public class MetricStrategyController {
    private final MetricStrategyContext metricStrategyContext;
    public MetricStrategyController(MetricStrategyContext metricStrategyContext) {
        this.metricStrategyContext = metricStrategyContext;
    }

    @GetMapping("/push")
    public String push(@RequestParam String strategyKey){
        metricStrategyContext.pushByStrategy(strategyKey, "user_login", Map.of("userId",10001));
        return "ok";
    }
}

✅ 特点总结

  1. 每个策略类本身就是算法主体,没有包装第三方异构SDK;
  2. 抽象基类承载模板逻辑,子类只写差异化;
  3. 新增上报方式,只新增策略类,调度代码不动,开闭原则;
  4. Spring Map注入只是路由工具,工具本身≠策略模式。

二、适配器模式实战(对象适配器,对标Spring HandlerAdapter,抹平异构SDK)

核心定义

适配器模式(结构型):已有一批接口签名完全不统一、不能修改源码的第三方/历史遗留组件,新增一层包装(Adapter),对外统一暴露一套Target接口;目的是消除接口不兼容,而不是切换算法。

重点:Adapter内部一定持有一个Adaptee(被适配对象),在方法内部做参数翻译、签名转换、异常包装。

Java工程几乎只用【对象适配器(组合)】,类适配器受单继承限制基本废弃。

适用场景

对接不同厂商SDK、不同埋点组件、多种存储客户端(OSS/乐享/销售易),也就是你代码库里 DifyClient、AliOssFileHandler 这类。

完整代码

复制代码
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.Map;

// ===================== 1. Target 统一目标接口(对外标准契约) =====================
public interface MetricAdapter {
    void push(String metricKey, Map<String, Object> data);
}

// ===================== 2. Adaptee:三方/历史组件,源码不可修改,签名五花八门 =====================
// 老埋点组件
public class OldTrackService {
    public void reportEvent(String eventCode, Map<String,Object> params) {
        System.out.println("[老埋点SDK原始方法] eventCode:" + eventCode + ", params:" + params);
    }
}
// 日志采集组件
public class LogCollector {
    public void collect(String type, Object data) {
        System.out.println("[日志采集SDK原始方法] type:" + type);
    }
}
// 云监控SDK
public class CloudMonitorSdk {
    public void send(String metricName, Object value, long timestamp) {
        System.out.println("[云监控SDK原始方法] metricName:" + metricName + ", ts:" + timestamp);
    }
}

// ===================== 3. Adapter:适配器,包装Adaptee,翻译签名 =====================
@Component("track")
public class TrackMetricAdapter implements MetricAdapter {
    private final OldTrackService oldTrackService;
    public TrackMetricAdapter(OldTrackService oldTrackService) {
        this.oldTrackService = oldTrackService;
    }
    @Override
    public void push(String metricKey, Map<String, Object> data) {
        // 适配翻译:统一接口 → 调用异构SDK的不同方法
        oldTrackService.reportEvent(metricKey, data);
    }
}

@Component("log")
public class LogMetricAdapter implements MetricAdapter {
    private final LogCollector logCollector;
    public LogMetricAdapter(LogCollector logCollector) {
        this.logCollector = logCollector;
    }
    @Override
    public void push(String metricKey, Map<String, Object> data) {
        logCollector.collect(metricKey, data);
    }
}

@Component("cloud")
public class CloudMetricAdapter implements MetricAdapter {
    private final CloudMonitorSdk cloudMonitorSdk;
    public CloudMetricAdapter(CloudMonitorSdk cloudMonitorSdk) {
        this.cloudMonitorSdk = cloudMonitorSdk;
    }
    @Override
    public void push(String metricKey, Map<String, Object> data) {
        // 适配翻译:补齐SDK需要的时间戳参数
        cloudMonitorSdk.send(metricKey, data, System.currentTimeMillis());
    }
}

// ===================== 4. 调度上下文(简易查表,只是选择壳子) =====================
@Service
public class MetricAdapterContext {
    private final Map<String, MetricAdapter> adapterMap;
    public MetricAdapterContext(Map<String, MetricAdapter> adapterMap) {
        this.adapterMap = adapterMap;
    }
    public void pushByAdapter(String adapterKey, String metricKey, Map<String,Object> data){
        MetricAdapter adapter = adapterMap.get(adapterKey);
        if(adapter == null){
            throw new IllegalArgumentException("不支持的上报渠道");
        }
        adapter.push(metricKey, data);
    }
}

调用示例

复制代码
@RestController
@RequestMapping("/metric/adapter")
public class MetricAdapterController {
    private final MetricAdapterContext adapterContext;
    public MetricAdapterController(MetricAdapterContext adapterContext) {
        this.adapterContext = adapterContext;
    }
    @GetMapping("/push")
    public String push(@RequestParam String adapterKey){
        adapterContext.pushByAdapter(adapterKey, "user_login", Map.of("userId",10001));
        return "ok";
    }
}

✅ 特点总结

  1. Adapter内部持有异构Adaptee,核心动作是参数/签名翻译;
  2. 底层SDK不能修改,通过包装层统一对外;
  3. 长得和策略的Map路由一模一样!区分点只在:实现内部有没有包装外部组件;
  4. Spring经典对标:HandlerAdapter,DispatcherServlet遍历supports匹配handler。

❗重点误区:这个Context查表逻辑只是调度壳子,不是策略模式!壳子通用,策略和适配器都能用。


三、适配器 + 策略模式 组合方案(业务高阶场景)

核心定义

两层职责分离:

  1. 底层:适配器层:抹平各个异构第三方SDK,全部翻译成一套统一接口(解决接口不兼容)
  2. 上层:策略层 :针对已经归一化后的接口,提供多套可替换的路由选择规则(解决"按什么规则选渠道")

这才是真正的组合模式:适配器负责【归一化能力】,策略负责【归一之后的选择算法】。

区别于前面两种:前面两种只有一层;这里是两层独立抽象。

适用场景

多存储渠道、多上报渠道,不仅SDK接口不一样,同时还要动态切换路由规则:按成本优先、按地域优先、按容量优先选择渠道。

完整代码

复制代码
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.Map;

// ========== 第一层:适配器层,和第二节适配器代码一致,产出统一接口 MetricChannel ==========
public interface MetricChannel {
    void push(String metricKey, Map<String, Object> data);
}
// 被适配的异构SDK
class OldTrackService {
    public void reportEvent(String eventCode, Map<String,Object> params) {}
}
class CloudMonitorSdk {
    public void send(String metricName, Object value, long ts) {}
}
// 适配器
@Component("trackChannel")
public class TrackChannelAdapter implements MetricChannel {
    private final OldTrackService oldTrackService;
    public TrackChannelAdapter(OldTrackService oldTrackService) {
        this.oldTrackService = oldTrackService;
    }
    @Override
    public void push(String metricKey, Map<String, Object> data) {
        oldTrackService.reportEvent(metricKey, data);
    }
}
@Component("cloudChannel")
public class CloudChannelAdapter implements MetricChannel {
    private final CloudMonitorSdk cloudMonitorSdk;
    public CloudChannelAdapter(CloudMonitorSdk cloudMonitorSdk) {
        this.cloudMonitorSdk = cloudMonitorSdk;
    }
    @Override
    public void push(String metricKey, Map<String, Object> data) {
        cloudMonitorSdk.send(metricKey, data, System.currentTimeMillis());
    }
}

// ========== 第二层:路由策略层!策略接口,定义【选择渠道的算法】 ==========
public interface ChannelRouteStrategy {
    /** 根据业务条件,选择一个归一后的渠道 */
    MetricChannel selectChannel(Map<String, Object> bizData, Map<String, MetricChannel> channelMap);
}

// 策略1:成本优先策略
@Component("costFirst")
public class CostFirstRouteStrategy implements ChannelRouteStrategy {
    @Override
    public MetricChannel selectChannel(Map<String, Object> bizData, Map<String, MetricChannel> channelMap) {
        // 模拟规则:云监控成本低,优先走cloudChannel
        return channelMap.get("cloudChannel");
    }
}

// 策略2:地域优先策略
@Component("regionFirst")
public class RegionFirstRouteStrategy implements ChannelRouteStrategy {
    @Override
    public MetricChannel selectChannel(Map<String, Object> bizData, Map<String, MetricChannel> channelMap) {
        // 模拟规则:国内用户走老埋点
        return channelMap.get("trackChannel");
    }
}

// ========== 顶层上下文:持有渠道集合 + 路由策略,两层调度 ==========
@Service
public class MetricRouteContext {
    // 所有已经被适配器归一后的渠道
    private final Map<String, MetricChannel> channelMap;
    // 多套可切换的路由策略
    private final Map<String, ChannelRouteStrategy> routeStrategyMap;

    public MetricRouteContext(Map<String, MetricChannel> channelMap,
                              Map<String, ChannelRouteStrategy> routeStrategyMap) {
        this.channelMap = channelMap;
        this.routeStrategyMap = routeStrategyMap;
    }

    public void push(String routeStrategyKey, String metricKey, Map<String,Object> bizData){
        ChannelRouteStrategy routeStrategy = routeStrategyMap.get(routeStrategyKey);
        if(routeStrategy == null){
            throw new IllegalArgumentException("路由策略不存在");
        }
        // 策略层:使用选择算法选出渠道
        MetricChannel channel = routeStrategy.selectChannel(bizData, channelMap);
        // 适配器归一后的统一调用
        channel.push(metricKey, bizData);
    }
}

调用示例

复制代码
@RestController
@RequestMapping("/metric/combine")
public class MetricCombineController {
    private final MetricRouteContext routeContext;
    public MetricCombineController(MetricRouteContext routeContext) {
        this.routeContext = routeContext;
    }
    // 传入路由策略key:costFirst / regionFirst
    @GetMapping("/push")
    public String push(@RequestParam String routeStrategyKey){
        routeContext.push(routeStrategyKey, "user_pay", Map.of("region","cn"));
        return "ok";
    }
}

✅ 特点总结

  1. 分层清晰:适配器负责【异构归一】,策略负责【选择规则替换】;
  2. 渠道新增 → 新增适配器;路由规则新增 → 新增路由策略;两者独立迭代;
  3. 这才是严格意义上:适配器模式 + 策略模式组合使用。

四、三者横向对比表(评审直接复制)

对比维度 纯策略模式 纯适配器模式 适配器+策略组合
核心目的 替换不同业务算法,消灭if-else 抹平异构、不兼容的第三方接口 先归一SDK,再动态切换路由选择算法
实现类内部 自己就是算法主体,不包装外部对象 内部持有Adaptee,做签名/参数翻译 两层:适配器包装SDK;策略写选择规则
接口来源 实现类原生满足该接口 Adaptee原本不满足Target接口 适配器产出统一接口,策略依赖这个统一接口
典型问句 我要用哪一套处理逻辑? 这个第三方SDK接口对不上,怎么包装? 这么多渠道,按什么规则选?
项目对应 TaskSubmitStrategy、Chat意图处理器 DifyClient包装、OSS适配器、HandlerAdapter 多存储+成本/地域路由

五、高频踩坑总结(工程重点)

  1. 不要看到 Map + Context 就判定是策略。Map注入只是实例查找工具,两个模式都可以复用这个壳子,判断标准看实现内部;
  2. 适配器不要写复杂业务逻辑,职责只做转换、异常包装、参数映射;业务判断放到上层;
  3. 组合模式不要写混层:不要把路由选择逻辑写进Adapter,Adapter只负责适配;
  4. 策略基类不要无限膨胀:模板方法用久了容易变成上帝类,建议拆细分接口、细分抽象类;
  5. Key对齐坑:@Component("xxx")、配置表key、getKey返回值三处不一致,容易静默降级,建议项目启动自检。

六、文末总结

  • 策略:算法可替换,生来同契约
  • 适配器:接口不兼容,加层做翻译
  • 适配器+策略:先翻译归一,再选路由规则

这一套就是我们业务系统最常用的分支解耦方案,看懂这套,以后代码评审、面试遇到这类代码,一眼就能分清。

相关推荐
是2的10次方啊9 小时前
设计模式与生活⑰:空腹体检,单子领着人走
设计模式
是2的10次方啊1 天前
设计模式与生活⑯:天冷了,出门这身得一层层配
设计模式
心若自由何处是束缚2 天前
适配器模式:从原理到实战的完整指南
设计模式·适配器模式
是2的10次方啊2 天前
设计模式与生活⑮:看到哪儿了,得有个地方替你记着
设计模式
2601_967979123 天前
设计模式 - 访问者模式
设计模式·访问者模式·应用场景·对象结构·操作分离
是2的10次方啊3 天前
设计模式与生活⑭:一栋楼,共用一份热
设计模式
是2的10次方啊4 天前
设计模式与生活⑬:你坐几号线,谁来运营是另一回事
设计模式
Tt有了t5 天前
设计模式速查
java·python·设计模式
一条Go5 天前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构