从硬编码到复合Key路由:一个策略工厂的救赎之路

摘要:在多平台电子面单架构中,策略工厂 StrategyFactory 承担着按平台分发请求构建、响应解析、异常判断三大策略的重任。然而,最初的设计埋下了一个致命的“覆盖”陷阱——抖音普通订单和代发订单共用一个平台编码,导致策略互相覆盖。本文记录了该工厂从单维度硬编码路由到引入复合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";
    }
}

这个方法随后被 StrategyFactoryApiInvoker 同时调用,确保全局 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 的构建与路由分发逻辑,这里补充一张流程图:

路由分发与执行

统一 Key 构建

platFormOriginal 为空

platFormOriginal 不为空

命中

未命中

命中

未命中

订单数据

提取字段

platformCode + _DEFAULT

platformCode + _ + platFormOriginal

buildCompositeKey

StrategyFactory 查找策略

ApiInvoker 查找 Handler

返回对应策略

返回默认策略兜底

调用对应平台 API

抛出异常 Fail-Fast

图释

  • 统一收口(左侧子图):无论 platFormOriginal 是否为空,所有订单数据均通过 buildCompositeKey 方法生成标准化的复合 Key,彻底杜绝了各组件内联拼接导致的规则不一致。
  • 差异化防御(右侧子图)StrategyFactoryApiInvoker 共享同一套 Key 构建规则,但在未命中时采取不同策略。前者返回默认策略进行保守兜底,防止系统崩溃;后者作为 API 调用的最后一道防线,若找不到 Handler 则直接抛出异常(Fail-Fast),避免向第三方平台发送错误报文。
  • 架构收益:该流程在视觉上直观证明了策略工厂和 API 调度器在路由规则上的完全统一,兼顾了系统的灵活性与健壮性。

七、工程权衡与后续演进

当前取舍:为什么先硬编码?

在架构设计中,没有完美的方案,只有适合当前阶段的方案。本次改造的核心目标是快速跑通十几个电商渠道的对接,因此在以下几个点上做了务实的取舍:

1. 策略注册仍保留硬编码

当前策略实例仍然在 StrategyFactory 构造方法中 new 出来直接 put,这确实违反了开闭原则(新增渠道需修改工厂代码)。但在目前阶段,这样做有两个好处:

  • 改动最小:不引入 Spring 自动扫描、自定义注解等新机制,团队上手成本为零。
  • 问题排查快:所有策略注册一目了然,出问题时直接看构造方法即可定位。

2. 复合 Key 仍使用字符串拼接

buildCompositeKey"_" 作为分隔符,理论上存在隐患——如果未来某个平台编码本身包含下划线,会产生歧义。但在当前所有平台编码(TMDYJDPDD 等)都不含下划线的前提下,这个风险可控。

3. 默认策略兜底偏保守

当前找不到策略时返回默认策略而非抛异常,是为了避免配置遗漏导致线上 NPE。默认策略内部会打印 WARN 日志,确保问题可追踪。

后续优化路线图

阶段 优化项 触发条件
短期 抽取 registerStrategy 方法,减少构造器重复代码 渠道数量超过 8 个
中期 Spring 自动扫描策略实现类,消除构造器硬编码 渠道数量稳定,不再频繁新增
长期 Key 升级为 CompositeKey 对象,消除字符串拼接隐患 出现含特殊字符的平台编码
长期 默认策略接入告警,走兜底时自动通知 生产环境出现过配置遗漏

八、总结

这次策略工厂的改造,表面上是修了一个“覆盖”Bug,实际上是完成了一次从单维度到多维度的路由升级。核心动作只有三个:

  1. 引入复合 Key,用 platformCode + "_" + platFormOriginal 区分同一平台下的不同业务模式。
  2. 统一 Key 构建方法,收口到常量类,避免规则散落。
  3. 增加默认策略兜底,提升系统健壮性。

改造完成后,策略工厂和 API 调度器在路由规则上实现了完全统一。当前保留的硬编码注册方式,是“快速交付”阶段的务实选择——不追求一步到位的完美架构,而是在正确的方向上逐步演进。这为后续接入京东、支付宝等新平台打下了坚实的基础,无论未来出现多么复杂的子渠道组合,只需遵循复合 Key 规范,便可轻松扩展。


九、系列导航与参考

本篇文章是「电商多平台电子面单对接实战」的第五篇(策略工厂路由改造篇),它解决了多平台对接中策略路由冲突的典型痛点,提炼出一套基于复合Key的路由方案,与系列架构设计篇相互呼应。

系列文章目录


延伸阅读:Java 23种设计模式实战系列

本文中策略工厂的复合Key路由改造,核心运用了工厂模式策略模式的组合——工厂负责路由定位,策略负责差异化执行。在《Java 23种设计模式:从踩坑到精通》系列中,这些模式有更体系化的拆解。如果你对以下问题感兴趣,推荐延伸阅读:

  • 工厂模式:简单工厂、工厂方法、抽象工厂,如何在路由场景中选型?
  • 策略模式:如何定义算法族并与工厂配合实现动态切换?
  • 单一职责原则:如何判断工厂是否承担了过多路由逻辑?

📖 《Java 23 种设计模式:从踩坑到精通》

💡 学习建议:电子面单系列侧重业务落地与路由设计,设计模式系列侧重理论体系与设计思维。两者搭配阅读,既能解决实际的路由冲突问题,又能掌握背后的设计模式精髓,形成“实战→理论→反哺实战”的闭环。


十、一起交流,共同进步

技术之路,一个人走得快,一群人走得远。
如果您的团队也在为多平台对接头疼,希望本文的路由设计能给您带来启发。欢迎留言交流。

  • 📌 关注我:点击上方“关注”,第一时间获取系列更新推送。
  • 💬 留言讨论:如果您在实际对接中遇到类似问题,或对文章有任何建议,欢迎在评论区留言,我会定期回复。
  • 🔗 分享转发:如果本文对您有帮助,请 点赞收藏分享,让更多同行看到。
Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐