策略模式:设计与实践
一、什么是策略模式
1. 基本定义
策略模式(Strategy Pattern)是一种行为型设计模式,由《设计模式:可复用面向对象软件的基础》(GOF著作)定义为:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
该模式通过将可变的算法逻辑封装为独立的策略对象,使客户端可以根据需求动态选择不同的算法,而无需修改使用算法的核心逻辑。核心是实现“行为的封装与切换”,解决在多种算法并存时,硬编码导致的扩展性差、维护困难问题。
2. 核心思想
策略模式的核心在于封装与替换。当系统中存在多种实现同一功能的算法(如不同的支付渠道、签名方式、计费规则),且这些算法可能随业务需求动态变化时,通过定义统一的策略接口,将每种算法封装为独立的策略类,客户端可在运行时根据条件选择合适的策略,从而实现算法与使用算法的客户端解耦,提高系统的灵活性和可扩展性。
二、策略模式的特点
1. 算法封装
每种算法被封装为独立的策略类,职责单一,便于维护和测试。
2. 接口统一
所有策略实现同一个策略接口,保证客户端可以用一致的方式使用不同策略。
3. 动态切换
客户端可在运行时根据条件动态选择或切换策略,无需修改原有代码。
4. 避免硬编码
用多态替代条件判断语句(如if-else、switch),减少代码冗余和复杂度。
5. 客户端需知晓策略
客户端需要了解不同策略的差异,才能选择合适的策略,可能增加使用成本。
| 特点 | 说明 |
|---|---|
| 算法封装 | 每种算法独立封装为策略类 |
| 接口统一 | 所有策略实现相同接口,保证使用方式一致 |
| 动态切换 | 运行时可根据条件选择不同策略 |
| 避免硬编码 | 用多态替代条件判断,减少代码冗余 |
| 客户端需知晓策略 | 客户端需了解策略差异以选择合适实现 |
三、策略模式的标准代码实现
1. 模式结构
策略模式包含三个核心角色:
- 策略接口(Strategy):定义所有支持的算法的公共接口,声明算法的执行方法。
- 具体策略(ConcreteStrategy):实现策略接口,封装具体的算法逻辑。
- 上下文(Context):持有策略对象的引用,负责策略的选择和调用,客户端通过上下文使用策略,无需直接与具体策略交互。
2. 代码实现示例
2.1 策略接口
/**
* 策略接口
* 定义算法的公共方法
*/
public interface Strategy {
/**
* 执行算法
* @param param 输入参数
* @return 算法执行结果
*/
String execute(String param);
}
2.2 具体策略
/**
* 具体策略A:算法A实现
*/
public class ConcreteStrategyA implements Strategy {
@Override
public String execute(String param) {
return "策略A处理:" + param.toUpperCase();
}
}
/**
* 具体策略B:算法B实现
*/
public class ConcreteStrategyB implements Strategy {
@Override
public String execute(String param) {
return "策略B处理:" + param.toLowerCase();
}
}
/**
* 具体策略C:算法C实现
*/
public class ConcreteStrategyC implements Strategy {
@Override
public String execute(String param) {
// 示例:反转字符串
return "策略C处理:" + new StringBuilder(param).reverse().toString();
}
}
2.3 上下文
/**
* 上下文类
* 持有策略引用,负责策略的选择和调用
*/
public class Context {
// 策略对象引用
private Strategy strategy;
// 构造函数注入策略
public Context(Strategy strategy) {
this.strategy = strategy;
}
// 动态设置策略
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
// 执行策略
public String executeStrategy(String param) {
return strategy.execute(param);
}
}
2.4 客户端使用示例
/**
* 客户端
* 使用策略模式
*/
public class Client {
public static void main(String[] args) {
// 创建上下文,初始使用策略A
Context context = new Context(new ConcreteStrategyA());
System.out.println(context.executeStrategy("Hello"));
// 动态切换为策略B
context.setStrategy(new ConcreteStrategyB());
System.out.println(context.executeStrategy("WORLD"));
// 动态切换为策略C
context.setStrategy(new ConcreteStrategyC());
System.out.println(context.executeStrategy("Strategy"));
}
}
3. 代码实现特点总结
| 角色 | 核心职责 | 代码特点 |
|---|---|---|
| 策略接口(Strategy) | 定义算法的公共接口 | 声明算法执行方法,是所有具体策略的超类型 |
| 具体策略(ConcreteStrategy) | 实现具体算法 | 实现策略接口,封装独立的算法逻辑,专注于单一算法 |
| 上下文(Context) | 管理策略并提供调用入口 | 持有策略引用,提供设置策略的方法,调用策略的执行方法 |
四、支付框架设计中策略模式的运用
以退款手续费计算策略为例,说明策略模式在支付系统中的具体实现:
1. 场景分析
支付系统中,退款手续费的计算方式因业务场景而异:
- 原路退回:按退款金额的固定比例(如1%)收取手续费,最低1元
- 退回余额:免手续费
- 过期退款:收取最低手续费(10元)+ 超额部分的0.5%
- 商户优惠退款:手续费由商户承担,用户实际支付0元
不同计算方式的逻辑差异显著,若将所有规则硬编码在退款服务中,会导致if-else语句堆砌,新增规则需修改核心代码。使用策略模式可将每种计算方式封装为独立策略,客户端根据退款类型动态选择策略,实现灵活扩展。
2. 设计实现
2.1 退款手续费策略接口
import java.math.BigDecimal;
/**
* 退款手续费策略接口
*/
public interface RefundFeeStrategy {
/**
* 计算退款手续费
* @param request 退款请求参数
* @return 手续费金额
*/
BigDecimal calculate(RefundRequest request);
/**
* 获取支持的退款类型
*/
String getRefundType();
}
2.2 具体策略实现
import java.math.BigDecimal;
/**
* 具体策略1:原路退回
*/
public class OriginalRefundStrategy implements RefundFeeStrategy {
// 费率:1%
private static final BigDecimal RATE = new BigDecimal("0.01");
// 最低手续费:1元
private static final BigDecimal MIN_FEE = BigDecimal.ONE;
@Override
public BigDecimal calculate(RefundRequest request) {
// 手续费 = 退款金额 × 费率,且不低于最低手续费
BigDecimal fee = request.getRefundAmount().multiply(RATE);
return fee.compareTo(MIN_FEE) < 0 ? MIN_FEE : fee;
}
@Override
public String getRefundType() {
return "ORIGINAL";
}
}
/**
* 具体策略2:退回余额
*/
public class BalanceRefundStrategy implements RefundFeeStrategy {
@Override
public BigDecimal calculate(RefundRequest request) {
// 余额退款免手续费
return BigDecimal.ZERO;
}
@Override
public String getRefundType() {
return "BALANCE";
}
}
/**
* 具体策略3:过期退款
*/
public class ExpiredRefundStrategy implements RefundFeeStrategy {
// 最低手续费
private static final BigDecimal MIN_FEE = new BigDecimal("10");
// 超额部分费率
private static final BigDecimal EXCESS_RATE = new BigDecimal("0.005");
// 起征点(超过此金额才收取超额部分费用)
private static final BigDecimal THRESHOLD = new BigDecimal("1000");
@Override
public BigDecimal calculate(RefundRequest request) {
BigDecimal refundAmount = request.getRefundAmount();
// 基础手续费为最低费用
BigDecimal fee = MIN_FEE;
// 超过起征点的部分加收超额费
if (refundAmount.compareTo(THRESHOLD) > 0) {
BigDecimal excess = refundAmount.subtract(THRESHOLD);
fee = fee.add(excess.multiply(EXCESS_RATE));
}
return fee.setScale(2, BigDecimal.ROUND_HALF_UP);
}
@Override
public String getRefundType() {
return "EXPIRED";
}
}
/**
* 具体策略4:商户优惠退款
*/
public class MerchantDiscountRefundStrategy implements RefundFeeStrategy {
@Override
public BigDecimal calculate(RefundRequest request) {
// 手续费由商户承担,用户端显示0元
return BigDecimal.ZERO;
}
@Override
public String getRefundType() {
return "MERCHANT_DISCOUNT";
}
}
2.3 相关数据模型
import java.math.BigDecimal;
/**
* 退款请求参数
*/
public class RefundRequest {
private String orderId; // 原订单ID
private String refundId; // 退款单ID
private BigDecimal refundAmount; // 退款金额
private String refundType; // 退款类型(ORIGINAL/BALANCE/EXPIRED/MERCHANT_DISCOUNT)
private String reason; // 退款原因
private String merchantId; // 商户ID
// getter和setter方法
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public BigDecimal getRefundAmount() { return refundAmount; }
public void setRefundAmount(BigDecimal refundAmount) { this.refundAmount = refundAmount; }
public String getRefundType() { return refundType; }
public void setRefundType(String refundType) { this.refundType = refundType; }
// 其他getter和setter...
}
2.4 策略上下文
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 退款手续费计算上下文
*/
public class RefundFeeContext {
// 策略注册表:退款类型→策略
private final Map<String, RefundFeeStrategy> strategyMap = new HashMap<>();
// 初始化时注册所有策略
public RefundFeeContext(List<RefundFeeStrategy> strategies) {
for (RefundFeeStrategy strategy : strategies) {
strategyMap.put(strategy.getRefundType(), strategy);
}
}
/**
* 计算退款手续费
* @param request 退款请求
* @return 手续费金额
*/
public BigDecimal calculateFee(RefundRequest request) {
// 根据退款类型获取对应的策略
RefundFeeStrategy strategy = strategyMap.get(request.getRefundType());
if (strategy == null) {
throw new IllegalArgumentException("不支持的退款类型:" + request.getRefundType());
}
// 执行策略计算手续费
BigDecimal fee = strategy.calculate(request);
System.out.printf("订单%s退款手续费计算:类型=%s,金额=%s,手续费=%s%n",
request.getOrderId(), request.getRefundType(),
request.getRefundAmount(), fee);
return fee;
}
/**
* 动态注册新策略
*/
public void registerStrategy(RefundFeeStrategy strategy) {
strategyMap.put(strategy.getRefundType(), strategy);
}
}
2.5 客户端使用示例
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
/**
* 退款服务(客户端)
*/
public class RefundService {
private final RefundFeeContext feeContext;
// 构造函数注入上下文
public RefundService(RefundFeeContext feeContext) {
this.feeContext = feeContext;
}
/**
* 处理退款
*/
public void processRefund(RefundRequest request) {
// 1. 计算退款手续费
BigDecimal fee = feeContext.calculateFee(request);
// 2. 执行退款逻辑(省略具体实现)
System.out.printf("处理退款:订单%s,金额%s,手续费%s%n",
request.getOrderId(), request.getRefundAmount(), fee);
// 3. 其他操作(如更新订单状态、通知用户等)
}
public static void main(String[] args) {
// 1. 初始化所有策略
List<RefundFeeStrategy> strategies = Arrays.asList(
new OriginalRefundStrategy(),
new BalanceRefundStrategy(),
new ExpiredRefundStrategy(),
new MerchantDiscountRefundStrategy()
);
// 2. 创建上下文
RefundFeeContext context = new RefundFeeContext(strategies);
// 3. 创建退款服务
RefundService refundService = new RefundService(context);
// 4. 处理不同类型的退款
System.out.println("=== 原路退回 ===");
RefundRequest originalRequest = new RefundRequest();
originalRequest.setOrderId("ORDER001");
originalRequest.setRefundAmount(new BigDecimal("80"));
originalRequest.setRefundType("ORIGINAL");
refundService.processRefund(originalRequest);
System.out.println("\n=== 过期退款 ===");
RefundRequest expiredRequest = new RefundRequest();
expiredRequest.setOrderId("ORDER002");
expiredRequest.setRefundAmount(new BigDecimal("2000"));
expiredRequest.setRefundType("EXPIRED");
refundService.processRefund(expiredRequest);
System.out.println("\n=== 商户优惠退款 ===");
RefundRequest discountRequest = new RefundRequest();
discountRequest.setOrderId("ORDER003");
discountRequest.setRefundAmount(new BigDecimal("500"));
discountRequest.setRefundType("MERCHANT_DISCOUNT");
refundService.processRefund(discountRequest);
}
}
3. 模式价值体现
- 消除条件判断:用策略选择替代
if-else/switch,代码结构更清晰,如RefundService中无需判断退款类型 - 灵活扩展:新增退款类型(如“极速退款”)只需添加策略实现,无需修改退款服务核心代码
- 责任单一:每种手续费计算逻辑封装在独立类中,便于维护和单元测试
- 动态切换:支持运行时动态注册新策略(如活动期间临时启用优惠策略)
- 易于调试:每种策略独立日志输出,问题定位更便捷
五、开源框架中策略模式的运用
以Spring Framework的Resource接口为例,说明策略模式在开源框架中的典型应用:
1. 核心实现分析
Spring的Resource接口定义了资源访问的统一接口,不同类型的资源(文件、类路径资源、URL资源、字节数组资源)有不同的实现,客户端可根据资源路径动态选择对应的资源访问策略。
1.1 策略接口(Resource)
public interface Resource extends InputStreamSource {
// 是否存在
boolean exists();
// 是否可读
boolean isReadable();
// 获取URL
URL getURL() throws IOException;
// 获取文件
File getFile() throws IOException;
// 获取资源描述
String getDescription();
}
1.2 具体策略实现
Spring提供了多种Resource实现(具体策略):
FileSystemResource:访问文件系统资源ClassPathResource:访问类路径下的资源UrlResource:访问URL资源(如http://、ftp://)ByteArrayResource:访问字节数组资源
以ClassPathResource为例:
public class ClassPathResource implements Resource {
private final String path;
private final ClassLoader classLoader;
@Override
public InputStream getInputStream() throws IOException {
InputStream is = classLoader.getResourceAsStream(path);
if (is == null) {
throw new FileNotFoundException("类路径资源不存在: " + path);
}
return is;
}
// 其他方法实现...
}
1.3 上下文(ResourceLoader)
ResourceLoader作为上下文,负责根据资源路径选择合适的Resource实现:
public interface ResourceLoader {
// 前缀常量
String CLASSPATH_URL_PREFIX = "classpath:";
// 获取资源
Resource getResource(String location);
}
// 默认实现
public class DefaultResourceLoader implements ResourceLoader {
@Override
public Resource getResource(String location) {
if (location.startsWith(CLASSPATH_URL_PREFIX)) {
// 类路径资源:使用ClassPathResource
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
} else if (location.startsWith("file:")) {
// 文件系统资源:使用FileSystemResource
return new FileSystemResource(location.substring("file:".length()));
} else {
try {
// URL资源:使用UrlResource
return new UrlResource(new URL(location));
} catch (MalformedURLException ex) {
// 默认视为文件系统资源
return new FileSystemResource(location);
}
}
}
}
1.4 客户端使用
public class ResourceDemo {
public static void main(String[] args) {
ResourceLoader loader = new DefaultResourceLoader();
// 自动选择ClassPathResource
Resource classPathResource = loader.getResource("classpath:config.properties");
// 自动选择FileSystemResource
Resource fileResource = loader.getResource("file:/data/logs/app.log");
// 自动选择UrlResource
Resource urlResource = loader.getResource("https://example.com/api/config");
}
}
2. 策略模式在Spring中的价值
- 统一资源访问接口:客户端通过
Resource接口访问不同类型资源,无需关心具体实现 - 灵活扩展:新增资源类型(如分布式文件系统资源)只需实现
Resource接口,无需修改框架核心 - 按需选择策略:
ResourceLoader根据资源路径自动选择合适的资源访问策略,简化客户端使用
六、总结
1. 策略模式的适用场景
- 当系统中存在多种实现同一功能的算法,且需要动态选择时
- 当使用
if-else或switch语句过多,导致代码冗余、维护困难时 - 当算法需要独立于使用它的客户端,且可被替换时
- 当需要隐藏算法的实现细节,只暴露必要接口时
2. 策略模式与其他模式的区别
- 与工厂模式:工厂模式专注于对象的创建,策略模式专注于算法的封装与切换,前者是创建型模式,后者是行为型模式
- 与状态模式:两者都使用多态实现行为切换,但状态模式中状态由对象内部条件决定,策略模式中策略由客户端主动选择
- 与模板方法模式:模板方法模式通过继承实现算法骨架与细节的分离,策略模式通过组合实现算法的替换,前者是类级别的复用,后者是对象级别的复用
3. 支付系统中的实践价值
- 业务扩展灵活:新增支付渠道、计费规则等只需添加策略实现,无需修改核心流程
- 代码结构清晰:消除大量条件判断语句,提高代码可读性和可维护性
- 团队并行开发:不同策略可由不同团队并行开发,减少协作成本
- 便于测试:每种策略可独立测试,提高测试效率和覆盖率
- 动态配置支持:可通过配置中心动态切换策略,快速响应业务变化(如活动期间切换费率)
4. 实践建议
- 合理设计策略接口:接口应包含所有策略的公共方法,避免接口膨胀
- 策略选择逻辑集中:将策略选择逻辑放在上下文或专门的策略工厂中,避免分散在客户端
- 避免策略过多:策略过多可能导致系统复杂度上升,可结合组合模式管理复杂策略
- 考虑缓存策略对象:频繁创建策略对象可能影响性能,可缓存常用策略实例
- 客户端无需知道所有策略:可通过配置或枚举隐藏策略细节,客户端只需指定策略标识
策略模式通过“封装算法、动态切换”的思想,有效解决了支付系统中多种业务规则并存的问题,既保证了核心流程的稳定性,又为业务扩展提供了灵活的支撑,是支付框架应对复杂业务场景的关键设计模式。