1. 策略模式初探:为什么我们需要它?
作为一名Java开发者,我经常看到新手在面对多种业务逻辑分支时,本能地使用if-else堆砌代码。这种写法虽然直观,但随着业务复杂度增加,代码会变得难以维护。策略模式正是为了解决这个问题而生的。
1.1 传统if-else的痛点
让我们看一个电商运费计算的例子。假设我们有三种用户类型:
- 普通用户:按重量计费(10元/公斤)
- VIP用户:包邮
- 团购订单:满200减20
传统实现可能是这样的:
java复制public double calculateFreight(String userType, double weight, double amount) {
if ("normal".equals(userType)) {
return weight * 10;
} else if ("vip".equals(userType)) {
return 0;
} else if ("group".equals(userType)) {
return amount >= 200 ? 0 : 15;
}
throw new IllegalArgumentException("未知用户类型");
}
这种写法存在几个明显问题:
- 每次新增用户类型都需要修改这个方法,违反开闭原则
- 所有逻辑耦合在一起,难以单独测试
- 方法会越来越长,可读性下降
- 业务规则变更时需要修改核心逻辑
1.2 策略模式的优势
策略模式通过将算法封装到独立的类中,使它们可以互相替换。这样做的优势包括:
- 解耦:业务逻辑与具体实现分离
- 可扩展:新增策略只需添加新类,无需修改现有代码
- 可测试:每个策略可以单独测试
- 可维护:代码结构更清晰,职责更明确
提示:策略模式特别适合处理那些在运行时需要根据不同条件选择不同算法的场景。比如支付方式选择、日志输出策略、排序算法切换等。
2. 环境准备与项目搭建
2.1 开发环境要求
在开始编码前,我们需要确保开发环境准备就绪:
- JDK:建议使用JDK 8或更高版本(推荐JDK 17)
- IDE:IntelliJ IDEA(社区版免费)或VS Code + Java插件
- 构建工具:Maven(IDE通常自带)
2.2 创建Maven项目
在IntelliJ IDEA中创建新项目的步骤:
- 选择File → New → Project
- 选择Maven项目类型
- 填写GroupId(如com.example)和ArtifactId(如strategy-demo)
- 确认Java版本设置为17(或你使用的版本)
2.3 项目结构规划
一个良好的项目结构有助于代码维护。我们的策略模式实现将采用以下结构:
code复制src/main/java/com/example/strategy/
├── FreightStrategy.java // 策略接口
├── NormalFreightStrategy.java // 普通用户策略
├── VipFreightStrategy.java // VIP用户策略
├── GroupFreightStrategy.java // 团购策略
├── FreightContext.java // 上下文类
└── App.java // 主程序
3. 策略模式基础实现
3.1 定义策略接口
首先,我们需要定义一个策略接口,规定所有具体策略必须实现的方法:
java复制public interface FreightStrategy {
/**
* 计算运费
* @param weight 商品重量(公斤)
* @param amount 订单金额
* @return 运费金额
*/
double calculate(double weight, double amount);
}
这个接口定义了一个契约:所有运费计算策略都必须提供calculate方法。
3.2 实现具体策略
接下来,我们为每种用户类型实现具体的策略类:
java复制// 普通用户策略
public class NormalFreightStrategy implements FreightStrategy {
private static final double PRICE_PER_KG = 10.0;
@Override
public double calculate(double weight, double amount) {
return weight * PRICE_PER_KG;
}
}
// VIP用户策略
public class VipFreightStrategy implements FreightStrategy {
@Override
public double calculate(double weight, double amount) {
return 0.0; // VIP包邮
}
}
// 团购策略
public class GroupFreightStrategy implements FreightStrategy {
private static final double DEFAULT_FREIGHT = 15.0;
private static final double FREE_THRESHOLD = 200.0;
@Override
public double calculate(double weight, double amount) {
return amount >= FREE_THRESHOLD ? 0.0 : DEFAULT_FREIGHT;
}
}
每个策略类都专注于自己的业务规则,职责单一,易于理解和测试。
3.3 创建上下文类
上下文类负责维护对策略对象的引用,并将计算请求委托给当前策略:
java复制public class FreightContext {
private FreightStrategy strategy;
// 设置当前策略
public void setStrategy(FreightStrategy strategy) {
this.strategy = strategy;
}
// 执行计算
public double executeCalculate(double weight, double amount) {
if (strategy == null) {
throw new IllegalStateException("请先设置运费计算策略");
}
return strategy.calculate(weight, amount);
}
}
上下文类不关心具体使用哪种策略,它只负责调用当前设置的策略。
3.4 使用示例
现在我们可以这样使用策略模式:
java复制public class App {
public static void main(String[] args) {
FreightContext context = new FreightContext();
// 普通用户订单:5公斤,300元
context.setStrategy(new NormalFreightStrategy());
System.out.println("普通用户运费:" + context.executeCalculate(5.0, 300.0));
// VIP用户订单:5公斤,300元
context.setStrategy(new VipFreightStrategy());
System.out.println("VIP用户运费:" + context.executeCalculate(5.0, 300.0));
// 团购订单:3公斤,150元
context.setStrategy(new GroupFreightStrategy());
System.out.println("团购运费:" + context.executeCalculate(3.0, 150.0));
}
}
输出结果:
code复制普通用户运费:50.0
VIP用户运费:0.0
团购运费:15.0
4. 策略模式进阶应用
4.1 策略工厂模式
手动创建策略实例不够优雅,我们可以引入工厂模式来管理策略:
java复制public class StrategyFactory {
private static final Map<String, FreightStrategy> strategies = new HashMap<>();
static {
strategies.put("normal", new NormalFreightStrategy());
strategies.put("vip", new VipFreightStrategy());
strategies.put("group", new GroupFreightStrategy());
}
public static FreightStrategy getStrategy(String type) {
FreightStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException("未知的策略类型: " + type);
}
return strategy;
}
}
使用方式:
java复制context.setStrategy(StrategyFactory.getStrategy("vip"));
4.2 枚举策略模式
结合枚举可以让代码更安全、更优雅:
java复制public enum FreightType {
NORMAL(new NormalFreightStrategy()),
VIP(new VipFreightStrategy()),
GROUP(new GroupFreightStrategy());
private final FreightStrategy strategy;
FreightType(FreightStrategy strategy) {
this.strategy = strategy;
}
public FreightStrategy getStrategy() {
return strategy;
}
}
使用方式:
java复制context.setStrategy(FreightType.VIP.getStrategy());
4.3 策略模式与Spring集成
在Spring应用中,我们可以利用依赖注入来管理策略:
java复制@Service
public class FreightService {
private final Map<String, FreightStrategy> strategies;
@Autowired
public FreightService(List<FreightStrategy> strategyList) {
strategies = strategyList.stream()
.collect(Collectors.toMap(
s -> s.getClass().getSimpleName().replace("FreightStrategy", "").toLowerCase(),
Function.identity()
));
}
public double calculateFreight(String type, double weight, double amount) {
FreightStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException("未知的运费策略: " + type);
}
return strategy.calculate(weight, amount);
}
}
5. 策略模式最佳实践与注意事项
5.1 何时使用策略模式
策略模式最适合以下场景:
- 一个系统需要在多种算法中选择一种
- 算法需要在运行时动态切换
- 需要避免使用多重条件判断语句
- 需要隔离算法实现细节,使业务逻辑更清晰
5.2 策略模式的优势与局限
| 优势 | 局限 |
|---|---|
| 避免条件语句 | 增加了类的数量 |
| 易于扩展新策略 | 客户端需要了解不同策略 |
| 提高代码复用 | 策略间共享数据较复杂 |
| 便于单元测试 | 可能增加对象创建开销 |
5.3 常见问题与解决方案
问题1:策略间需要共享数据怎么办?
解决方案:将共享数据封装到Context对象中传递给策略:
java复制public interface FreightStrategy {
double calculate(FreightContextData data);
}
public class FreightContextData {
private double weight;
private double amount;
// 其他共享数据...
// getters/setters
}
问题2:如何避免策略类过多?
解决方案:
- 使用lambda表达式简化简单策略
- 将相关策略组合到一个类中
- 考虑是否真的需要策略模式
问题3:如何选择策略?
解决方案:
- 使用工厂模式或枚举管理策略
- 可以结合配置文件动态选择策略
- 在Spring中可以使用@Conditional注解
6. 策略模式与其他设计模式的关系
6.1 策略模式 vs 状态模式
虽然结构相似,但目的不同:
- 策略模式:封装可互换的算法,客户端主动选择策略
- 状态模式:对象内部状态改变导致行为变化,状态转换通常由状态类自身控制
6.2 策略模式 vs 模板方法模式
- 策略模式:通过对象组合实现算法变化
- 模板方法:通过类继承实现算法部分变化
6.3 策略模式与工厂模式的结合
常见组合方式:
- 使用工厂创建策略实例
- 策略工厂可以根据运行时条件返回不同策略
- 在Spring中,可以使用策略工厂管理所有策略bean
7. 实际项目中的应用建议
7.1 性能考虑
如果策略对象是无状态的,可以考虑使用单例模式来减少对象创建开销:
java复制public enum SingletonStrategy implements FreightStrategy {
INSTANCE;
@Override
public double calculate(double weight, double amount) {
// 实现代码
}
}
7.2 测试策略
策略模式使单元测试更容易,因为每个策略可以单独测试:
java复制public class NormalFreightStrategyTest {
@Test
public void testCalculate() {
FreightStrategy strategy = new NormalFreightStrategy();
assertEquals(50.0, strategy.calculate(5.0, 300.0), 0.001);
}
}
7.3 文档化策略
为每个策略添加清晰的文档说明其适用场景:
java复制/**
* VIP用户运费策略
* 特点:
* - 无论重量和金额多少,一律免运费
* - 适用于标记为VIP的用户
*/
public class VipFreightStrategy implements FreightStrategy {
// 实现代码
}
8. 从策略模式看设计原则
策略模式很好地体现了几个重要的面向对象设计原则:
8.1 开闭原则(OCP)
对扩展开放,对修改关闭。新增策略只需添加新类,无需修改现有代码。
8.2 单一职责原则(SRP)
每个策略类只负责一种算法实现,职责单一。
8.3 依赖倒置原则(DIP)
高层模块(Context)不依赖低层模块(具体策略),二者都依赖抽象(Strategy接口)。
8.4 接口隔离原则(ISP)
策略接口应该保持精简,只包含必要的方法。
9. 重构实战:将if-else改为策略模式
让我们看一个实际的重构例子。假设我们有以下代码:
java复制public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if ("creditcard".equals(paymentType)) {
// 处理信用卡支付
System.out.println("处理信用卡支付: " + amount);
} else if ("paypal".equals(paymentType)) {
// 处理PayPal支付
System.out.println("处理PayPal支付: " + amount);
} else if ("banktransfer".equals(paymentType)) {
// 处理银行转账
System.out.println("处理银行转账: " + amount);
} else {
throw new IllegalArgumentException("未知支付方式");
}
}
}
重构步骤:
- 定义支付策略接口:
java复制public interface PaymentStrategy {
void processPayment(double amount);
}
- 实现具体策略:
java复制public class CreditCardStrategy implements PaymentStrategy {
@Override
public void processPayment(double amount) {
System.out.println("处理信用卡支付: " + amount);
}
}
public class PayPalStrategy implements PaymentStrategy {
@Override
public void processPayment(double amount) {
System.out.println("处理PayPal支付: " + amount);
}
}
public class BankTransferStrategy implements PaymentStrategy {
@Override
public void processPayment(double amount) {
System.out.println("处理银行转账: " + amount);
}
}
- 创建支付上下文:
java复制public class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void executePayment(double amount) {
strategy.processPayment(amount);
}
}
- 使用方式:
java复制PaymentContext context = new PaymentContext();
context.setStrategy(new CreditCardStrategy());
context.executePayment(100.0);
重构后的代码更加灵活,易于扩展新的支付方式。
10. 策略模式的变体与扩展
10.1 使用函数式接口简化策略
在Java 8+中,可以使用函数式接口简化简单策略:
java复制@FunctionalInterface
public interface FreightStrategy {
double calculate(double weight, double amount);
}
// 使用lambda表达式创建策略
FreightStrategy normalStrategy = (weight, amount) -> weight * 10;
FreightStrategy vipStrategy = (weight, amount) -> 0;
10.2 策略模式与注解结合
可以自定义注解来标记策略实现:
java复制@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FreightStrategyType {
String value();
}
@FreightStrategyType("normal")
public class NormalFreightStrategy implements FreightStrategy {
// 实现代码
}
然后通过反射发现所有策略类。
10.3 组合策略模式
有时需要组合多个策略:
java复制public class CompositeFreightStrategy implements FreightStrategy {
private final List<FreightStrategy> strategies;
public CompositeFreightStrategy(List<FreightStrategy> strategies) {
this.strategies = strategies;
}
@Override
public double calculate(double weight, double amount) {
double total = 0;
for (FreightStrategy strategy : strategies) {
total += strategy.calculate(weight, amount);
}
return total;
}
}
11. 策略模式在不同业务场景的应用
11.1 电商系统中的策略模式应用
- 促销活动:满减、折扣、赠品等不同促销策略
- 运费计算:基于地区、重量、商品类型的多种运费策略
- 支付方式:信用卡、支付宝、微信支付等不同支付处理策略
- 会员积分:不同会员等级使用不同的积分计算策略
11.2 金融系统中的策略模式应用
- 风险评估:不同风险等级使用不同的评估策略
- 利息计算:定期、活期、贷款等不同利息计算策略
- 交易费用:不同交易类型使用不同的手续费计算策略
11.3 游戏开发中的策略模式应用
- AI行为:不同NPC使用不同的行为策略
- 伤害计算:不同武器使用不同的伤害计算策略
- 经验值计算:不同任务使用不同的经验值奖励策略
12. 策略模式的性能优化
12.1 策略对象的创建开销
如果策略对象创建成本高,可以考虑:
- 使用对象池
- 将策略实现为单例
- 使用享元模式共享策略对象
12.2 策略选择的性能优化
频繁的策略选择可能成为性能瓶颈,可以:
- 缓存常用策略
- 使用更高效的策略查找方式(如数组索引代替Map查找)
- 预编译策略选择逻辑
12.3 多线程环境下的策略模式
确保策略对象是线程安全的:
- 无状态策略天然线程安全
- 有状态策略需要同步或使用ThreadLocal
- 考虑使用不可变对象
13. 策略模式的测试策略
13.1 单元测试策略实现
每个策略类应该有自己的单元测试:
java复制public class NormalFreightStrategyTest {
@Test
public void testCalculate() {
FreightStrategy strategy = new NormalFreightStrategy();
assertEquals(50.0, strategy.calculate(5.0, 100.0), 0.001);
}
}
13.2 测试上下文类
测试上下文类是否正确委托给策略:
java复制public class FreightContextTest {
@Test
public void testExecuteCalculate() {
FreightContext context = new FreightContext();
context.setStrategy(weight -> weight * 10); // 使用lambda简化测试
assertEquals(50.0, context.executeCalculate(5.0, 0), 0.001);
}
}
13.3 集成测试策略工厂
测试工厂是否能正确创建和返回策略:
java复制public class StrategyFactoryTest {
@Test
public void testGetStrategy() {
FreightStrategy strategy = StrategyFactory.getStrategy("normal");
assertTrue(strategy instanceof NormalFreightStrategy);
}
}
14. 策略模式的常见误用与避免方法
14.1 过度设计
问题:简单逻辑也使用策略模式,导致代码复杂化。
解决:只有当一个行为有多个变体,且可能增加或变化时,才考虑策略模式。
14.2 策略膨胀
问题:策略类过多,难以管理。
解决:
- 合并相关策略
- 使用组合模式管理策略
- 考虑是否真的需要这么多策略
14.3 上下文过重
问题:上下文类承担了太多职责。
解决:确保上下文只负责维护和委托策略,不包含业务逻辑。
15. 策略模式在Java标准库中的应用
Java标准库中有许多策略模式的例子:
- Comparator:定义不同的排序策略
- ThreadPoolExecutor的RejectedExecutionHandler:定义任务拒绝策略
- LayoutManager:定义不同的布局策略
- FileFilter和FilenameFilter:定义文件过滤策略
例如,使用Comparator实现不同的排序策略:
java复制List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 按长度排序策略
names.sort((a, b) -> a.length() - b.length());
// 按字母顺序排序策略
names.sort(String::compareTo);
16. 策略模式与Lambda表达式
Java 8的lambda表达式使策略模式更加简洁:
java复制// 传统方式
context.setStrategy(new FreightStrategy() {
@Override
public double calculate(double weight, double amount) {
return weight * 5 + amount * 0.1;
}
});
// 使用lambda表达式
context.setStrategy((weight, amount) -> weight * 5 + amount * 0.1);
对于简单策略,可以直接使用lambda表达式,避免创建单独的类。
17. 策略模式与Spring框架的集成
在Spring应用中,可以更优雅地管理策略:
- 定义策略接口:
java复制public interface DiscountStrategy {
double applyDiscount(double originalPrice);
}
- 实现策略并标记为Spring组件:
java复制@Service("vipDiscount")
public class VipDiscountStrategy implements DiscountStrategy {
@Override
public double applyDiscount(double originalPrice) {
return originalPrice * 0.9; // VIP打9折
}
}
@Service("seasonalDiscount")
public class SeasonalDiscountStrategy implements DiscountStrategy {
@Override
public double applyDiscount(double originalPrice) {
return originalPrice * 0.8; // 季节性8折
}
}
- 使用策略:
java复制@Service
public class DiscountService {
private final Map<String, DiscountStrategy> strategies;
@Autowired
public DiscountService(Map<String, DiscountStrategy> strategyMap) {
this.strategies = strategyMap;
}
public double applyDiscount(String strategyName, double price) {
DiscountStrategy strategy = strategies.get(strategyName);
if (strategy == null) {
throw new IllegalArgumentException("未知的折扣策略: " + strategyName);
}
return strategy.applyDiscount(price);
}
}
18. 策略模式在微服务架构中的应用
在微服务架构中,策略模式可以用于:
- 服务调用策略:根据条件选择不同的服务调用方式(直接调用、通过网关、降级处理)
- 负载均衡策略:轮询、随机、权重等不同负载均衡策略
- 容错策略:失败重试、快速失败、降级等不同容错策略
- 数据同步策略:全量同步、增量同步、定时同步等不同数据同步策略
例如,定义一个服务调用策略:
java复制public interface ServiceInvocationStrategy {
Response invokeService(Request request);
}
public class DirectInvocationStrategy implements ServiceInvocationStrategy {
@Override
public Response invokeService(Request request) {
// 直接调用服务
}
}
public class CircuitBreakerInvocationStrategy implements ServiceInvocationStrategy {
@Override
public Response invokeService(Request request) {
// 带熔断机制的服务调用
}
}
19. 策略模式与设计模式组合使用
策略模式常与其他模式组合使用:
- 策略+工厂模式:工厂负责创建策略实例
- 策略+模板方法模式:模板方法定义算法骨架,策略实现具体步骤
- 策略+装饰器模式:装饰器动态添加策略功能
- 策略+组合模式:组合多个策略形成新策略
例如,策略模式与模板方法模式结合:
java复制public abstract class PaymentProcessor {
// 模板方法
public final void processPayment(double amount) {
validate(amount);
executePayment(amount);
logPayment(amount);
}
protected abstract void executePayment(double amount);
protected void validate(double amount) {
if (amount <= 0) throw new IllegalArgumentException("金额必须大于0");
}
protected void logPayment(double amount) {
System.out.println("支付金额: " + amount);
}
}
public class CreditCardProcessor extends PaymentProcessor {
@Override
protected void executePayment(double amount) {
System.out.println("信用卡支付: " + amount);
}
}
20. 策略模式的演进与替代方案
随着编程语言和范式的发展,策略模式也有一些演进和替代方案:
- 函数式编程:使用高阶函数代替策略接口
- 规则引擎:对于复杂策略,可以使用Drools等规则引擎
- 策略即服务:将策略实现为独立微服务
- 配置化策略:通过配置文件定义策略行为
例如,使用函数式编程简化策略模式:
java复制public class FreightCalculator {
private DoubleBinaryOperator strategy;
public void setStrategy(DoubleBinaryOperator strategy) {
this.strategy = strategy;
}
public double calculate(double weight, double amount) {
return strategy.applyAsDouble(weight, amount);
}
}
// 使用
FreightCalculator calculator = new FreightCalculator();
calculator.setStrategy((weight, amount) -> weight * 10);
double freight = calculator.calculate(5.0, 100.0);
在实际项目中,我经常发现策略模式的最佳使用时机是在第一次需要添加新的业务规则变体时。与其直接添加if-else分支,不如考虑引入策略模式,为未来的扩展做好准备。