Skip to content

策略模式 ​

策略模式定义一系列算法,把它们封装起来,并且使它们可互相替换。策略模式让算法独立于使用它的客户而变化。

📋 模式概述 ​

定义: 定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

适用场景:

  • 系统中有许多类,它们的区别仅在于行为不同
  • 需要动态地选择算法
  • 避免使用大量的if-else或switch-case语句
  • 算法的实现和使用分离

🎯 一、项目案例:优惠券计算策略 ​

1.1 问题场景 ​

在电商系统中,有多种优惠券类型(满减券、折扣券、直减券等),每种优惠券的计算逻辑不同。如果使用if-else处理,代码会非常臃肿。

问题代码示例:

java
/**
 * 订单服务 - 使用if-else计算优惠(问题示例)
 */
public class OrderService {
    
    public BigDecimal calculateDiscount(Order order, Coupon coupon) {
        BigDecimal discount = BigDecimal.ZERO;
        
        if ("FULL_REDUCTION".equals(coupon.getType())) {
            // 满减券逻辑
            if (order.getTotalAmount().compareTo(coupon.getMinAmount()) >= 0) {
                discount = coupon.getDiscountAmount();
            }
        } else if ("DISCOUNT".equals(coupon.getType())) {
            // 折扣券逻辑
            discount = order.getTotalAmount()
                .multiply(coupon.getDiscountRate())
                .setScale(2, RoundingMode.HALF_UP);
        } else if ("DIRECT_REDUCTION".equals(coupon.getType())) {
            // 直减券逻辑
            discount = coupon.getDiscountAmount();
        } else if ("PERCENTAGE".equals(coupon.getType())) {
            // 百分比折扣逻辑
            discount = order.getTotalAmount()
                .multiply(coupon.getDiscountPercent())
                .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
        }
        // ... 更多if-else
        
        return discount;
    }
}

问题:

  • 代码臃肿,可读性差
  • 新增优惠券类型需要修改现有代码
  • 违反开闭原则
  • 难以测试和维护

1.2 策略模式解决方案 ​

项目背景: 电商系统优惠券计算

步骤1:定义策略接口 ​

java
/**
 * 优惠券计算策略接口
 * 项目案例:电商系统优惠券计算
 */
public interface DiscountStrategy {
    
    /**
     * 计算优惠金额
     * @param order 订单
     * @param coupon 优惠券
     * @return 优惠金额
     */
    BigDecimal calculateDiscount(Order order, Coupon coupon);
    
    /**
     * 获取策略类型
     * @return 策略类型
     */
    String getType();
}

步骤2:实现具体策略 ​

java
/**
 * 满减券策略
 */
@Component
public class FullReductionStrategy implements DiscountStrategy {
    
    @Override
    public BigDecimal calculateDiscount(Order order, Coupon coupon) {
        // 满减券:订单金额达到最低金额时,减免固定金额
        BigDecimal totalAmount = order.getTotalAmount();
        BigDecimal minAmount = coupon.getMinAmount();
        BigDecimal discountAmount = coupon.getDiscountAmount();
        
        if (totalAmount.compareTo(minAmount) >= 0) {
            // 优惠金额不能超过订单金额
            return discountAmount.min(totalAmount);
        }
        return BigDecimal.ZERO;
    }
    
    @Override
    public String getType() {
        return "FULL_REDUCTION";
    }
}
java
/**
 * 折扣券策略
 */
@Component
public class DiscountStrategy implements DiscountStrategy {
    
    @Override
    public BigDecimal calculateDiscount(Order order, Coupon coupon) {
        // 折扣券:按折扣率计算优惠金额
        BigDecimal totalAmount = order.getTotalAmount();
        BigDecimal discountRate = coupon.getDiscountRate();
        
        // 折扣率应该在0-1之间
        if (discountRate.compareTo(BigDecimal.ZERO) > 0 
            && discountRate.compareTo(BigDecimal.ONE) <= 0) {
            return totalAmount
                .multiply(BigDecimal.ONE.subtract(discountRate))
                .setScale(2, RoundingMode.HALF_UP);
        }
        return BigDecimal.ZERO;
    }
    
    @Override
    public String getType() {
        return "DISCOUNT";
    }
}
java
/**
 * 直减券策略
 */
@Component
public class DirectReductionStrategy implements DiscountStrategy {
    
    @Override
    public BigDecimal calculateDiscount(Order order, Coupon coupon) {
        // 直减券:直接减免固定金额
        BigDecimal totalAmount = order.getTotalAmount();
        BigDecimal discountAmount = coupon.getDiscountAmount();
        
        // 优惠金额不能超过订单金额
        return discountAmount.min(totalAmount);
    }
    
    @Override
    public String getType() {
        return "DIRECT_REDUCTION";
    }
}
java
/**
 * 百分比折扣策略
 */
@Component
public class PercentageDiscountStrategy implements DiscountStrategy {
    
    @Override
    public BigDecimal calculateDiscount(Order order, Coupon coupon) {
        // 百分比折扣:按百分比计算优惠金额
        BigDecimal totalAmount = order.getTotalAmount();
        BigDecimal discountPercent = coupon.getDiscountPercent();
        
        // 百分比应该在0-100之间
        if (discountPercent.compareTo(BigDecimal.ZERO) > 0 
            && discountPercent.compareTo(new BigDecimal("100")) <= 0) {
            return totalAmount
                .multiply(discountPercent)
                .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
        }
        return BigDecimal.ZERO;
    }
    
    @Override
    public String getType() {
        return "PERCENTAGE";
    }
}

步骤3:创建策略上下文 ​

java
/**
 * 优惠券计算上下文
 * 项目案例:电商系统优惠券计算
 */
@Service
public class DiscountContext {
    
    // 存储所有策略
    private final Map<String, DiscountStrategy> strategies = new HashMap<>();
    
    /**
     * 通过构造函数注入所有策略
     */
    public DiscountContext(List<DiscountStrategy> strategies) {
        // 将策略注册到Map中
        for (DiscountStrategy strategy : strategies) {
            this.strategies.put(strategy.getType(), strategy);
        }
    }
    
    /**
     * 计算优惠金额
     */
    public BigDecimal calculateDiscount(Order order, Coupon coupon) {
        DiscountStrategy strategy = strategies.get(coupon.getType());
        
        if (strategy == null) {
            throw new IllegalArgumentException("不支持的优惠券类型: " + coupon.getType());
        }
        
        return strategy.calculateDiscount(order, coupon);
    }
}

步骤4:使用策略模式 ​

java
/**
 * 订单服务 - 使用策略模式计算优惠
 * 项目案例:电商订单优惠计算
 */
@Service
public class OrderService {
    
    @Autowired
    private DiscountContext discountContext;
    
    /**
     * 计算订单最终金额
     */
    public BigDecimal calculateFinalAmount(Order order, Coupon coupon) {
        BigDecimal totalAmount = order.getTotalAmount();
        
        // 使用策略模式计算优惠
        BigDecimal discount = discountContext.calculateDiscount(order, coupon);
        
        // 最终金额 = 原价 - 优惠金额
        BigDecimal finalAmount = totalAmount.subtract(discount);
        
        // 最终金额不能小于0
        return finalAmount.max(BigDecimal.ZERO);
    }
    
    /**
     * 创建订单并应用优惠券
     */
    public Order createOrderWithCoupon(CreateOrderRequest request) {
        // 创建订单
        Order order = new Order();
        order.setItems(request.getItems());
        order.setTotalAmount(calculateTotalAmount(request.getItems()));
        
        // 如果有优惠券,计算优惠
        if (request.getCouponId() != null) {
            Coupon coupon = couponRepository.findById(request.getCouponId());
            BigDecimal discount = discountContext.calculateDiscount(order, coupon);
            order.setDiscountAmount(discount);
            order.setFinalAmount(order.getTotalAmount().subtract(discount));
            order.setCouponId(coupon.getId());
        } else {
            order.setFinalAmount(order.getTotalAmount());
        }
        
        // 保存订单
        return orderRepository.save(order);
    }
    
    private BigDecimal calculateTotalAmount(List<OrderItem> items) {
        return items.stream()
            .map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

好处:

  1. 消除if-else: 用策略替换大量的条件判断
  2. 易于扩展: 新增优惠券类型只需添加新的策略类
  3. 易于测试: 每个策略可以独立测试
  4. 符合开闭原则: 对扩展开放,对修改关闭

🎯 二、项目案例:数据导出策略 ​

2.1 问题场景 ​

在报表系统中,需要支持多种数据导出格式(Excel、PDF、CSV等),每种格式的导出逻辑不同。

2.2 策略模式解决方案 ​

项目背景: 报表系统数据导出

java
/**
 * 数据导出策略接口
 */
public interface ExportStrategy {
    
    /**
     * 导出数据
     * @param data 数据
     * @param fileName 文件名
     * @return 文件路径
     */
    String export(List<?> data, String fileName);
    
    /**
     * 获取支持的格式
     */
    String getFormat();
}
java
/**
 * Excel导出策略
 */
@Component
public class ExcelExportStrategy implements ExportStrategy {
    
    @Override
    public String export(List<?> data, String fileName) {
        try (Workbook workbook = new XSSFWorkbook();
             FileOutputStream fos = new FileOutputStream(fileName + ".xlsx")) {
            
            Sheet sheet = workbook.createSheet("数据");
            
            // 创建表头
            Row headerRow = sheet.createRow(0);
            // ... 设置表头
            
            // 填充数据
            int rowNum = 1;
            for (Object item : data) {
                Row row = sheet.createRow(rowNum++);
                // ... 填充行数据
            }
            
            workbook.write(fos);
            return fileName + ".xlsx";
        } catch (Exception e) {
            throw new RuntimeException("Excel导出失败", e);
        }
    }
    
    @Override
    public String getFormat() {
        return "EXCEL";
    }
}
java
/**
 * PDF导出策略
 */
@Component
public class PDFExportStrategy implements ExportStrategy {
    
    @Override
    public String export(List<?> data, String fileName) {
        try {
            Document document = new Document();
            PdfWriter writer = PdfWriter.getInstance(document, 
                new FileOutputStream(fileName + ".pdf"));
            
            document.open();
            
            // 创建表格
            PdfPTable table = new PdfPTable(getColumnCount());
            // ... 填充表格数据
            
            document.add(table);
            document.close();
            
            return fileName + ".pdf";
        } catch (Exception e) {
            throw new RuntimeException("PDF导出失败", e);
        }
    }
    
    @Override
    public String getFormat() {
        return "PDF";
    }
    
    private int getColumnCount() {
        // 返回列数
        return 5;
    }
}
java
/**
 * CSV导出策略
 */
@Component
public class CSVExportStrategy implements ExportStrategy {
    
    @Override
    public String export(List<?> data, String fileName) {
        try (FileWriter writer = new FileWriter(fileName + ".csv");
             CSVWriter csvWriter = new CSVWriter(writer)) {
            
            // 写入表头
            csvWriter.writeNext(getHeaders());
            
            // 写入数据
            for (Object item : data) {
                csvWriter.writeNext(convertToRow(item));
            }
            
            return fileName + ".csv";
        } catch (Exception e) {
            throw new RuntimeException("CSV导出失败", e);
        }
    }
    
    @Override
    public String getFormat() {
        return "CSV";
    }
    
    private String[] getHeaders() {
        return new String[]{"列1", "列2", "列3"};
    }
    
    private String[] convertToRow(Object item) {
        // 转换为CSV行
        return new String[]{"值1", "值2", "值3"};
    }
}
java
/**
 * 导出服务 - 使用策略模式
 */
@Service
public class ExportService {
    
    private final Map<String, ExportStrategy> strategies = new HashMap<>();
    
    public ExportService(List<ExportStrategy> strategies) {
        for (ExportStrategy strategy : strategies) {
            this.strategies.put(strategy.getFormat(), strategy);
        }
    }
    
    /**
     * 导出数据
     */
    public String exportData(List<?> data, String format, String fileName) {
        ExportStrategy strategy = strategies.get(format.toUpperCase());
        
        if (strategy == null) {
            throw new IllegalArgumentException("不支持的导出格式: " + format);
        }
        
        return strategy.export(data, fileName);
    }
}

📖 策略模式最佳实践 ​

1. 策略的选择 ​

  • 使用工厂模式选择策略
  • 使用Spring的依赖注入管理策略
  • 根据配置动态选择策略

2. 策略的注册 ​

java
/**
 * 策略管理器 - 自动注册策略
 */
@Component
public class StrategyManager {
    
    private final Map<String, DiscountStrategy> strategies = new ConcurrentHashMap<>();
    
    @Autowired
    public StrategyManager(List<DiscountStrategy> strategies) {
        for (DiscountStrategy strategy : strategies) {
            register(strategy.getType(), strategy);
        }
    }
    
    public void register(String type, DiscountStrategy strategy) {
        strategies.put(type, strategy);
    }
    
    public DiscountStrategy getStrategy(String type) {
        return strategies.get(type);
    }
}

✅ 学习检查清单 ​

  • [ ] 已理解策略模式的定义和适用场景
  • [ ] 已掌握如何用策略模式消除if-else
  • [ ] 已在项目中应用策略模式
  • [ ] 已了解策略模式和工厂模式的结合使用
  • [ ] 已理解策略模式的优缺点

上一章节:工厂模式
下一章节:观察者模式
返回目录:设计模式