1. 策略模式初探:为什么我们需要它?
作为一名Java开发者,我经常遇到这样的场景:系统需要根据不同的条件执行不同的算法或业务逻辑。最初,我习惯性地使用if-else或switch-case来解决这类问题,但随着业务复杂度的增加,这种做法的弊端逐渐显现。
想象一下电商系统中的运费计算场景:普通用户按重量计费,VIP用户享受包邮,企业客户则根据订单金额阶梯减免。如果用传统方式实现,代码会变成这样:
java复制public double calculateFreight(String userType, double weight, double amount) {
if ("VIP".equals(userType)) {
return 0;
} else if ("ENTERPRISE".equals(userType)) {
if (amount >= 10000) return 0;
else if (amount >= 5000) return 20;
else return 50;
} else {
return weight * 10;
}
}
这种实现方式存在几个明显问题:
- 代码可读性差,逻辑混杂在一起
- 新增用户类型时需要修改原有方法,违反开闭原则
- 难以进行单元测试,需要覆盖所有分支
- 业务逻辑无法复用
1.1 策略模式的定义与价值
策略模式(Strategy Pattern)正是为解决这类问题而生。它的核心思想是:定义一组算法,将它们分别封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。
在实际项目中,策略模式带来的好处包括:
- 提高代码的可维护性和可扩展性
- 便于单元测试和代码复用
- 支持运行时动态切换算法
- 符合单一职责原则和开闭原则
提示:策略模式特别适用于那些需要在不同条件下应用不同业务规则的场景,如支付方式选择、促销策略计算、日志级别处理等。
2. 环境准备与基础实现
2.1 开发环境配置
要实践策略模式,你只需要准备最基本的Java开发环境:
- JDK 8或更高版本(推荐JDK 17)
- 任意Java IDE(IntelliJ IDEA、Eclipse或VS Code)
验证环境是否配置正确:
bash复制java -version
javac -version
如果遇到环境问题,常见解决方案如下:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Error: Could not find or load main class | 类名与文件名不一致或运行目录错误 | 确保.java文件名与public类名一致,在项目根目录执行 |
| NoClassDefFoundError | main方法签名错误或缺失 | 检查public static void main(String[] args)的拼写 |
| IDE报红但代码正确 | 项目配置问题 | 刷新项目依赖,检查JDK版本设置 |
2.2 基础策略模式实现
让我们用运费计算案例来演示策略模式的基本实现:
2.2.1 定义策略接口
首先创建一个表示运费计算策略的接口:
java复制public interface FreightStrategy {
double calculate(double weight, double amount);
}
2.2.2 实现具体策略
为每种用户类型实现具体的策略类:
java复制// 普通用户策略
public class NormalStrategy implements FreightStrategy {
@Override
public double calculate(double weight, double amount) {
return weight * 10; // 10元/公斤
}
}
// VIP用户策略
public class VipStrategy implements FreightStrategy {
@Override
public double calculate(double weight, double amount) {
return 0; // VIP包邮
}
}
// 企业客户策略
public class EnterpriseStrategy implements FreightStrategy {
@Override
public double calculate(double weight, double amount) {
if (amount >= 10000) return 0;
if (amount >= 5000) return 20;
return 50;
}
}
2.2.3 创建上下文类
上下文类负责持有策略引用并执行计算:
java复制public class FreightCalculator {
private FreightStrategy strategy;
public void setStrategy(FreightStrategy strategy) {
this.strategy = strategy;
}
public double execute(double weight, double amount) {
if (strategy == null) {
throw new IllegalStateException("请先设置运费策略");
}
return strategy.calculate(weight, amount);
}
}
2.2.4 客户端使用示例
java复制public class Main {
public static void main(String[] args) {
FreightCalculator calculator = new FreightCalculator();
// 普通用户订单
calculator.setStrategy(new NormalStrategy());
System.out.println("普通用户运费:" + calculator.execute(1.5, 299));
// VIP用户订单
calculator.setStrategy(new VipStrategy());
System.out.println("VIP用户运费:" + calculator.execute(2.0, 899));
// 企业客户订单
calculator.setStrategy(new EnterpriseStrategy());
System.out.println("企业客户运费:" + calculator.execute(5.0, 12000));
}
}
运行结果:
code复制普通用户运费:15.0
VIP用户运费:0.0
企业客户运费:0.0
3. 策略模式进阶技巧
3.1 使用Map管理策略
当策略较多时,可以使用Map来集中管理策略,避免分散的if-else判断:
java复制public class StrategyRegistry {
private static final Map<String, FreightStrategy> strategies = new HashMap<>();
static {
strategies.put("NORMAL", new NormalStrategy());
strategies.put("VIP", new VipStrategy());
strategies.put("ENTERPRISE", new EnterpriseStrategy());
}
public static FreightStrategy getStrategy(String type) {
return strategies.getOrDefault(type.toUpperCase(), new NormalStrategy());
}
}
// 使用方式
calculator.setStrategy(StrategyRegistry.getStrategy("VIP"));
3.2 结合枚举使用
枚举提供了更类型安全的方式来管理策略:
java复制public enum FreightType {
NORMAL(new NormalStrategy()),
VIP(new VipStrategy()),
ENTERPRISE(new EnterpriseStrategy());
private final FreightStrategy strategy;
FreightType(FreightStrategy strategy) {
this.strategy = strategy;
}
public FreightStrategy getStrategy() {
return strategy;
}
}
// 使用方式
calculator.setStrategy(FreightType.VIP.getStrategy());
3.3 策略模式与Spring集成
在实际企业应用中,我们通常会将策略模式与Spring框架结合:
java复制// 定义策略接口
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// 实现具体策略
@Service("creditCardPayment")
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
// 信用卡支付逻辑
}
}
@Service("alipayPayment")
public class AlipayPayment implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
// 支付宝支付逻辑
}
}
// 策略上下文
@Service
public class PaymentContext {
@Autowired
private Map<String, PaymentStrategy> strategies;
public void executePayment(String paymentType, BigDecimal amount) {
PaymentStrategy strategy = strategies.get(paymentType + "Payment");
if (strategy != null) {
strategy.pay(amount);
} else {
throw new IllegalArgumentException("不支持的支付方式");
}
}
}
4. 策略模式的最佳实践与陷阱
4.1 何时使用策略模式
策略模式最适合以下场景:
- 系统需要在不同条件下使用不同的算法或业务规则
- 算法或规则可能经常变化或需要扩展
- 需要避免使用多重条件判断语句
- 需要将算法的使用与实现分离
4.2 常见陷阱与解决方案
-
策略类膨胀问题
- 问题:当策略过多时,会产生大量小类
- 解决方案:使用组合模式或将简单策略合并
-
策略间共享状态
- 问题:策略间需要共享数据
- 解决方案:引入上下文对象传递共享数据
-
客户端需要了解所有策略
- 问题:客户端需要知道具体策略的存在
- 解决方案:使用工厂模式或依赖注入隐藏策略创建细节
4.3 性能考量
策略模式通常会带来一些额外的对象创建开销,但在大多数情况下这种开销可以忽略不计。对于性能敏感的场景,可以考虑以下优化:
- 重用策略对象(如果策略是无状态的)
- 使用枚举实现单例策略
- 在策略选择频繁的场景,缓存策略对象
5. 策略模式与其他设计模式的比较
5.1 策略模式 vs 工厂模式
| 特性 | 策略模式 | 工厂模式 |
|---|---|---|
| 目的 | 封装算法,使它们可以互换 | 创建对象,隐藏创建逻辑 |
| 关注点 | 行为 | 创建 |
| 典型应用 | 算法选择,业务规则 | 对象实例化 |
5.2 策略模式 vs 状态模式
| 特性 | 策略模式 | 状态模式 |
|---|---|---|
| 目的 | 封装可互换的算法 | 封装与状态相关的行为 |
| 切换机制 | 客户端显式选择 | 状态对象自动转换 |
| 状态感知 | 策略通常无状态 | 状态对象了解上下文 |
5.3 策略模式 vs 模板方法模式
| 特性 | 策略模式 | 模板方法模式 |
|---|---|---|
| 实现方式 | 对象组合 | 类继承 |
| 灵活性 | 运行时切换算法 | 编译时确定算法骨架 |
| 扩展性 | 通过新增策略类扩展 | 通过子类扩展 |
6. 实战案例:电商促销系统
让我们通过一个更复杂的案例来展示策略模式的实际应用 - 电商促销系统。
6.1 需求分析
电商系统需要支持多种促销策略:
- 满减促销(满100减20)
- 折扣促销(8折优惠)
- 赠品促销(买一赠一)
- 组合促销(满200减50,同时赠送礼品)
6.2 策略设计
首先定义促销策略接口:
java复制public interface PromotionStrategy {
Order applyPromotion(Order order);
}
实现具体策略:
java复制// 满减策略
public class FullReductionPromotion implements PromotionStrategy {
private BigDecimal condition;
private BigDecimal reduction;
public FullReductionPromotion(BigDecimal condition, BigDecimal reduction) {
this.condition = condition;
this.reduction = reduction;
}
@Override
public Order applyPromotion(Order order) {
if (order.getTotalAmount().compareTo(condition) >= 0) {
order.setDiscountAmount(order.getDiscountAmount().add(reduction));
}
return order;
}
}
// 折扣策略
public class DiscountPromotion implements PromotionStrategy {
private BigDecimal discountRate;
public DiscountPromotion(BigDecimal discountRate) {
this.discountRate = discountRate;
}
@Override
public Order applyPromotion(Order order) {
BigDecimal discount = order.getTotalAmount()
.multiply(BigDecimal.ONE.subtract(discountRate));
order.setDiscountAmount(order.getDiscountAmount().add(discount));
return order;
}
}
6.3 策略组合
有时我们需要组合多个策略:
java复制public class CompositePromotionStrategy implements PromotionStrategy {
private List<PromotionStrategy> strategies;
public CompositePromotionStrategy(List<PromotionStrategy> strategies) {
this.strategies = strategies;
}
@Override
public Order applyPromotion(Order order) {
Order result = order;
for (PromotionStrategy strategy : strategies) {
result = strategy.applyPromotion(result);
}
return result;
}
}
6.4 策略工厂
创建策略工厂来管理策略:
java复制public class PromotionStrategyFactory {
private static final Map<String, PromotionStrategy> strategies = new HashMap<>();
static {
strategies.put("FULL_REDUCTION",
new FullReductionPromotion(new BigDecimal("100"), new BigDecimal("20")));
strategies.put("DISCOUNT",
new DiscountPromotion(new BigDecimal("0.8")));
// 其他策略...
}
public static PromotionStrategy getStrategy(String promotionType) {
return strategies.get(promotionType);
}
public static PromotionStrategy getCompositeStrategy(List<String> promotionTypes) {
List<PromotionStrategy> strategyList = promotionTypes.stream()
.map(PromotionStrategyFactory::getStrategy)
.filter(Objects::nonNull)
.collect(Collectors.toList());
return new CompositePromotionStrategy(strategyList);
}
}
7. 策略模式的测试策略
7.1 单元测试策略类
每个策略类应该独立测试:
java复制public class FullReductionPromotionTest {
@Test
public void testApplyPromotion_WhenAmountMeetsCondition_ShouldApplyReduction() {
FullReductionPromotion strategy = new FullReductionPromotion(
new BigDecimal("100"), new BigDecimal("20"));
Order order = new Order(new BigDecimal("150"));
Order result = strategy.applyPromotion(order);
assertEquals(new BigDecimal("20"), result.getDiscountAmount());
}
@Test
public void testApplyPromotion_WhenAmountBelowCondition_ShouldNotApply() {
FullReductionPromotion strategy = new FullReductionPromotion(
new BigDecimal("100"), new BigDecimal("20"));
Order order = new Order(new BigDecimal("80"));
Order result = strategy.applyPromotion(order);
assertEquals(BigDecimal.ZERO, result.getDiscountAmount());
}
}
7.2 集成测试策略上下文
测试上下文类如何与策略交互:
java复制public class PromotionContextTest {
@Test
public void testExecuteStrategy_ShouldApplyCorrectPromotion() {
PromotionContext context = new PromotionContext();
Order order = new Order(new BigDecimal("150"));
context.setStrategy(new FullReductionPromotion(
new BigDecimal("100"), new BigDecimal("20")));
Order result = context.applyPromotion(order);
assertEquals(new BigDecimal("20"), result.getDiscountAmount());
}
}
7.3 测试策略工厂
验证工厂是否正确创建和返回策略:
java复制public class PromotionStrategyFactoryTest {
@Test
public void testGetStrategy_WithValidType_ShouldReturnStrategy() {
PromotionStrategy strategy = PromotionStrategyFactory.getStrategy("FULL_REDUCTION");
assertTrue(strategy instanceof FullReductionPromotion);
}
@Test
public void testGetCompositeStrategy_WithMultipleTypes_ShouldReturnComposite() {
PromotionStrategy strategy = PromotionStrategyFactory.getCompositeStrategy(
Arrays.asList("FULL_REDUCTION", "DISCOUNT"));
assertTrue(strategy instanceof CompositePromotionStrategy);
}
}
8. 策略模式在实际项目中的应用建议
8.1 项目规模考量
- 小型项目:如果策略数量有限(2-3种),可以考虑简化实现
- 中型项目:推荐使用标准策略模式实现
- 大型项目:考虑策略注册中心、动态加载等高级特性
8.2 团队协作规范
- 命名约定:策略接口使用
XxxStrategy,实现类使用XxxStrategyImpl或具体描述 - 包结构:将相关策略放在同一包下,如
com.example.strategy.payment - 文档要求:每个策略类应该用注释说明适用场景和算法逻辑
8.3 性能优化技巧
- 策略对象复用:如果策略是无状态的,可以设计为单例
- 延迟加载:对于初始化成本高的策略,采用懒加载方式
- 缓存机制:缓存频繁使用的策略对象
8.4 监控与维护
- 策略使用统计:记录各策略的执行次数和性能指标
- 版本兼容:当修改策略接口时,考虑向后兼容
- 废弃策略处理:提供明确的废弃标记和迁移路径
9. 策略模式的变体与扩展
9.1 带上下文的策略模式
有时策略需要访问更广泛的上下文信息:
java复制public interface ShippingStrategy {
double calculate(ShippingContext context);
}
public class ShippingContext {
private Order order;
private User user;
private Warehouse warehouse;
// 其他上下文信息...
// getters and setters
}
9.2 可配置策略
允许运行时动态配置策略参数:
java复制public interface ConfigurableStrategy<T> {
void configure(T config);
void execute();
}
public class TimeBasedDiscountStrategy implements ConfigurableStrategy<DiscountConfig> {
private DiscountConfig config;
@Override
public void configure(DiscountConfig config) {
this.config = config;
}
@Override
public void execute() {
// 使用config实现策略逻辑
}
}
9.3 策略链模式
将多个策略组合成处理链:
java复制public class StrategyChain {
private List<Strategy> strategies;
private int index = 0;
public StrategyChain(List<Strategy> strategies) {
this.strategies = strategies;
}
public void proceed(Context context) {
if (index < strategies.size()) {
strategies.get(index++).execute(context, this);
}
}
}
public interface Strategy {
void execute(Context context, StrategyChain chain);
}
10. 从策略模式看设计原则
策略模式很好地体现了几个重要的面向对象设计原则:
10.1 开闭原则(OCP)
策略模式允许在不修改现有代码的情况下引入新策略,符合"对扩展开放,对修改关闭"的原则。
10.2 单一职责原则(SRP)
每个策略类只负责一个特定的算法或业务规则,职责单一。
10.3 依赖倒置原则(DIP)
高层模块(上下文)依赖于抽象(策略接口),而不是具体实现。
10.4 接口隔离原则(ISP)
策略接口通常很小且专注,不会强迫实现类依赖它们不需要的方法。
10.5 组合优于继承
策略模式使用对象组合来动态改变行为,比使用继承更灵活。
在实际开发中,我发现策略模式最大的价值不在于模式本身,而在于它促使我们思考如何更好地组织代码。当我开始识别系统中的"变化点"并将它们封装成策略时,代码的可维护性会显著提高。记住,设计模式的终极目标是帮助我们写出更清晰、更灵活的代码,而不是为了使用模式而使用模式。