如果只按类图去认设计模式策略模式和适配器模式确实容易让人懵圈都是接口、都有实现类、类里面都藏着一个对象引用。我见过不少人在代码评审里为“这个类到底算策略还是适配器”争得面红耳赤结果发现其实是一个适配器里套了一个策略两种模式串在一起用。这篇就是想把这个事讲清楚策略模式和适配器模式各自解决什么问题在真实项目里长什么样为什么它们总和“接口、解耦、可扩展”这些词绑在一起以及我自己在实际重构中踩过的坑和总结出的判断标准。先说最核心的一句策略模式管的是“算法的可替换”适配器模式管的是“接口的兼容”。一个是行为层面的运行时切换一个是结构层面的接口翻译。下面拆开聊。1. 为什么这两个模式总被放在同一篇文章里聊设计模式这个圈子有个奇怪的现象策略模式和适配器模式很少单独出现在讨论里但一旦出现基本就是成对出现。这背后有两个原因一个是表层的形式相似另一个是深层的职责纠缠。1.1 类图相似造成的经典混淆如果只看简化类图这两个模式确实长得像。策略模式里Context 持有 Strategy 接口具体策略类实现接口适配器模式里Client 持有 Target 接口Adapter 实现 Target 并持有 Adaptee。两张图都是“一个类实现接口接口又关联到另一个类”UML 形状几乎重叠。但类图只是静态结构设计模式的灵魂不在结构而在意图。这也是为什么很多从 UML 入手学设计模式的人最后都卡在“结构都懂代码写不对”这个阶段。我自己的判断口诀很简单问“能不能换一种做法继续跑”——这是策略模式。问“能不能让两边接上继续跑”——这是适配器模式。一个是换法一个是接线方向上完全不同。1.2 行为型与结构型的本质区别从 GoF 的分类体系看策略模式属于行为型模式适配器模式属于结构型模式。这个分类不是学术名词抠概念而是直接决定代码怎么组织。行为型模式关注的是“行为怎么分配、算法怎么封装、对象之间怎么协作”。策略模式把所有可替换的算法收编到同一个接口下让算法的变更不影响到使用算法的上下文。它的变化维度是算法族。结构型模式关注的是“类与类之间怎么组合、怎么形成更大的结构”。适配器模式的全部意义在于兼容把一个已有接口转换为客户端期望的接口它的变化维度是接口形态。这两个模式的组合场景也因此在真实项目里非常常见先用适配器把各个外部系统的接口统一成一种形态再用策略模式在统一后的接口上做算法切换。前者解决外部差异后者解决内部选择各管一段。2. 策略模式把一坨 if-else 收编成规格统一的可插拔组件策略模式在实战中出现频率极高但大多数人接触它是从一段越来越长的 if-else 开始的。2.1 场景信号什么时候该怀疑自己在写策略模式拿一个最常见的例子说电商订单的运费计算。业务方最初只要求按订单金额满 99 包邮代码写起来很直接金额够了就免运费不够就收 12 元。后来运营加需求会员等级不同运费规则不同再后来增加重货按重量计费再后来跨境订单按固定费率。于是你看到了这样的代码if (order.isVip() order.getAmount() 99) { return 0; } else if (order.getWeight() 20) { return order.getWeight() * 1.5; } else if (order.isOverseas()) { return order.getAmount() * 0.08; } else { return 12.0; }这段代码的问题不是丑而是新增一个规则就要改这个函数线上风险跟着累加。更麻烦的是测试一个方法里五六条分支每条分支的入参还得互相避让造数据越造越痛苦。这时候策略模式该上场了。它做的事情很朴素把“运费怎么算”抽成一个接口每种规则一个实现类计算方只依赖接口不关心具体规则。public interface ShippingFeeStrategy { double calculate(Order order); }public class VipFreeShippingStrategy implements ShippingFeeStrategy { Override public double calculate(Order order) { return order.getAmount() 99 ? 0 : 12.0; } } public class HeavyGoodsStrategy implements ShippingFeeStrategy { Override public double calculate(Order order) { return order.getWeight() 20 ? order.getWeight() * 1.5 : 12.0; } } public class OverseasStrategy implements ShippingFeeStrategy { Override public double calculate(Order order) { return order.getAmount() * 0.08; } }调用方持有策略接口构造时注入运行时要切换就提供一个 setterpublic class OrderContext { private ShippingFeeStrategy strategy; public OrderContext(ShippingFeeStrategy strategy) { this.strategy strategy; } public void setStrategy(ShippingFeeStrategy strategy) { this.strategy strategy; } public double shippingFee(Order order) { return strategy.calculate(order); } }运行时代码就是OrderContext context new OrderContext(new VipFreeShippingStrategy()); double fee context.shippingFee(order); // 订单状态变化后切换策略 context.setStrategy(new HeavyGoodsStrategy());策略模式的本质是开闭原则的落地新增一种运费规则不用改已有的计算函数只新增一个策略实现类。运费规则的变化频率高但每种规则的内部逻辑相对独立正好是策略模式发挥优势的地方。2.2 java8 时代的简写函数式接口和 Lambda传统策略模式要写一堆策略实现类这也劝退了不少人。但在 Java 8 之后“标准策略模式”可以大幅简写这正是“java8标准策略模式”这个热词背后的价值。如果策略接口只有一个抽象方法它就是天然的函数式接口可以直接用 Lambda 表达式替代实现类FunctionalInterface public interface ShippingFeeStrategy { double calculate(Order order); }创建策略时不再需要新建类文件ShippingFeeStrategy vipFree order - order.getAmount() 99 ? 0 : 12.0; ShippingFeeStrategy heavy order - order.getWeight() 20 ? order.getWeight() * 1.5 : 12.0;如果某个策略本身就是现成的方法比如已经有一个FeeRuleService.calcVipFee(Order)还可以直接用法引用ShippingFeeStrategy vipFree feeRuleService::calcVipFee;还能用Map做策略注册表替代 if-else 的选路逻辑MapString, ShippingFeeStrategy strategyMap new HashMap(); strategyMap.put(VIP, vipFree); strategyMap.put(HEAVY, heavy); strategyMap.put(OVERSEAS, overseas); ShippingFeeStrategy strategy strategyMap.getOrDefault(order.getType(), defaultStrategy);到了这一步策略模式已经不需要满地的类文件核心变成“一个函数式接口 一组 Lambda/方法引用 一个策略注册表”。对于规则简单、无状态的策略这种简写风格非常干净也是我目前在新项目里最常用的写法。2.3 什么时候别用 Lambda 版的策略Lambda 简写虽然香但有前提策略内部不能有复杂状态和多步骤逻辑。比如跨境运费策略它不是一个纯函数而是依赖汇率服务、关税配置、第三方物流报价三个外部依赖内部还有缓存逻辑。这种策略如果硬塞进 Lambda代码会变成一坨不可读的长表达式没法命名、没法测试、没法复用。我的经验分界线策略内部只有一两行计算逻辑无状态直接用 Lambda。策略内部需要组装多个依赖有状态或者会被多处复用老老实实建策略类。另外还有一种折中策略类保留但策略的选择和注册用 Lambda Map。这样既保留策略类的可维护性又消除选择逻辑里的 if-else。3. 适配器模式两套接口谈不拢时缺的只是一个翻译适配器模式的定位和策略完全不同。策略模式面对的是“同一件事有不同做法”适配器面对的是**“两边的接口对不上但你又不该改任何一边”**。3.1 生活类比与模式定位用生活里最常见的例子说你从国内带了一个两脚插头的设备到国外那边的墙插是三孔的这个时候你需要的不是换设备也不是改造墙插而是买一个转换头。功能什么都没变只是让两边的物理形态能接上。适配器在代码里就是这个转换头。典型场景是项目里接入了第三方的 SDK第三方不可能按你的接口设计来开发你也没法为了一个 SDK 去改全公司的调用规范。这个时候适配器的任务就是实现你自己的目标接口内部调用第三方老的接口把参数翻译过去。// 你系统里的统一日志接口 public interface Logger { void info(String message); } // 遗留系统的文件日志实现 public class FileLogger { public void record(String line) { // 写入文件 } } // 适配器把 Logger.info 翻译成 FileLogger.record public class FileLoggerAdapter implements Logger { private final FileLogger fileLogger; public FileLoggerAdapter(FileLogger fileLogger) { this.fileLogger fileLogger; } Override public void info(String message) { fileLogger.record(message); } }这就是适配器的全部核心实现目标接口持有被适配者方法内部做转发和参数转换。3.2 对象适配器和类适配器的取舍适配器有两种实现方式很多初学者不太注意这个细节。对象适配器用组合适配器持有被适配者的引用。上面代码就是这种。类适配器用多继承适配器同时继承 Target 和 Adaptee。在 Java 里就是继承被适配类同时实现目标接口。类适配器的写法大致长这样public class FileLoggerAdapter extends FileLogger implements Logger { Override public void info(String message) { record(message); } }Java 里我基本不用类适配器只用一个理由组合优于继承。类适配器会占用唯一的继承位万一将来需要继承别的类就没路可走而且继承意味着暴露被适配者的内部方法适配器接口变得不干净。对象适配器没有这些问题构造函数传依赖进来测试时还可以轻松替换成 mock。3.3 真实需求多个上报 SDK 统一接入适配器在高频场景里往往不止适配一个对象而是适配一批外部依赖。我举个项目里非常典型的需求统一埋点上报。公司早期自研了一套上报接口业务代码直接调用它。后来老板说友盟也要接过一阵又说神策的数据分析要用。这三个 SDK 的调用方法完全不同自研接口是report.request(eventName, attrs)友盟是UmengAnalytics.onEvent(context, eventId, map)神策是sensors.track(eventName, properties)如果业务代码里出现三种调用并存那就是灾难现场每个页面要写三遍上报新增一个 SDK 要改所有页面。正确做法就是定义一个自己的上报目标接口让每个 SDK 各出一个适配器业务方只面对自己的接口。这个场景里的适配器职责非常纯粹把不同 SDK 的呼叫方式翻译成我院里的上报方法不让任何 SDK 的类型泄漏到业务层。4. 最容易混用的地方形式相同意图完全不同这是全篇最想强调的部分。很多项目代码里适配器和策略长得几乎一样都是“实现接口、持有对象、转发调用”但它们的意图南辕北辙。把两者混用短期看不出毛病后期扩展却会卡死。4.1 一张表分清两种模式的意图差异我把实践中的区分维度整理成一张表方便对照对比维度策略模式适配器模式所属分类行为型结构型核心意图算法族可替换隔离变化接口转换兼容存量关注点同一种行为的多种做法不同接口之间的对齐类关系特征Context 依赖 Strategy运行时切换Adapter 实现 Target同时持有 Adaptee方法内部通常是完整算法逻辑通常是转发调用加参数映射典型识别信号一个接口多个实现类上下文持有接口引用带 setStrategy实现自己的接口方法体内调别人的接口类名带 Adapter滥用典型形态只有两个分支也硬上策略接口接口本来自己能改却套一层适配器策略模式的代码最终回答的问题是“这个行为现在用哪种算法来执行”。适配器模式的代码最终回答的问题是“怎么把外部的调用形态翻译成我自己的调用形态”。你去看一段代码如果类的方法体是一串完整业务算法比如先算折扣、再算叠加、最后处理封顶这是策略。如果类的方法体只有xxx.forceTrack(...)、yyy.record(...)这种转发名字还带 Adapter这就是适配器。4.2 判别信号设计行为 vs 兼容存量我自己在实际看代码时会问三个问题这个接口是我设计的还是为了对接外部才引入的我关心的是“这个功能怎么做的”还是“这个接口怎么对上”将来我要新增的是一种行为还是一个外部系统的接入答案变量指向哪边模式就基本清楚了。还有一个更直观的判别信号看接口里有没有外部 SDK 的类型。适配器对接外部系统时目标接口里绝对不允许出现第三方 SDK 的字段否则适配器没做干净外部类型泄漏到业务层了。举一个典型错误有人在定义统一上报接口时方法签名写的是push(SensorsContext context)。这就等于把神策的上下文对象传进业务层业务层从此对神策产生编译期依赖。以后想换掉神策这个接口就是最大的障碍。正确的目标接口应该只包含自己的领域对象比如push(EventPayload payload)SDK 相关对象只允许在适配器内部出现。4.3 两者串联的正确姿势很多人在争论一个类到底是策略还是适配器时逻辑是“非此即彼”但真实项目里往往两个模式是串联出现的。串联的顺序有讲究先适配后策略。外部系统进来第一层是适配器把不同 SDK 的方法签名翻译成你自己的目标接口。这一层只做翻译不做业务决策。翻译之后你的业务层得到了一个统一的调用入口在这个入口之上如果要选择不同的上报时机、不同的重试方式、不同的批量合并算法那是策略模式的职责。写出来就是这个层次业务代码 ↓ 只依赖 Reporter上下文 ↓ 内部持有 ReportStrategy策略实时/批量/重试 ↓ 内部持有 ReportTarget统一目标接口 ↓ 各适配器 SensorsAPI / UmengAPI / 自研API适配器管外部差异策略管内部选择中间的ReportTarget是两个模式的分界。如果代码里出现了“适配器里写算法选择”或“策略实现里直接调第三方 SDK”那模式串味的信号就亮了。5. 实战案例埋点上报系统里的串联配合前面讲了不少理论这一章给一个完整的重构案例把这套思路落到代码上。5.1 重构前的混乱现场旧项目的埋点代码长这样业务类里直接调用后台接口也掺杂一两处已接入的友盟调用public class OrderService { public void paySuccess(Order order) { // 业务逻辑... // 埋点调用自研上报接口 ReportRequest request new ReportRequest(); request.setEventName(pay_success); request.setAttrs(buildAttrs(order)); ReportClient.getInstance().request(request); // 某些入口还要额外调友盟 UmengAnalytics.onEvent(context, pay_success, attrs); } }问题一眼就能看出来业务方法里混着两种上报调用新增一个埋点渠道要改所有埋点位置。而且上报策略是写死的——所有事件都实时上报线上的低谷期没必要实时高峰期又怕全堵在队列里。5.2 第一步用适配器抹平 SDK 接口差异先定义统一目标接口这是适配器的 Targetpublic interface ReportTarget { void push(EventPayload payload); }EventPayload是领域对象包含事件名、属性 Map、时间戳等。它的存在保证了 SDK 类型不泄漏到上层。然后为每个 SDK 写适配器public class SensorsAdapter implements ReportTarget { private final SensorsAPI sensors; public SensorsAdapter(SensorsAPI sensors) { this.sensors sensors; } Override public void push(EventPayload payload) { sensors.track(payload.getEventName(), payload.getAttributes()); } } public class UmengAdapter implements ReportTarget { private final Context context; public UmengAdapter(Context context) { this.context context; } Override public void push(EventPayload payload) { UmengAnalytics.onEvent(context, payload.getEventName(), payload.getAttributes()); } } public class SelfReportAdapter implements ReportTarget { Override public void push(EventPayload payload) { ReportRequest request new ReportRequest(); request.setEventName(payload.getEventName()); request.setAttrs(payload.getAttributes()); ReportClient.getInstance().request(request); } }这一步完成之后业务层面对的就只有ReportTarget.push(EventPayload)三个 SDK 的差异被完全封在了适配器内部。5.3 第二步用策略模式替换上报行为目标接口统一了接下来处理另一个维度上报行为。同一个事件高峰期需要批量合并重要事件需要实时上报失败了还要重试。这就是三种上报策略。定义策略接口FunctionalInterface public interface ReportStrategy { void report(ReportTarget target, EventPayload payload); }三种策略实现public class RealTimeStrategy implements ReportStrategy { Override public void report(ReportTarget target, EventPayload payload) { target.push(payload); } } public class BatchStrategy implements ReportStrategy { private final QueueEventPayload queue new LinkedList(); private final ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); public BatchStrategy() { scheduler.scheduleAtFixedRate(this::flush, 10, 10, TimeUnit.SECONDS); } private void flush() { EventPayload payload; while ((payload queue.poll()) ! null) { target.push(payload); } } Override public void report(ReportTarget target, EventPayload payload) { queue.offer(payload); } } public class RetryStrategy implements ReportStrategy { private static final int MAX_RETRY 3; Override public void report(ReportTarget target, EventPayload payload) { for (int i 0; i MAX_RETRY; i) { try { target.push(payload); return; } catch (Exception ex) { sleepWithBackoff(i); } } } }5.4 第三步Context 组装与测试策略需要一个上下文把目标和策略组合起来public class Reporter { private final ReportTarget target; private ReportStrategy strategy; public Reporter(ReportTarget target, ReportStrategy strategy) { this.target target; this.strategy strategy; } public void setStrategy(ReportStrategy strategy) { this.strategy strategy; } public void submit(EventPayload payload) { strategy.report(target, payload); } }组装ReportTarget target new SensorsAdapter(new SensorsAPI()); Reporter reporter new Reporter(target, new RealTimeStrategy()); // 支付成功等重要事件实时上报 reporter.submit(paySuccessPayload); // 页面浏览等普通事件切换为批量上报 reporter.setStrategy(new BatchStrategy()); reporter.submit(pageViewPayload);以后新增一个上报渠道只需要新增一个ReportTarget实现类业务代码零改动新增一种上报行为只需要实现一个新的ReportStrategyReporter 代码零改动。测试上也有明确分工适配器测试用 mock 的 SDK验证push方法是否把EventPayload正确转换为 SDK 参数。策略测试用一个 fake 的ReportTarget记录收到的 payload验证批量策略是否会定时 flush、重试策略失败后是否真的重试三次。两边测试边界清楚不会互相干扰。6. 工程落地里的判断标准与踩坑心得最后聊几个代码评审和日常开发里真正影响取舍的经验。这些内容在教科书里一般不会写但对项目的健康度影响很大。6.1 反向的坑什么时候别用这两个模式策略模式不是万金油。我见过有人把只有两种分支的代码也硬套策略模式最后策略工厂、策略接口、实现类一应俱全代码量翻了一倍可读性直线下降。我的底线是三条分支只有两三个且没有继续扩展的迹象别着急上策略模式先写好 if-else 或者 switch。策略之间没有明显的行为差异只是参数不同用配置或规则引擎更合适。策略数量还在个位数增长但每个策略只有一行返回语句用 Lambda 加 Map 注册表就够了不需要建一堆类。适配器模式的过度使用也有典型形态。最常见的是两边的接口本来都在自己项目里却不去改接口反而套了一层适配器。比如两个内部模块接口对不上直接把被调用方的接口改成调用方期望的形态比加适配器干净得多。适配器的定位是兼容存量、对接外部不是给自己制造层。6.2 命名、分层和评审检查点我在项目里对模式的使用有一套强制约定核心就两条一命名必须暴露意图。策略实现用“行为 Strategy”例如RealTimeStrategy、BatchStrategy适配器统一叫XxxAdapter例如SensorsAdapter。代码评审时一眼就能分清层次。二适配器里不允许有业务规则和算法选择逻辑。如果一个叫OrderAdapter的类方法里写了“订单金额大于多少走什么逻辑”这类业务分支那它已经越界了。适配器的职责边界是参数转换和调用转发业务规则应该回到策略模式或业务层去表达。评审时我基本按这个检查点走这个类是不是只在做转发和参数映射如果是但名字没有 Adapter说明命名有问题或职责有问题。实现类之间是否存在可替换的行为差异如果存在却没经过策略接口说明行为耦合在业务层。目标接口的方法签名里有没有外部 SDK 类型如果有适配器没做干净赶紧改。6.3 结合两个模式时的顺序经验很多项目里两个模式同时用是常态。我的顺序经验是适配器永远贴在外侧接外部策略永远贴在内侧接行为。曾经有一个支付项目对接微信和支付宝时先设计了PayChannel策略接口让每个支付渠道各自实现但微信 SDK 的重试机制和支付宝 SDK 的重试机制完全不同策略实现无法统一。后来改成先用PayChannelAdapter统一三个渠道的下单、查询、退款方法再在适配器之上定义RetryStrategy问题才解决。本质原因在于策略模式要求所有实现的行为签名一致如果外部 SDK 的参数差异直接暴露在策略接口里策略接口就不再是一个稳定的抽象了。所以先适配、后策略不是一种风格偏好而是保持接口稳定性的必然路径。代码里没有哪种模式是非用不可的只有哪种模式恰好匹配当前变化的维度。策略模式应对行为变化适配器模式应对接口差异它们的组合在真实项目里几乎是标准解法。我自己在评审里看到一组漂亮的 Aapter 加 Strategy 组合时第一反应不是夸代码优雅而是去看这个团队的接口边界是不是清晰。边界清晰模式自然各就各位。