策略模式管“怎么算”,简单工厂模式管“用哪个算”,二者组合实现开闭原则:新增优惠规则只需加两个类、改一处注册逻辑,不触碰原有业务代码。

直接说结论:策略模式管「怎么算」,简单工厂模式管「用哪个算」,两者组合后,新增一种优惠规则,只需加两个类、改一处注册逻辑,不用碰原有任何业务代码。
为什么不能只用 if-else 或只用策略模式
if-else 写死在方法里,每次加新会员等级或新活动规则,都得打开 actualPay 方法改逻辑,一不小心就破坏已有分支;只上策略模式但没工厂,客户端就得自己 new 具体策略类(比如 new SeniorMemberStrategy()),等于把创建逻辑散落在各处,后续想统一加日志、缓存或参数校验时无从下手。
常见错误现象包括:
- 新增一个「钻石会员」后,忘记在某个支付入口的 if 分支里补 case,导致该入口始终按非会员结算
- 策略类构造依赖了
RedisTemplate,但上下文手动 new 时没传,运行时报NullPointerException - 不同模块(订单、退款、对账)各自 new 同一类策略,导致配置不一致,比如折扣率一个写 0.6,一个写 0.599
策略接口定义要聚焦行为契约,而非数据结构
接口名别叫 MemberDiscountStrategy 这种带具体业务词的,容易锁死场景。更通用的命名是 CalculationStrategy,方法签名只暴露必要参数:
public interface CalculationStrategy {
BigDecimal calculate(BigDecimal baseAmount, Map<String, Object> context);
}
这样未来扩展「满减」「跨店叠加」「积分抵扣」等新算路时,不用改接口,只新增实现类即可。关键点:
-
baseAmount是原始金额,不预设是否含税、是否已折上折,由调用方决定传什么 -
context用Map而非 DTO,避免每加一个字段就改接口——策略内部只取自己关心的 key,比如context.get("memberLevel")或context.get("orderTotal") - 返回值用
BigDecimal,不返回「减免额」也不返回「实付额」,由上下文决定怎么用结果
简单工厂必须控制实例生命周期,不能每次都 new
工厂类里别写 return new JuniorMemberStrategy() 这种裸 new,而应提前初始化好单例对象:
public class CalculationStrategyFactory {
private static final Map<String, CalculationStrategy> STRATEGY_MAP = new ConcurrentHashMap<>();
static {
STRATEGY_MAP.put("JUNIOR", new JuniorMemberStrategy());
STRATEGY_MAP.put("SENIOR", new SeniorMemberStrategy());
STRATEGY_MAP.put("FLASH_SALE", new FlashSaleStrategy());
}
public static CalculationStrategy getStrategy(String code) {
CalculationStrategy strategy = STRATEGY_MAP.get(code);
if (strategy == null) {
throw new IllegalArgumentException("No strategy found for code: " + code);
}
return strategy;
}
}
这么做的原因:
- 避免重复构造开销,尤其策略里含连接池、缓存 client 时
- 方便后续统一做 AOP,比如所有策略执行前记录耗时、失败自动降级
- 防止误传空字符串或大小写不一致导致
getStrategy(null)报 NPE,工厂层主动兜底
注意:如果策略需要运行时动态加载(如从 DB 配置表读规则),那就不能用静态 map,得换成 Spring 的 @Scope("prototype") + ObjectProvider 拉取,但那是另一层复杂度了。
上下文类要隔离策略选择与策略执行
别让业务代码直接调工厂拿策略再调方法,中间必须包一层 CalculationContext:
public class CalculationContext {
private CalculationStrategy strategy;
public void setStrategy(CalculationStrategy strategy) {
this.strategy = strategy;
}
public BigDecimal execute(BigDecimal amount, Map<String, Object> context) {
if (strategy == null) {
throw new IllegalStateException("Strategy not set");
}
return strategy.calculate(amount, context);
}
}
这样做的实际好处:
- 测试时可轻松 mock 策略行为,不用启动整个 Spring 容器
- 后续加「策略链」(比如先打折再满减)只要改
execute方法,不侵入已有策略类 - 前端传错
memberLevel=senior(小写),可以在setStrategy里统一转大写或抛明确异常,而不是让策略类自己处理脏数据
真正容易被忽略的是:策略类内部永远不要尝试从 Spring 上下文里 getBean,那会制造隐式依赖,导致单元测试无法脱离容器运行。所有外部依赖(如 Redis、配置中心)必须通过构造函数注入,由工厂类统一组装。

















