Java策略模式实战:解耦算法与主流程的工程化落地
1. 什么是策略模式?它真不是“写一堆if-else再包个接口”那么简单
策略模式(Strategy Design Pattern)在Java开发中被反复提及,尤其在面试场景里几乎成了设计模式的“入门必答题”。但很多人一开口就是“定义:定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换”,说完就卡壳——这就像告诉你“汽车是四个轮子加发动机”,却没讲清楚为什么底盘要调校、变速箱为什么要匹配齿比、ABS介入时机怎么标定。策略模式真正的价值,从来不在“能换算法”这个表象,而在于 把变化点从主流程中剥离出来,让核心逻辑稳定、可测、可演进 。
我带过不少刚转Java的同事,他们第一次写支付模块时,常会写出这样的代码:
public class PaymentService {
public void processPayment(String type, BigDecimal amount) {
if ("alipay".equals(type)) {
// 支付宝签名、验签、调用SDK...
} else if ("wechat".equals(type)) {
// 微信统一下单、回调验签、结果处理...
} else if ("bank_transfer".equals(type)) {
// 银行接口对接、对账文件生成...
}
}
}
这段代码的问题,远不止“违反开闭原则”这么抽象。实操中它会立刻暴露出三个硬伤:第一,每次新增一种支付方式(比如突然要上数字人民币),就得改 processPayment 方法,动一次就要全量回归测试;第二,单元测试极其痛苦——你得mock所有分支路径,一个 if 条件漏测,线上就可能跳转到错误渠道;第三,不同支付渠道的异常处理逻辑混在一起,支付宝超时重试和微信证书过期的兜底策略根本没法复用,最后变成满屏 try-catch 嵌套。
策略模式的解法,本质是 用多态替代条件分支,用组合替代继承,用接口契约约束行为边界 。它不追求“炫技”,而是解决一个非常具体的问题:当系统中存在 同一类行为的多种实现,且这些实现之间互斥、可替换、未来大概率会增删 时,如何让主干代码像混凝土一样坚固,而算法细节像乐高积木一样可插拔。这不是教科书里的理想模型,而是我在电商大促压测、金融风控规则迭代、IoT设备协议适配等真实项目里,反复验证过的“防抖方案”。
你可能会问:那和简单工厂模式有什么区别?关键就在 谁持有策略实例、何时决定策略、策略间是否需要共享上下文 。工厂模式解决的是“创建谁”,策略模式解决的是“用谁来执行”。比如订单创建用工厂,订单支付用策略——前者关注对象诞生,后者关注行为执行。这个分界线,我在三次跨团队重构中踩过坑:有次把支付策略的初始化硬编码在Spring Bean里,结果灰度发布时无法动态切换渠道,最后连夜改成 @ConditionalOnProperty 配合策略注册表才救回来。所以别只背UML图,先想清楚你的业务里,哪些“变”是高频的、哪些“不变”是核心的,这才是策略模式落地的第一步。
2. 策略模式的核心结构拆解:为什么必须有Context、Strategy、ConcreteStrategy三层?
策略模式的标准结构由三部分组成: 策略接口(Strategy)、具体策略实现(ConcreteStrategy)、策略上下文(Context) 。很多教程把Context简单说成“持有一个策略引用”,这严重低估了它的工程价值。在我参与的7个中大型Java项目里,Context从来不是个透明胶水层,而是 策略调度中枢、上下文透传管道、执行生命周期管理者 。下面逐层拆解每个角色的真实职责和常见误用。
2.1 Strategy接口:契约即法律,不是摆设
策略接口定义的是 行为契约 ,而非数据契约。以支付为例,正确的接口设计是:
public interface PaymentStrategy {
/**
* 执行支付主流程
* @param order 订单信息(含金额、用户ID、商品明细)
* @return 支付结果(含流水号、状态、跳转URL)
*/
PaymentResult execute(Order order);
/**
* 验证当前策略是否适用此订单
* @param order 订单信息
* @return true表示可执行,false需降级或报错
*/
boolean supports(Order order);
/**
* 获取策略唯一标识,用于日志追踪和监控
*/
String getStrategyCode();
}
注意三个关键点:第一, execute() 方法参数是 Order 对象而非原始字段(如 String orderId, BigDecimal amount ),这是为了 避免策略实现中重复解析订单 ,也方便未来扩展订单属性;第二,强制要求 supports() 方法,这是策略模式的“安全阀”——没有这个方法,系统就无法做运行时策略路由,只能靠外部硬编码判断;第三, getStrategyCode() 是监控命脉,线上出问题时,日志里看到 strategyCode=alipay_v3 比看到 class=AlipayV3Strategy 直观十倍。
反面案例我见过太多:有人把策略接口设计成 void pay(String orderId, BigDecimal amount) ,结果每个实现都要自己查库、自己组装参数,不仅性能差,还导致 AlipayStrategy 和 WechatStrategy 里出现大量重复代码。还有人把 supports() 逻辑写在Context里,结果新增策略时总忘记同步修改Context的判断逻辑,造成策略“幽灵失效”。
2.2 ConcreteStrategy:每个实现都是独立的“微服务”
具体策略实现不是简单的算法函数,而是一个 自包含的业务单元 。以支付宝支付策略为例,它的完整实现要考虑:
- 依赖隔离 :支付宝SDK版本与微信SDK版本冲突怎么办?我们采用Maven
providedscope + ClassLoader隔离,在AlipayStrategy里通过Thread.currentThread().getContextClassLoader()加载专属SDK,避免污染全局类路径。 - 配置外化 :AppId、私钥路径、网关地址不能写死在代码里。我们约定所有策略实现必须实现
Configurable接口,通过@Value("${payment.alipay.app-id}")注入,且配置项前缀与策略码强绑定(payment.{strategyCode}.xxx)。 - 异常分类 :支付宝返回
INVALID_SIGN和TRADE_NOT_EXIST必须走不同告警通道。我们在execute()里统一捕获AlipayApiException,按getSubCode()映射为预定义的PaymentErrorType(如SIGN_ERROR,ORDER_NOT_FOUND),上层Context只处理这几种标准错误。
这种设计让每个策略实现像一个微型服务:它知道自己该做什么、依赖什么、失败时怎么报错。我在某银行项目里曾把12种支付渠道拆成12个独立JAR包,部署时按需加载,运维同学反馈故障定位时间从小时级降到分钟级——因为 alipay-v3.2.1.jar 出问题,绝不会影响 unionpay-2.8.0.jar 。
2.3 Context:被严重低估的“策略交响乐指挥家”
Context常被简化为:
public class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) { this.strategy = strategy; }
public PaymentResult execute(Order order) { return strategy.execute(order); }
}
这在Demo里没问题,但在生产环境会出大事。真实的Context必须承担以下职责:
- 策略路由 :根据订单属性(如用户等级、支付金额、地域)选择策略。我们用
Map<String, PaymentStrategy>缓存所有策略,key为strategyCode,路由逻辑抽成StrategyRouter组件,支持SPI扩展。 - 上下文透传 :支付过程中需要传递风控Token、营销优惠券ID、物流单号等,这些不能塞进
Order对象污染领域模型。Context提供withContext(Map<String, Object> context)方法,将临时上下文透传给策略。 - 执行增强 :自动添加日志埋点(记录策略码、耗时、输入摘要)、熔断保护(调用超时自动降级)、结果缓存(对查询类策略)。这些通用能力通过装饰器模式注入,而非在每个策略里重复写。
最典型的教训:某次大促,微信支付策略因证书更新失败,但Context没做熔断,导致所有微信支付请求堆积,最终拖垮整个订单服务。后来我们强制要求Context必须集成Hystrix,且 execute() 方法默认500ms超时,超时后自动触发 fallbackStrategy 。这个改动让系统稳定性提升了40%。
3. 从零手写一个生产级策略模式:电商优惠券计算实战
光讲理论不够,我们来实操一个真实场景:电商优惠券计算。这个需求完美契合策略模式——优惠类型多(满减、折扣、赠品、N选一)、规则复杂(叠加限制、互斥条件)、变更频繁(运营天天改规则)。下面展示如何从零构建一个可维护、可监控、可扩展的优惠券策略体系。
3.1 需求分析与策略边界划定
先明确什么该放进策略,什么不该:
- ✅ 应放入策略:满300减50的计算逻辑、85折的折扣公式、赠品SKU的选取规则;
- ❌ 不应放入策略:优惠券库存扣减(这是事务性操作,应由CouponService统一处理)、用户资格校验(如新用户专享,应前置拦截)、日志记录(Context统一处理)。
我们定义策略接口:
public interface CouponCalculationStrategy {
/**
* 计算优惠金额/赠品列表
* @param cart 购物车(含商品、数量、价格)
* @param coupon 优惠券(含面额、门槛、类型)
* @param context 运行时上下文(如用户ID、设备指纹)
* @return 计算结果(优惠金额、赠品列表、是否可用)
*/
CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context);
/**
* 判断当前优惠券是否适用于此购物车
* @param cart 购物车
* @param coupon 优惠券
* @param context 上下文
* @return 是否支持
*/
boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context);
/**
* 获取策略码,对应优惠券type字段
* @return 策略码
*/
String getStrategyCode();
}
这里 CalculationResult 是统一返回结构,包含 discountAmount (优惠金额)、 giftSkus (赠品SKU列表)、 usable (是否可用)、 reason (不可用原因)。所有策略必须返回这个结构,上层无需关心具体实现。
3.2 具体策略实现:满减与折扣的差异化处理
满减策略(FullReductionStrategy)
@Component
@ConditionalOnProperty(name = "coupon.strategy.full-reduction.enabled", havingValue = "true")
public class FullReductionStrategy implements CouponCalculationStrategy {
private final Logger logger = LoggerFactory.getLogger(FullReductionStrategy.class);
@Value("${coupon.full-reduction.min-amount:300}")
private BigDecimal minAmount;
@Value("${coupon.full-reduction.discount-amount:50}")
private BigDecimal discountAmount;
@Override
public CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
BigDecimal totalAmount = cart.getTotalAmount();
if (totalAmount.compareTo(minAmount) >= 0) {
// 满足门槛,直接减固定金额
return CalculationResult.builder()
.discountAmount(discountAmount)
.usable(true)
.build();
} else {
return CalculationResult.builder()
.usable(false)
.reason("订单金额" + totalAmount + "不足" + minAmount + "元")
.build();
}
}
@Override
public boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
// 检查优惠券类型是否为FULL_REDUCTION,且未过期
return "FULL_REDUCTION".equals(coupon.getType())
&& coupon.getExpiredAt().isAfter(Instant.now());
}
@Override
public String getStrategyCode() {
return "FULL_REDUCTION";
}
}
折扣策略(DiscountStrategy)
@Component
@ConditionalOnProperty(name = "coupon.strategy.discount.enabled", havingValue = "true")
public class DiscountStrategy implements CouponCalculationStrategy {
private final Logger logger = LoggerFactory.getLogger(DiscountStrategy.class);
@Value("${coupon.discount.rate:0.85}")
private BigDecimal discountRate;
@Override
public CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
BigDecimal totalAmount = cart.getTotalAmount();
// 折扣计算需保留两位小数,避免浮点误差
BigDecimal discount = totalAmount.multiply(BigDecimal.ONE.subtract(discountRate))
.setScale(2, RoundingMode.HALF_UP);
return CalculationResult.builder()
.discountAmount(discount)
.usable(true)
.build();
}
@Override
public boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
return "DISCOUNT".equals(coupon.getType())
&& coupon.getExpiredAt().isAfter(Instant.now())
&& isUserEligible(context); // 额外校验:比如仅限VIP用户
}
private boolean isUserEligible(Map<String, Object> context) {
String userId = (String) context.get("userId");
// 调用用户服务查询VIP等级
return userService.isVip(userId);
}
@Override
public String getStrategyCode() {
return "DISCOUNT";
}
}
关键细节:
- 使用
@ConditionalOnProperty控制策略开关,上线新策略时无需发版,改配置即可; calculate()方法内不做远程调用(如查用户等级),而是通过context传入必要数据,保证策略纯函数特性;- 金额计算强制
setScale(2, RoundingMode.HALF_UP),这是金融计算铁律,我见过因double精度导致优惠多算0.01元被用户投诉的事故。
3.3 Context实现:策略路由与执行增强
@Service
public class CouponCalculationContext {
// 策略注册表:策略码 -> 策略实例
private final Map<String, CouponCalculationStrategy> strategyMap;
// 熔断器配置
private final HystrixCommand.Setter hystrixSetter;
public CouponCalculationContext(List<CouponCalculationStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(CouponCalculationStrategy::getStrategyCode, Function.identity()));
this.hystrixSetter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("COUPON_CALCULATION"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CALCULATE"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.defaultSetter()
.withExecutionTimeoutInMilliseconds(300) // 300ms超时
.withCircuitBreakerEnabled(true)
);
}
/**
* 主执行入口
*/
public CalculationResult execute(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
long startTime = System.currentTimeMillis();
// 1. 路由:找到匹配的策略
CouponCalculationStrategy strategy = findStrategy(cart, coupon, context);
if (strategy == null) {
return CalculationResult.builder()
.usable(false)
.reason("无匹配的优惠券计算策略")
.build();
}
// 2. 执行:带熔断和日志
CalculationResult result = new HystrixCommand<CalculationResult>(hystrixSetter) {
@Override
protected CalculationResult run() throws Exception {
return strategy.calculate(cart, coupon, context);
}
@Override
protected CalculationResult getFallback() {
// 熔断降级:返回默认结果或调用备用策略
return CalculationResult.builder()
.usable(false)
.reason("策略执行超时,已降级")
.build();
}
}.execute();
// 3. 日志:记录关键指标
long costTime = System.currentTimeMillis() - startTime;
logger.info("CouponCalculation executed | strategy={} | cartId={} | couponId={} | cost={}ms | result={}",
strategy.getStrategyCode(), cart.getId(), coupon.getId(), costTime, result);
return result;
}
/**
* 策略路由逻辑
*/
private CouponCalculationStrategy findStrategy(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
return strategyMap.values().stream()
.filter(s -> s.supports(cart, coupon, context))
.findFirst()
.orElse(null);
}
}
这个Context实现了三大能力:
- 动态路由 :
findStrategy()遍历所有注册策略,调用supports()判断,无需硬编码if-else; - 熔断保护 :Hystrix确保单个策略故障不影响全局,降级逻辑清晰;
- 可观测性 :日志包含策略码、耗时、输入摘要,排查问题时直接grep策略码即可。
3.4 策略注册与Spring集成:告别new关键字
策略实例不能手动 new ,必须由Spring容器管理,才能享受依赖注入、AOP增强等能力。我们采用两种注册方式:
方式一:自动扫描(推荐)
@Configuration
public class StrategyAutoConfiguration {
@Bean
@Primary
public CouponCalculationContext couponCalculationContext(
List<CouponCalculationStrategy> strategies) {
return new CouponCalculationContext(strategies);
}
}
只要策略类加了 @Component ,Spring就会自动收集到 List<CouponCalculationStrategy> 中。这是最简洁的方式,也是我们线上项目的标准做法。
方式二:显式注册(适合多模块)
如果策略分散在不同模块(如 coupon-core 、 coupon-marketing ),可在主模块中显式声明:
@Configuration
public class StrategyManualConfiguration {
@Bean
public CouponCalculationStrategy fullReductionStrategy() {
return new FullReductionStrategy();
}
@Bean
public CouponCalculationStrategy discountStrategy() {
return new DiscountStrategy();
}
@Bean
@Primary
public CouponCalculationContext couponCalculationContext(
FullReductionStrategy fullReductionStrategy,
DiscountStrategy discountStrategy) {
List<CouponCalculationStrategy> strategies = Arrays.asList(
fullReductionStrategy, discountStrategy);
return new CouponCalculationContext(strategies);
}
}
无论哪种方式,核心原则是: Context只依赖Strategy接口,不感知具体实现类 。这样未来新增 GiftStrategy (赠品策略)时,只需写实现类+加 @Component ,其他代码零修改。
4. 策略模式的进阶技巧与避坑指南:那些文档里不会写的实战经验
策略模式看似简单,但在线上环境会遇到一堆“文档里没写”的坑。下面分享我在6个Java项目中总结的独家经验,全是血泪教训换来的。
4.1 策略选择的性能陷阱:别让supports()成为性能杀手
supports() 方法看似只是个布尔判断,但如果在里面做数据库查询、远程调用,就会让策略路由变成性能瓶颈。某次我们发现优惠券计算平均耗时突增到800ms,排查发现 DiscountStrategy.supports() 里调用了 userService.isVip(userId) ,而这个接口RT高达400ms。
解决方案 :
- 预加载上下文 :在调用
execute()前,由上层服务(如OrderService)统一加载必要数据到context中,策略里直接取用; - 本地缓存 :对高频查询(如用户VIP状态),在
supports()里用Caffeine缓存,expireAfterWrite(10, TimeUnit.MINUTES); - 异步预检 :对严格要求实时性的场景(如风控),改用
CompletableFuture异步加载,supports()只做快速判断,真正校验延迟到calculate()中。
提示:
supports()方法必须满足“快、轻、无副作用”三原则。我们团队的红线是:单次调用不超过5ms,不能有IO,不能修改任何状态。
4.2 策略间的协同与组合:当一个订单需要多个策略时
真实业务中,一个订单可能同时使用满减+折扣+赠品,这时策略模式怎么用?答案是: 策略模式不解决组合问题,它解决的是“单个行为的多实现”,组合是上层Context的责任 。
我们设计了 CompositeCouponCalculationStrategy :
public class CompositeCouponCalculationStrategy implements CouponCalculationStrategy {
private final List<CouponCalculationStrategy> strategies;
public CompositeCouponCalculationStrategy(List<CouponCalculationStrategy> strategies) {
this.strategies = strategies;
}
@Override
public CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
// 按顺序执行多个策略,结果合并
CalculationResult result = CalculationResult.empty();
for (CouponCalculationStrategy strategy : strategies) {
CalculationResult partial = strategy.calculate(cart, coupon, context);
result = result.merge(partial); // 合并逻辑:金额相加、赠品去重等
}
return result;
}
@Override
public boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
// 只要有一个策略支持,就认为复合策略支持
return strategies.stream()
.anyMatch(s -> s.supports(cart, coupon, context));
}
@Override
public String getStrategyCode() {
return "COMPOSITE";
}
}
然后在Context中,当检测到订单含多个优惠券时,动态构建 CompositeCouponCalculationStrategy 。这样既保持了单一策略的纯粹性,又实现了灵活组合。
4.3 策略的版本管理与灰度发布:如何安全上线新算法
运营经常要求“新满减规则先对10%用户灰度”,这时不能停机发布。我们的方案是:
- 策略码带版本号 :
FULL_REDUCTION_V2、DISCOUNT_V3; - 路由规则外置 :将灰度规则配置在Apollo/Nacos中,如
coupon.strategy.route.rule=userId % 100 < 10 ? FULL_REDUCTION_V2 : FULL_REDUCTION_V1; - Context动态解析 :
findStrategy()方法读取配置,按规则选择策略码,再从strategyMap中获取实例。
这样新策略上线后,先配置1%灰度,观察监控指标(成功率、耗时、错误率),没问题再逐步放大。我们曾用此方案在双11前一周灰度上线新折扣算法,提前发现并发下锁竞争问题,避免了大促事故。
4.4 单元测试的黄金法则:每个策略必须有独立测试类
策略模式的测试重点不是“能不能跑”,而是“边界条件是否覆盖”。我们强制要求每个 ConcreteStrategy 对应一个 *StrategyTest ,且必须覆盖:
- ✅ 正常流程(满300减50,计算结果准确);
- ✅ 边界值(刚好300元、299.99元、0元订单);
- ✅ 异常场景(优惠券过期、金额为null、负数);
- ✅ 性能基准(单次计算耗时<5ms)。
以 FullReductionStrategyTest 为例:
@SpringBootTest
class FullReductionStrategyTest {
@Autowired
private FullReductionStrategy strategy;
@Test
void should_calculate_discount_when_amount_meets_threshold() {
// given
ShoppingCart cart = createCartWithAmount(new BigDecimal("300.00"));
Coupon coupon = createCoupon("FULL_REDUCTION");
// when
CalculationResult result = strategy.calculate(cart, coupon, Collections.emptyMap());
// then
assertThat(result.isUsable()).isTrue();
assertThat(result.getDiscountAmount()).isEqualTo(new BigDecimal("50.00"));
}
@Test
void should_not_apply_discount_when_amount_below_threshold() {
// given
ShoppingCart cart = createCartWithAmount(new BigDecimal("299.99"));
Coupon coupon = createCoupon("FULL_REDUCTION");
// when
CalculationResult result = strategy.calculate(cart, coupon, Collections.emptyMap());
// then
assertThat(result.isUsable()).isFalse();
assertThat(result.getReason()).contains("不足300元");
}
}
注意:测试中
createCartWithAmount()和createCoupon()必须是静态工厂方法,确保测试数据纯净。我们禁止在测试中使用new创建领域对象,全部通过工厂构造,避免测试数据污染。
4.5 监控与告警:让策略“看得见、管得住”
没有监控的策略模式就是定时炸弹。我们在Context中埋点,上报到Prometheus:
coupon_calculation_total{strategy="FULL_REDUCTION",result="success"}:成功次数;coupon_calculation_duration_seconds{strategy="DISCOUNT"}:耗时直方图;coupon_calculation_fallback_total{strategy="GIFT"}:熔断次数。
告警规则:
- 当
coupon_calculation_fallback_total5分钟内>10次,立即告警; - 当
coupon_calculation_duration_secondsP95>200ms,触发性能优化工单; - 当某策略
result="failure"占比连续10分钟>5%,自动禁用该策略(通过Apollo配置coupon.strategy.{code}.enabled=false)。
这套监控让我们在某次支付宝SDK升级后,10分钟内发现 AlipayStrategy 错误率飙升,及时回滚配置,避免了资损。
5. 策略模式 vs 其他设计模式:什么时候该用它?什么时候该换思路?
策略模式常被拿来和状态模式、模板方法、工厂模式对比。但实际选型不能只看UML图,要看 业务语义、变化频率、团队认知成本 。下面用真实案例说明。
5.1 策略模式 vs 状态模式:订单状态流转该用谁?
订单有“待支付”、“已支付”、“已发货”、“已完成”等状态,状态改变时行为不同(如“待支付”可取消,“已发货”不可取消)。很多人第一反应是状态模式,但这是误区。
正确选择:状态模式 。因为:
- 状态是 对象的内在属性 ,订单的状态改变会影响其所有行为;
- 状态间有 严格流转关系 (不能从“已完成”跳回“待支付”);
- 状态变更由 订单自身驱动 (
order.pay()触发状态机迁移)。
而策略模式适用于:
- 行为是 外部选择的 (运营配置用满减还是折扣);
- 实现间 完全独立、无状态依赖 ;
- 变更由 配置或参数驱动 ,而非对象内部状态。
实战经验:我们曾把订单状态机错误地设计成策略模式,结果状态迁移逻辑散落在12个策略里,新人根本看不懂流转规则。重构为状态模式后,用
StateMachine框架(如Spring State Machine)统一管理,代码量减少60%,可读性大幅提升。
5.2 策略模式 vs 模板方法:算法骨架固定时怎么选?
模板方法定义算法骨架,子类实现步骤。比如导出报表: export() 方法固定为“查数据→格式化→写文件”,子类只重写 formatData() 。
选择策略模式当 :
- 算法实现 完全独立 ,没有公共骨架;
- 需要 运行时动态切换 (如根据用户等级选不同导出格式);
- 实现类 可能来自不同模块或第三方 (如接入飞书、钉钉的推送策略)。
选择模板方法当 :
- 算法步骤 高度相似 ,只有少数步骤差异;
- 步骤间有 强依赖和顺序约束 (必须先查数据再格式化);
- 团队希望 强制规范实现流程 (所有子类必须实现
validateInput())。
5.3 策略模式 vs 规则引擎:复杂规则该不该上Drools?
当优惠规则变成“新用户首单满100减20,且非周末,且商品类目为美妆,且用户近30天未下单”时,策略模式还适用吗?
答案:视情况而定 。我们做过对比:
| 维度 | 策略模式 | Drools规则引擎 |
|---|---|---|
| 开发效率 | 高(Java代码,IDE友好) | 低(需学DRL语法,调试困难) |
| 运维成本 | 极低(和应用同生命周期) | 高(需单独部署、监控、版本管理) |
| 规则复杂度 | 适合≤5个条件的规则 | 适合≥10个条件的动态规则 |
| 变更频率 | 中低频(周级) | 高频(运营随时改) |
我们的决策树 :
- 如果规则由 开发写死 ,且半年内不会大改 → 用策略模式,加
@ConditionalOnProperty控制; - 如果规则由 运营后台配置 ,且每天调整 → 上Drools,但用策略模式封装Drools调用(
DroolsRuleStrategy),保持上层接口不变; - 如果规则 介于两者之间 (如季度调整)→ 用JSON配置+策略模式解析,如
{"type":"full_reduction","minAmount":100,"discount":20,"conditions":[{"field":"userType","op":"eq","value":"new"}]}。
最后分享一个心法: 设计模式是解决问题的工具,不是炫技的勋章 。我在某项目评审会上,看到同事为“发送短信”写了5个策略(阿里云、腾讯云、华为云、自建SMSC、Mock),只为体现“开闭原则”。但实际业务中99%流量走阿里云,其他全是备用,最后我们砍掉4个策略,用 AliyunSmsStrategy + FallbackSmsStrategy 搞定,代码更少,故障面更小,监控更聚焦。记住:简单、可靠、可维护,永远是第一优先级。
更多推荐

所有评论(0)