从物流查询聊策略模式:后端开发中的多策略设计

在后端开发中,我们经常会遇到这样一种场景:

同一个功能,对外看起来是统一入口,但内部却存在多种完全不同的实现方式。

比如"物流详情查询"这个功能。

有些订单是系统自营物流,直接查数据库即可;有些订单走第三方快递,需要调用外部接口并解析复杂 JSON;还有一些订单甚至还没有真正发货,只是用户保存的一份草稿,需要系统动态生成一份"虚拟物流信息"。

很多业务系统最开始都会这样写:

arduino 复制代码
public LogisticsDTO getLogisticsDetail(LogisticsQuery query) {

    if (query.getType() == INTERNAL) {

        // 查询自营物流数据库

    } else if (query.getType() == THIRD_PARTY) {

        // 调用第三方接口
        // 解析 JSON
        // 处理异常

    } else if (query.getType() == DRAFT) {

        // 生成草稿预填数据

    } else {

        throw new BusinessException("未知物流类型");
    }
}

刚开始的时候,这种写法其实没有任何问题。

逻辑简单、直观,开发速度也快。

但真实项目的问题在于:业务不会停在第一版。

第三方物流可能很快从"一个渠道"变成:

  • 顺丰
  • 京东
  • DHL
  • 菜鸟
  • 邮政

接着:

  • 每个平台 JSON 格式都不同;
  • 每个平台错误码不同;
  • 每个平台签名机制不同;
  • 每个平台超时重试逻辑不同。

然后草稿单又开始区分:

  • 海外用户
  • 企业用户
  • VIP 用户

最后,一个原本几十行的方法,会逐渐膨胀成一个没人敢动的巨石方法。

真正麻烦的地方并不是代码变长。

而是:

你已经无法确定,自己新加的一个 else if,会不会把线上另外一个逻辑改炸。

这其实就是典型的"变化耦合"。

所有不断变化的业务,都被堆积到了同一个方法里。

而策略模式,本质上就是在解决这个问题。

可以阅读一下菜鸟教程的策略模式:www.runoob.com/design-patt...

策略模式(Strategy Pattern)的核心思想其实非常简单:

同一件事情,允许存在多种不同实现,并且这些实现之间可以自由切换。

重点不在"切换"。

重点在于:

把变化拆开。

不要把所有逻辑都塞进一个方法里,而是把每一种处理方式,独立封装成一个单独的策略类。

例如:

  • 自营物流是一种策略;
  • 第三方物流是一种策略;
  • 草稿物流是一种策略。

每个策略只关心自己的逻辑。

这样以后新增功能时,本质上是在"增加新代码",而不是"修改旧代码"。

这一点其实非常重要。

很多人第一次学习开闭原则(OCP)时,会觉得它是在强调"代码优雅"。

但真实工程里,它真正重要的是:

降低回归风险。

因为线上旧代码往往:

  • 已经过测试;
  • 已经跑了很久;
  • 已经被很多业务依赖。

修改它的风险非常高。

优秀架构最核心的目标之一,其实就是:

新增功能时,尽量不要触碰旧逻辑。

下面我们来看一下,一个更适合真实项目的实现方式。

首先定义物流类型枚举:

less 复制代码
@Getter
@AllArgsConstructor
public enum LogisticsTypeEnum {

    INTERNAL(1, "内部自营"),
    THIRD_PARTY(2, "第三方快递"),
    DRAFT(3, "虚拟草稿");

    private final Integer code;
    private final String description;

    public static LogisticsTypeEnum match(Integer code) {

        return Arrays.stream(values())
                .filter(item -> Objects.equals(item.code, code))
                .findFirst()
                .orElseThrow(() -> 
                        new IllegalArgumentException("未知物流类型"));
    }
}

然后定义统一的策略抽象类:

csharp 复制代码
public abstract class AbsLogisticsQuery {

    public final LogisticsDTO queryDetail(LogisticsQueryParam param) {

        LogisticsEntity entity = getRawLogisticsData(param);

        return convertToDTO(entity);
    }

    /**
     * 获取原始物流数据
     */
    protected abstract LogisticsEntity getRawLogisticsData(
            LogisticsQueryParam param);

    /**
     * 转换统一 DTO
     */
    protected abstract LogisticsDTO convertToDTO(
            LogisticsEntity entity);
}

这里其实不仅仅是策略模式。

还额外结合了模板方法模式(Template Method)。

策略模式负责:

选择哪一种实现。

模板方法模式负责:

固定整体流程。

也就是说:

无论是哪一种物流查询,它都必须遵循:

rust 复制代码
获取数据 -> 转换 DTO

这个流程不允许被破坏。

但具体怎么获取数据、怎么转换,则交给不同子类自由实现。

比如自营物流:

typescript 复制代码
@Component("internalQueryStrategy")
public class InternalLogisticsQuery 
        extends AbsLogisticsQuery {

    @Override
    protected LogisticsEntity getRawLogisticsData(
            LogisticsQueryParam param) {

        return new LogisticsEntity(
                "INTERNAL_001",
                "已从上海仓发出");
    }

    @Override
    protected LogisticsDTO convertToDTO(
            LogisticsEntity entity) {

        return LogisticsDTO.builder()
                .trackNo(entity.getTrackNo())
                .statusDescription(entity.getLastMessage())
                .source("系统自营")
                .build();
    }
}

第三方物流:

typescript 复制代码
@Component("thirdPartyQueryStrategy")
public class ThirdPartyLogisticsQuery
        extends AbsLogisticsQuery {

    @Override
    protected LogisticsEntity getRawLogisticsData(
            LogisticsQueryParam param) {

        String rawJson = callThirdPartyApi();

        JSONObject json = JSONObject.parseObject(rawJson);

        return new LogisticsEntity(
                json.getString("trace_id"),
                json.getJSONObject("data")
                        .getJSONArray("nodes")
                        .getJSONObject(0)
                        .getString("msg")
        );
    }

    @Override
    protected LogisticsDTO convertToDTO(
            LogisticsEntity entity) {

        return LogisticsDTO.builder()
                .trackNo(entity.getTrackNo())
                .statusDescription(entity.getLastMessage())
                .source("第三方接口")
                .build();
    }
}

草稿物流:

typescript 复制代码
@Component("draftQueryStrategy")
public class DraftLogisticsQuery
        extends AbsLogisticsQuery {

    @Override
    protected LogisticsEntity getRawLogisticsData(
            LogisticsQueryParam param) {

        return new LogisticsEntity(
                "PENDING",
                "待发货");
    }

    @Override
    protected LogisticsDTO convertToDTO(
            LogisticsEntity entity) {

        return LogisticsDTO.builder()
                .trackNo("暂无单号")
                .statusDescription("等待快递员揽收")
                .source("系统预设")
                .build();
    }
}

接下来,需要解决的问题是:

系统如何根据不同类型,找到对应策略。

这里可以结合 Spring 的 Bean 管理机制,把所有策略统一注册到一个 Map 中:

less 复制代码
@Configuration
public class LogisticsStrategyConfig {

    @Bean("logisticsQueryMap")
    public Map<LogisticsTypeEnum, AbsLogisticsQuery> 
            logisticsQueryMap(

            @Qualifier("internalQueryStrategy")
            AbsLogisticsQuery internal,

            @Qualifier("thirdPartyQueryStrategy")
            AbsLogisticsQuery thirdParty,

            @Qualifier("draftQueryStrategy")
            AbsLogisticsQuery draft) {

        Map<LogisticsTypeEnum, AbsLogisticsQuery> map =
                new HashMap<>();

        map.put(LogisticsTypeEnum.INTERNAL, internal);
        map.put(LogisticsTypeEnum.THIRD_PARTY, thirdParty);
        map.put(LogisticsTypeEnum.DRAFT, draft);

        return map;
    }
}

最终,Service 层会变得非常干净:

typescript 复制代码
@Service
public class LogisticsService {

    @Resource(name = "logisticsQueryMap")
    private Map<LogisticsTypeEnum, AbsLogisticsQuery>
            queryMap;

    public LogisticsDTO queryDetail(
            LogisticsQueryParam param) {

        LogisticsTypeEnum type =
                LogisticsTypeEnum.match(param.getType());

        AbsLogisticsQuery strategy =
                queryMap.get(type);

        return strategy.queryDetail(param);
    }
}

整个过程中,Service 不再关心:

  • JSON 怎么解析;
  • 数据从哪里来;
  • 草稿逻辑怎么生成;
  • 第三方接口怎么处理。

它只负责:

找到对应策略,然后执行。

这也是策略模式真正优雅的地方。

它不是为了"炫技"。

而是在复杂业务不断扩展时,主动把变化隔离开。

还有一个很容易被忽略,但实际上非常重要的点:

虽然底层逻辑完全不同,但系统最终统一返回:

复制代码
LogisticsDTO

这其实是一种非常典型的架构思想:

内部多样化,对外标准化。

否则:

Controller 层会开始知道:

  • 顺丰字段叫什么;
  • DHL JSON 怎么解析;
  • 草稿状态如何兼容。

最后复杂度会不断向上蔓延。

而统一 DTO,本质上是在隔离底层复杂性。

不过最后还是要说一句:

设计模式并不是银弹。

很多人学完设计模式之后,会进入一种"万物皆模式"的状态。

实际上:

如果你的业务只有两个稳定分支,而且未来几乎不会扩展,那么直接写 if-else 反而是最合理的。

因为:

架构的本质,不是堆抽象。

而是控制复杂度。

模式是为了应对变化,而不是为了制造复杂。

相关推荐
bcbnb1 小时前
iOS开发中手动实现代码混淆的完整步骤与示例
后端·ios
河阿里2 小时前
SpringBoot:项目启动速度深度优化
java·spring boot·后端
Code_Artist2 小时前
线程池的终结?协程/纤程/虚拟线程带来的并发范式变化!
后端·架构·代码规范
阿丰资源2 小时前
基于SpringBoot的企业客户管理系统(附源码)
java·spring boot·后端
两年半的个人练习生^_^2 小时前
SpringBoot 项目使用 Jasypt 实现配置文件敏感信息加密
java·spring boot·后端
阿凡9807302 小时前
从零实现嘉立创 EDA 与 FreeCAD 的 PCB 双向实时协同
后端
AIData搭子2 小时前
一条命令迁移,一个记忆库共享——基于阿里云 Tablestore 的迁移实战指南来了,全文干货,赶紧收藏!
后端
Rust研习社3 小时前
开源项目里的 deny.toml 是什么?
后端·rust·编程语言
undefinedType3 小时前
PostgreSQL JIT 详细讲解
后端