从硬编码到复合Key路由:一个策略工厂的救赎之路
从硬编码到复合Key路由:一个策略工厂的救赎之路
摘要:在多平台电子面单架构中,策略工厂
StrategyFactory承担着按平台分发请求构建、响应解析、异常判断三大策略的重任。然而,最初的设计埋下了一个致命的“覆盖”陷阱——抖音普通订单和代发订单共用一个平台编码,导致策略互相覆盖。本文记录了该工厂从单维度硬编码路由到引入复合Key机制、统一Key构建方法、增加默认策略兜底的全过程,是一份可复用的设计演进案例。
📖 系列导航
- 系列开篇:从“能跑就行”到“整洁架构”
- 上一篇:多平台统一架构设计
- 本文:策略工厂复合Key路由改造
- 后续:京东、拼多多等平台专项篇
一、事故:抖音普通订单“串门”到了代发策略
故事要从一次测试说起。
那天我正验证抖音普通订单的取号流程,日志却打印出:“抖音代发API调用”。我心头一紧,普通订单怎么走代发逻辑了?
翻开 StrategyFactory 的构造方法,问题一目了然:
public StrategyFactory() {
// 奇门系列
requestMap.put("TM", new QiMenRequestStrategy());
requestMap.put("TB", new QiMenRequestStrategy());
// 抖音:危险!同一个 Key 被用了两次
requestMap.put("DY", new DouYinRequestStrategy()); // 抖音普通
requestMap.put("DY", new DouYinDaiFaRequestStrategy()); // 抖音代发(覆盖了上面)
}
同一个 platformCode 被 put 了两次,第二次直接覆盖了第一次。 抖音普通订单永远拿不到正确的请求策略,因为它被代发策略“偷梁换柱”了。
这可不是小问题——如果上线,所有抖音普通订单都会按代发的逻辑去签名、调接口,轻则报错,重则发错面单。
本文重点讲工厂路由的演进,工厂模式的完整讲解见《Java 23 种设计模式:从踩坑到精通》系列文章。
二、问题根源:单维度 Key 的致命缺陷
当初设计 StrategyFactory 时,只考虑了“一个平台对应一套策略”,于是直接用 platformCode 作为 Map 的 Key。这在只有淘宝、京东等“一个平台一个编码”的场景下没问题。
但抖音的特殊之处在于:同一个平台下存在两种完全不同的业务模式——普通订单和代发订单。它们的请求格式、签名算法、API 地址都不同,必须用不同的策略处理。
而我们的系统里,这两种模式的 platformCode 都是 "DY"。单维度 Key 无法区分,必然发生覆盖。
更糟糕的是,这个问题不仅存在于策略工厂。ApiInvoker(API 调用调度层)同样用 platformCode 路由处理器,抖音普通和代发也会互相覆盖。两处隐患,一触即发。
🏭 设计模式视角:这种“单维度路由导致覆盖”的问题,本质上是工厂模式在路由设计上的不足——简单工厂通常只用单一标识符定位对象,当同一个标识符对应多种实现时就会失效。在《Java 23种设计模式:从踩坑到精通》系列的**第3篇(工厂模式)和第4篇(抽象工厂)**中,我详细拆解了工厂模式如何应对多维度路由场景,欢迎延伸阅读。
三、改造思路:引入复合Key
3.1 从“平台编码”到“平台编码+原始渠道”
仔细观察订单数据,每个订单除了 sourcePlatformCode(平台编码,如 "DY"),还有一个字段叫 tocPlatFormOriginal(原始渠道,如 "DF" 表示代发,普通订单则为 null)。
于是方案自然浮现:用 platformCode + "_" + platFormOriginal 作为复合 Key。
| 订单类型 | platformCode | platFormOriginal | 复合Key |
|---|---|---|---|
| 抖音普通 | "DY" |
null |
"DY_DEFAULT" |
| 抖音代发 | "DY" |
"DF" |
"DY_DF" |
| 奇门/天猫 | "TM" |
null |
"TM_DEFAULT" |
这样,两个抖音模式各走各的 Key,再也不冲突了。
3.2 统一 Key 构建方法
复合 Key 的拼接规则如果散落在各处,未来调整(比如加租户维度)就会成为灾难。必须收口到一个地方。
我在平台常量类 TocWmsSourcePlatFormType 中新增了一个静态方法:
public abstract class TocWmsSourcePlatFormType {
// 平台常量...
public static final String PLAT_DY_CODE = "DY";
public static final String PLAT_DY_DF_CODE = "DF";
/**
* 构建策略/处理器查找的复合 Key
* @param platformCode 订单来源平台编码
* @param platFormOriginal 订单原始渠道(可为 null)
* @return 复合 Key,如 "DY_DEFAULT" / "DY_DF"
*/
public static String buildCompositeKey(String platformCode, String platFormOriginal) {
if (platformCode == null || platformCode.isEmpty()) {
throw new IllegalArgumentException("platformCode 不能为空");
}
if (platFormOriginal != null && !platFormOriginal.isEmpty()) {
return platformCode + "_" + platFormOriginal;
}
return platformCode + "_DEFAULT";
}
}
这个方法随后被 StrategyFactory 和 ApiInvoker 同时调用,确保全局 Key 规则一致。将来如果要加维度,改这一个方法就行。
四、改造实施
4.1 策略工厂:从单维度 put 到复合 Key 注册
改造后的构造方法:
public StrategyFactory() {
// 奇门系列
String tmKey = "TM_DEFAULT";
requestMap.put(tmKey, new QiMenRequestStrategy());
parseMap.put(tmKey, new QiMenParseStrategy());
exceptionMap.put(tmKey, new QiMenExceptionStrategy());
// 抖音普通:DY_DEFAULT
String dyDefaultKey = "DY_DEFAULT";
requestMap.put(dyDefaultKey, new DouYinRequestStrategy());
parseMap.put(dyDefaultKey, new DouYinParseStrategy());
exceptionMap.put(dyDefaultKey, new DouYinExceptionStrategy());
// 抖音代发:DY_DF
String dyDfKey = "DY_DF";
requestMap.put(dyDfKey, new DouYinDaiFaRequestStrategy());
parseMap.put(dyDfKey, new DouYinDaiFaParseStrategy());
exceptionMap.put(dyDfKey, new DouYinDaiFaExceptionStrategy());
}
获取策略的方法也同步调整,增加 platFormOriginal 参数,并加入默认策略兜底:
public RequestStrategy getRequestStrategy(String platformCode, String platFormOriginal) {
String key = TocWmsSourcePlatFormType.buildCompositeKey(platformCode, platFormOriginal);
RequestStrategy strategy = requestMap.get(key);
return strategy != null ? strategy : defaultRequest; // 默认策略兜底
}
4.2 API 调度器:同步升级路由
ApiInvoker 中同样改造:
// 注册 Handler
registerPlatRequestHandler("DY", null, new DouYinSampleHandler()); // 抖音普通 → DY_DEFAULT
registerPlatRequestHandler("DY", "DF", new SimpleHttpHandler("douyin_daifa")); // 抖音代发 → DY_DF
// invoke 方法中
String key = TocWmsSourcePlatFormType.buildCompositeKey(platformCode, platformOriginal);
RequestHandler handler = platHandlerMap.get(key);
4.3 调用方适配
门面层 WaybillFetchService 获取策略时,传入两个参数:
String platformCode = ticket.getSourcePlatformCode();
String platFormOriginal = ticket.getTocPlatFormOriginal();
RequestStrategy req = strategyFactory.getRequestStrategy(platformCode, platFormOriginal);
ParseStrategy parse = strategyFactory.getParseStrategy(platformCode, platFormOriginal);
ExceptionStrategy ex = strategyFactory.getExceptionStrategy(platformCode, platFormOriginal);
订单原有的两个字段直接复用,无需额外改动数据模型。
关于策略模式的更深入讲解,可参考我的《Java面试·实战笔记》系列文章《从多平台电子面单架构看接口与抽象类的真实选型》。
五、防御性设计的额外收益
5.1 默认策略兜底
在改造过程中,我还发现一个隐患:如果某个平台忘记注册策略,获取方法直接返回 null,后续调用必然抛 NullPointerException。
于是增加了默认策略兜底:
private final RequestStrategy defaultRequest = new DefaultRequestStrategy();
private final ParseStrategy defaultParse = new DefaultParseStrategy();
private final ExceptionStrategy defaultException = new DefaultExceptionStrategy();
即使配置遗漏,系统也不至于崩溃,只会走默认的“保守”逻辑,并打印 WARN 日志方便排查。
5.2 平台编码非空校验
buildCompositeKey 中对 platformCode 做了非空校验。如果上游数据出问题导致平台编码为空,会在路由层直接抛异常,而不是拼出一个奇怪的 "_DEFAULT" Key,避免排查困难。
六、改造前后对比
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 路由方式 | 单维度 platformCode |
复合 Key platformCode_original |
| 抖音普通/代发 | 互相覆盖,路由错乱 | 各自独立,精确匹配 |
| Key 构建 | 各组件内联拼接 | 统一静态方法 buildCompositeKey |
| 未注册渠道 | 返回 null,NPE 风险 |
返回默认策略,安全兜底 |
| 扩展性 | 新增子渠道需改造路由逻辑 | 新增子渠道只需增加复合 Key 映射 |
为了更直观地展示复合 Key 的构建与路由分发逻辑,这里补充一张流程图:
图释:
- 统一收口(左侧子图):无论
platFormOriginal是否为空,所有订单数据均通过buildCompositeKey方法生成标准化的复合 Key,彻底杜绝了各组件内联拼接导致的规则不一致。 - 差异化防御(右侧子图):
StrategyFactory与ApiInvoker共享同一套 Key 构建规则,但在未命中时采取不同策略。前者返回默认策略进行保守兜底,防止系统崩溃;后者作为 API 调用的最后一道防线,若找不到 Handler 则直接抛出异常(Fail-Fast),避免向第三方平台发送错误报文。 - 架构收益:该流程在视觉上直观证明了策略工厂和 API 调度器在路由规则上的完全统一,兼顾了系统的灵活性与健壮性。
七、工程权衡与后续演进
当前取舍:为什么先硬编码?
在架构设计中,没有完美的方案,只有适合当前阶段的方案。本次改造的核心目标是快速跑通十几个电商渠道的对接,因此在以下几个点上做了务实的取舍:
1. 策略注册仍保留硬编码
当前策略实例仍然在 StrategyFactory 构造方法中 new 出来直接 put,这确实违反了开闭原则(新增渠道需修改工厂代码)。但在目前阶段,这样做有两个好处:
- 改动最小:不引入 Spring 自动扫描、自定义注解等新机制,团队上手成本为零。
- 问题排查快:所有策略注册一目了然,出问题时直接看构造方法即可定位。
2. 复合 Key 仍使用字符串拼接
buildCompositeKey 用 "_" 作为分隔符,理论上存在隐患——如果未来某个平台编码本身包含下划线,会产生歧义。但在当前所有平台编码(TM、DY、JD、PDD 等)都不含下划线的前提下,这个风险可控。
3. 默认策略兜底偏保守
当前找不到策略时返回默认策略而非抛异常,是为了避免配置遗漏导致线上 NPE。默认策略内部会打印 WARN 日志,确保问题可追踪。
后续优化路线图
| 阶段 | 优化项 | 触发条件 |
|---|---|---|
| 短期 | 抽取 registerStrategy 方法,减少构造器重复代码 |
渠道数量超过 8 个 |
| 中期 | Spring 自动扫描策略实现类,消除构造器硬编码 | 渠道数量稳定,不再频繁新增 |
| 长期 | Key 升级为 CompositeKey 对象,消除字符串拼接隐患 |
出现含特殊字符的平台编码 |
| 长期 | 默认策略接入告警,走兜底时自动通知 | 生产环境出现过配置遗漏 |
八、总结
这次策略工厂的改造,表面上是修了一个“覆盖”Bug,实际上是完成了一次从单维度到多维度的路由升级。核心动作只有三个:
- 引入复合 Key,用
platformCode + "_" + platFormOriginal区分同一平台下的不同业务模式。 - 统一 Key 构建方法,收口到常量类,避免规则散落。
- 增加默认策略兜底,提升系统健壮性。
改造完成后,策略工厂和 API 调度器在路由规则上实现了完全统一。当前保留的硬编码注册方式,是“快速交付”阶段的务实选择——不追求一步到位的完美架构,而是在正确的方向上逐步演进。这为后续接入京东、支付宝等新平台打下了坚实的基础,无论未来出现多么复杂的子渠道组合,只需遵循复合 Key 规范,便可轻松扩展。
九、系列导航与参考
本篇文章是「电商多平台电子面单对接实战」的第五篇(策略工厂路由改造篇),它解决了多平台对接中策略路由冲突的典型痛点,提炼出一套基于复合Key的路由方案,与系列架构设计篇相互呼应。
系列文章目录:
- 开篇:从“能跑就行”到“整洁架构”
- 第一篇:奇门对接顺丰电子面单
- 第二篇:抖音代发电子面单对接
- 第三篇:抖音普通订单电子面单对接
- 第四篇:多平台统一架构设计
- 第五篇:策略工厂复合Key路由改造(本文)
- 第六篇:快递公司前置校验改造
- 第七篇:解析器职责分离改造
- 第八篇:模板方法的组合与继承抉择
- 第九篇:API调用调度层Handler分组设计
- 第十篇:奇门 trade_order_list 排查实录
- 第十一篇:数据库查询优化让多包裹取号快一倍
- 第十二篇:两次架构升级完整复盘
- 第十三篇:常量与配置集中管控改造
- 后续:京东、拼多多、微信视频号等平台专项篇
延伸阅读:Java 23种设计模式实战系列
本文中策略工厂的复合Key路由改造,核心运用了工厂模式和策略模式的组合——工厂负责路由定位,策略负责差异化执行。在《Java 23种设计模式:从踩坑到精通》系列中,这些模式有更体系化的拆解。如果你对以下问题感兴趣,推荐延伸阅读:
- 工厂模式:简单工厂、工厂方法、抽象工厂,如何在路由场景中选型?
- 策略模式:如何定义算法族并与工厂配合实现动态切换?
- 单一职责原则:如何判断工厂是否承担了过多路由逻辑?
📖 《Java 23 种设计模式:从踩坑到精通》
💡 学习建议:电子面单系列侧重业务落地与路由设计,设计模式系列侧重理论体系与设计思维。两者搭配阅读,既能解决实际的路由冲突问题,又能掌握背后的设计模式精髓,形成“实战→理论→反哺实战”的闭环。
十、一起交流,共同进步
技术之路,一个人走得快,一群人走得远。
如果您的团队也在为多平台对接头疼,希望本文的路由设计能给您带来启发。欢迎留言交流。
- 📌 关注我:点击上方“关注”,第一时间获取系列更新推送。
- 💬 留言讨论:如果您在实际对接中遇到类似问题,或对文章有任何建议,欢迎在评论区留言,我会定期回复。
- 🔗 分享转发:如果本文对您有帮助,请 点赞、收藏、分享,让更多同行看到。
更多推荐




所有评论(0)