Java 策略模式中"设置策略"的深入解析
策略模式的核心在于将算法封装成独立对象,并通过上下文(Context)组合这些对象,从而实现算法的动态替换 。其中 "设置策略" 这一操作是连接客户端与具体算法的桥梁,也是整个模式能够灵活运转的关键。下面从概念、作用、运行机制、实际场景四个维度深入分析。
一、概念:什么是"设置策略"?
"设置策略"指上下文(Context)对外提供一种方式,使客户端能够将某个具体的策略对象(实现了策略接口)注入到上下文中。此后,上下文在执行核心方法时,会将工作委派给当前所持有的策略对象。
从代码形式上看,通常表现为:
-
构造函数注入:在创建上下文时传入初始策略。
-
Setter 方法注入 :提供
setStrategy(Strategy s)方法,允许运行时动态更改策略。 -
方法参数注入:直接在调用上下文的核心方法时传入策略(不常单独使用,常与前面结合)。
二、作用:为什么要单独强调"设置策略"?
| 作用 | 说明 |
|---|---|
| 实现运行时动态切换 | 不用销毁上下文对象,只需调用 setStrategy() 即可更换算法,适用于用户交互、环境变化等场景。 |
| 解耦客户端与具体算法 | 客户端只需知道策略接口,无需关心算法实现细节;通过设置策略完成依赖注入。 |
| 遵循开闭原则 | 新增算法只需新增策略类,客户端通过设置新策略即可使用,上下文代码零修改。 |
| 增强测试性 | 可以方便地注入 Mock 策略对象进行单元测试,隔离真实算法。 |
三、运行机制与底层原理
3.1 依赖关系图
客户端 (Client)
│
│ 1. 创建具体策略对象 │ 2. 设置策略(setStrategy)
▼ ▼
上下文 (Context) ────持有────► 策略接口 (Strategy)
│ ▲
│ 3. 执行算法 │ 4. 多态调用
▼ │
execute() ──委派调用──→ 具体策略 (ConcreteStrategyA/B...)
3.2 核心运行步骤(以 Setter 注入为例)
// 1. 策略接口
public interface DiscountStrategy {
double applyDiscount(double price);
}
// 2. 具体策略(满减、折扣)
public class FullReductionStrategy implements DiscountStrategy {
private double threshold;
private double reduction;
public FullReductionStrategy(double threshold, double reduction) { ... }
public double applyDiscount(double price) {
return price >= threshold ? price - reduction : price;
}
}
public class PercentageStrategy implements DiscountStrategy {
private double percent; // 0~1
public double applyDiscount(double price) { return price * (1 - percent); }
}
// 3. 上下文
public class ShoppingCart {
private DiscountStrategy strategy;
// 设置策略(关键操作)
public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}
public double checkout(double originalPrice) {
if (strategy == null) throw new IllegalStateException("未设置折扣策略");
return strategy.applyDiscount(originalPrice);
}
}
// 4. 客户端
public class Client {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// 设置满减策略
cart.setStrategy(new FullReductionStrategy(300, 50));
System.out.println(cart.checkout(350)); // 输出 300
// 动态切换为打折策略
cart.setStrategy(new PercentageStrategy(0.2));
System.out.println(cart.checkout(350)); // 输出 280
}
}
3.3 底层机制详析
-
多态绑定
-
上下文持有策略接口类型的引用(如
DiscountStrategy)。 -
客户端传入的具体策略对象实现了接口,Java 运行时通过动态绑定调用实际的方法。
-
-
组合优于继承
- 上下文 包含 策略对象(Has-A 关系),而不是通过继承算法基类来获得行为。这样可以在运行时改变"行为",而继承是静态的。
-
委派模型
- 上下文本身不实现算法,当
checkout()被调用时,它将计算工作委派 给所持有的策略对象的applyDiscount()方法。这符合"少用继承多用组合"和"委托优于实现"的原则。
- 上下文本身不实现算法,当
-
对象间的松耦合
- 策略对象独立于上下文,可以单独测试、修改、甚至动态创建(如通过工厂模式)。上下文只依赖抽象接口,不依赖具体实现。
3.4 不同"设置策略"方式的机制差异
| 注入方式 | 代码示例 | 运行机制特点 | 适用场景 |
|---|---|---|---|
| 构造函数注入 | new Context(strategy) |
策略在上下文创建时固定,不可变 | 策略在整个生命周期不变 |
| Setter 方法注入 | context.setStrategy(new Strategy()) |
可以随时替换策略,上下文保持同一实例 | 需要动态切换策略(如用户选择支付方式) |
| 方法参数注入 | context.execute(new Strategy(), data) |
每次执行都传入策略,上下文不保存状态 | 策略仅单次使用,无状态 |
| 配置/注解注入 | @Autowired + @Qualifier(Spring) |
容器管理策略对象的生命周期和注入 | 企业级应用,依赖 IoC 容器 |
四、实际场景深度分析
场景 1:电商订单价格计算(多折扣叠加之前的策略选择)
需求:用户下单时,系统根据用户等级、优惠券、活动类型等选择一种最优折扣策略(如会员折扣、满减券、限时打折)。
设置策略的时机:
-
用户在结算页面切换优惠方式时,后端接收到请求,动态调用
order.setDiscountStrategy()切换策略。 -
如果不使用策略模式,会写出大量
if (type==1){...} else if (type==2){...}。
运行机制:
-
策略接口:
PriceCalculator -
具体策略:
VipDiscount,CouponDiscount,FlashSaleDiscount -
上下文:
OrderContext,持有策略引用,提供calculateTotal()方法。 -
客户端(Controller)根据用户选择的优惠类型,从工厂获取对应策略,然后设置到订单上下文中。
代码骨架示例:
// 策略接口
public interface PriceCalculator {
BigDecimal calculate(Order order);
}
// 上下文
public class OrderContext {
private PriceCalculator calculator;
public void setCalculator(PriceCalculator calculator) { this.calculator = calculator; }
public BigDecimal finalPrice(Order order) {
return calculator.calculate(order);
}
}
// Controller 层
@PostMapping("/checkout")
public Result checkout(@RequestBody CheckoutParam param) {
OrderContext context = new OrderContext();
PriceCalculator strategy = strategyFactory.getStrategy(param.getPromotionType());
context.setCalculator(strategy);
BigDecimal finalPrice = context.finalPrice(order);
// ...
}
场景 2:文件导出格式选择(Excel、PDF、CSV)
需求:报表系统允许用户选择导出格式,点击按钮后生成对应格式的文件。
设置策略的时机:
- 用户在前端下拉框选择"导出为 PDF"或"导出为 Excel",后端收到请求后,根据格式字符串创建对应的导出策略,设置到导出上下文中。
优势:
- 新增一种导出格式(如 Word)只需要添加新的策略类和一个工厂分支,完全不需要修改导出流程代码。
场景 3:数据校验流水线(配合责任链模式)
虽然策略模式本身不强调顺序,但在校验场景中,常常动态设置校验策略:
public class ValidatorContext {
private ValidationStrategy strategy;
public void setStrategy(ValidationStrategy strategy) { this.strategy = strategy; }
public boolean validate(String input) { return strategy.validate(input); }
}
// 客户端根据字段类型设置不同校验策略
if (fieldType.equals("email")) {
validator.setStrategy(new EmailValidationStrategy());
} else if (fieldType.equals("phone")) {
validator.setStrategy(new PhoneValidationStrategy());
}
场景 4:游戏角色攻击方式
需求:游戏角色可以切换武器(剑、弓、法杖),每种武器有不同的攻击算法。
设置策略的时机:
- 玩家按快捷键或打开装备栏换武器时,游戏调用
character.setWeaponStrategy(new BowStrategy())。
运行机制:
-
角色类(上下文)持有
AttackStrategy接口。 -
当角色攻击时,
character.attack()内部调用currentStrategy.attack()。 -
换武器只需调用
setStrategy(),角色行为立即改变,无需重建角色对象。
五、高级话题:设置策略时的注意事项
5.1 线程安全
如果上下文被多线程共享,且策略对象是可变的(有状态),那么动态设置策略可能导致线程安全问题。解决方案:
-
使用不可变策略对象。
-
每次切换策略时创建新的上下文实例(请求作用域)。
-
加锁或使用
volatile(如果策略引用需要立即可见)。
5.2 与工厂模式结合
避免客户端直接 new 具体策略,造成紧耦合。通常会:
-
创建
StrategyFactory,根据类型枚举或字符串返回对应的策略实例。 -
客户端只需传入类型标识,工厂负责创建策略对象并注入(或返回给客户端自行设置)。
5.3 策略缓存与享元模式
如果策略对象是无状态的,可以设计为单例或享元,减少对象创建开销。设置策略时复用同一个实例。
5.4 空策略模式(Null Strategy)
为了避免上下文中的策略为 null,可以定义一个 NullStrategy 实现策略接口,其方法不做任何操作或返回默认值。设置策略时默认注入该策略。
六、总结
| 维度 | 核心要点 |
|---|---|
| 概念 | 设置策略是向上下文注入具体算法对象的行为,通常通过构造函数、Setter 或参数传递实现。 |
| 作用 | 运行时动态切换算法、解耦客户端与实现、支持开闭原则、便于测试。 |
| 运行机制 | 基于多态、组合、委派;上下文保存策略接口引用,执行时委派给策略对象的具体方法。 |
| 实际场景 | 支付方式切换、折扣计算、文件导出、数据校验、游戏角色武器切换等。 |
| 最佳实践 | 与工厂模式结合、考虑线程安全、使用不可变策略、空策略避免 null。 |
理解"设置策略"这一动作的底层机制,能够帮助你在实际开发中更灵活地运用策略模式,写出易扩展、可维护的代码。当遇到算法族频繁变化或需要消除长串条件判断时,记得想起"向上下文设置一个策略"这个简单的操作。