策略模式替换 if-else 后,为什么代码反而更难读
先给结论:策略模式不是把 if-else 搬进类里就完事,选择逻辑放错位置,代码只会从"一坨分支"变成"一堆类加一坨分支"。这篇用一个真实场景把这件事挖透:订单优惠计算。
手把手实操
场景:下单要算优惠,会员折扣、满减、优惠券三种规则,以后还会加。第一版你大概率这么写:
public BigDecimal discount(Order order) {
if (order.getType() == Type.MEMBER) {
return order.getAmount().multiply(new BigDecimal("0.9"));
} else if (order.getType() == Type.FULL_REDUCE) {
return order.getAmount().compareTo(new BigDecimal("200")) >= 0
? order.getAmount().subtract(new BigDecimal("30")) : order.getAmount();
} else if (order.getType() == Type.COUPON) {
return order.getAmount().subtract(order.getCouponValue());
}
return order.getAmount();
}
问题不在分支多,在于每加一种规则都要改这个方法,测试也得重跑全部。
抽策略接口:
public interface DiscountStrategy {
BigDecimal apply(Order order);
DiscountType type();
}
三个实现类各管一种算法。关键在"谁来选"。很多人写成工厂里再套 if-else,等于没改。正确做法是让 Spring 帮你选:实现类加 @Component,注入 List,启动时建 Map。
@Component
public class DiscountStrategyFactory {
private final Map<DiscountType, DiscountStrategy> map = new HashMap<>();
public DiscountStrategyFactory(List<DiscountStrategy> strategies) {
for (DiscountStrategy s : strategies) {
map.put(s.type(), s);
}
}
public DiscountStrategy get(DiscountType type) {
DiscountStrategy s = map.get(type);
if (s == null) throw new IllegalArgumentException("unsupported: " + type);
return s;
}
}
调用方只剩 factory.get(order.getType()).apply(order)。关键点有两个:构造器注入 List 是 Spring 的集合注入,容器会把所有实现按类型塞进来;type() 方法让每个策略自己声明身份,工厂不用认识具体类。踩坑点:如果两个实现返回同一个 type(),put 会静默覆盖,线上表现为"某个优惠莫名其妙不生效"。加个 if (map.put(...) != null) throw 就能在启动时炸出来。
看源码及解析
Spring 里这套玩法的源头是 DefaultListableBeanFactory 的按类型查找。getBeanNamesForType 走的是 doGetBeanNamesForType,它遍历 beanDefinitionNames,对每个 Bean 判断是否匹配目标类型,匹配结果会缓存进 allBeanNamesByType 和 singletonBeanNamesByType 两个 Map,所以集合注入不是每次启动都全量扫描。这解释了为什么策略类多到几十个也不至于拖慢启动——第一次查完就缓存了。
再往下,DefaultListableBeanFactory 对 List 这种依赖的解析在 resolveMultipleBeans 里:先按类型拿到所有候选 Bean 名,再逐个 getBean 实例化,最后 adaptBeanInstance 转成集合。顺序默认跟 Bean 定义注册顺序有关,不是字母序,别依赖 List 的下标。
设计动机很直白:Spring 把"按类型找实现"这件事做成了容器能力,你就不用自己维护注册表。代价是隐式——看代码时不知道谁实现了接口,得靠 IDE 的"查找实现"。这也是策略模式被吐槽"难读"的根源,不是模式的问题,是注册关系从显式变成了隐式。
验证方法
启动应用,日志里搜不到异常就说明没有重复 type()。再写个测试:往容器里 @Autowired List,断言 size 等于实现类个数,少一个就说明某个类没被扫到(常见原因:实现类没加 @Component,或包不在扫描路径下)。最后手动传一个不存在的 DiscountType,确认抛的是你自定义的异常而不是 NullPointerException——这能证明工厂的兜底分支真的走到了。
面试速答
- 策略模式解决的是算法可替换,不是消灭 if-else;选择逻辑要收敛到工厂或注册表,否则等于没重构。
- Spring 用构造器注入
List<接口>自动收集实现,靠getBeanNamesForType按类型查找并缓存,顺序不保证。 - 每个策略用
type()自报身份,工厂建 Map 时检测重复 key,避免静默覆盖。 - 代价是注册关系隐式化,可读性下降,实现类少时不如直接 if-else。
核心收获:策略模式的价值在选择逻辑集中且可扩展,不在类多。下一步,把工厂里那个 Map 换成枚举驱动的注册,看看能不能连 type() 方法都省掉。
本文关键词:策略模式、Spring 集合注入、工厂、重构