策略模式:设计与实践

0 阅读12分钟

策略模式:设计与实践

一、什么是策略模式

1. 基本定义

策略模式(Strategy Pattern)是一种行为型设计模式,由《设计模式:可复用面向对象软件的基础》(GOF著作)定义为:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化

该模式通过将可变的算法逻辑封装为独立的策略对象,使客户端可以根据需求动态选择不同的算法,而无需修改使用算法的核心逻辑。核心是实现“行为的封装与切换”,解决在多种算法并存时,硬编码导致的扩展性差、维护困难问题。

2. 核心思想

策略模式的核心在于封装与替换。当系统中存在多种实现同一功能的算法(如不同的支付渠道、签名方式、计费规则),且这些算法可能随业务需求动态变化时,通过定义统一的策略接口,将每种算法封装为独立的策略类,客户端可在运行时根据条件选择合适的策略,从而实现算法与使用算法的客户端解耦,提高系统的灵活性和可扩展性。

二、策略模式的特点

1. 算法封装

每种算法被封装为独立的策略类,职责单一,便于维护和测试。

2. 接口统一

所有策略实现同一个策略接口,保证客户端可以用一致的方式使用不同策略。

3. 动态切换

客户端可在运行时根据条件动态选择或切换策略,无需修改原有代码。

4. 避免硬编码

用多态替代条件判断语句(如if-elseswitch),减少代码冗余和复杂度。

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-elseswitch语句过多,导致代码冗余、维护困难时
  • 当算法需要独立于使用它的客户端,且可被替换时
  • 当需要隐藏算法的实现细节,只暴露必要接口时

2. 策略模式与其他模式的区别

  • 与工厂模式:工厂模式专注于对象的创建,策略模式专注于算法的封装与切换,前者是创建型模式,后者是行为型模式
  • 与状态模式:两者都使用多态实现行为切换,但状态模式中状态由对象内部条件决定,策略模式中策略由客户端主动选择
  • 与模板方法模式:模板方法模式通过继承实现算法骨架与细节的分离,策略模式通过组合实现算法的替换,前者是类级别的复用,后者是对象级别的复用

3. 支付系统中的实践价值

  • 业务扩展灵活:新增支付渠道、计费规则等只需添加策略实现,无需修改核心流程
  • 代码结构清晰:消除大量条件判断语句,提高代码可读性和可维护性
  • 团队并行开发:不同策略可由不同团队并行开发,减少协作成本
  • 便于测试:每种策略可独立测试,提高测试效率和覆盖率
  • 动态配置支持:可通过配置中心动态切换策略,快速响应业务变化(如活动期间切换费率)

4. 实践建议

  • 合理设计策略接口:接口应包含所有策略的公共方法,避免接口膨胀
  • 策略选择逻辑集中:将策略选择逻辑放在上下文或专门的策略工厂中,避免分散在客户端
  • 避免策略过多:策略过多可能导致系统复杂度上升,可结合组合模式管理复杂策略
  • 考虑缓存策略对象:频繁创建策略对象可能影响性能,可缓存常用策略实例
  • 客户端无需知道所有策略:可通过配置或枚举隐藏策略细节,客户端只需指定策略标识

策略模式通过“封装算法、动态切换”的思想,有效解决了支付系统中多种业务规则并存的问题,既保证了核心流程的稳定性,又为业务扩展提供了灵活的支撑,是支付框架应对复杂业务场景的关键设计模式。