在后端开发中,我们经常会遇到这样一种场景:
同一个功能,对外看起来是统一入口,但内部却存在多种完全不同的实现方式。
比如"物流详情查询"这个功能。
有些订单是系统自营物流,直接查数据库即可;有些订单走第三方快递,需要调用外部接口并解析复杂 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 反而是最合理的。
因为:
架构的本质,不是堆抽象。
而是控制复杂度。
模式是为了应对变化,而不是为了制造复杂。