23|策略:通过组合替换一组算法
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.strategy。
系列导航:Java 25 设计模式学习指南
文章校对可以按篇固定收费,也可以按字数收费。如果 QuoteService 每次根据字符串类型判断公式,算法增加后,业务服务会不断积累与报价流程无关的细节。
策略把每种算法放在独立对象中。QuoteService 的使用方式保持稳定,在装配时换一个策略,就得到不同计算规则。
从问题提炼设计意图
策略把可互换的算法封装在共同接口后,由上下文通过组合使用,使算法能够独立于调用它的业务变化。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
FeePolicy |
策略接口,约定非负字数与分单位费用 |
FlatFee |
固定每篇 1000 分的策略 |
PerWordFee |
按每字单价计算的策略 |
QuoteService |
上下文,只负责委托已选择策略 |
classDiagram
FeePolicy <|.. FlatFee
FeePolicy <|.. PerWordFee
QuoteService --> FeePolicy : delegates
Demo ..> QuoteService : quotes
代码思路:变化应该落在哪个对象上
先统一算法契约:输入字数必须非负,输出是以分为单位的 long,溢出应该报告异常。共同接口的价值在于可替换语义,而不只是所有方法都碰巧叫 fee。
FlatFee 返回固定费用,即使零字也按篇计费;PerWordFee 用单价乘字数,零字费用为零。这种差异是策略定义的业务规则,不能以“实现结果不同”判断某个策略违反契约。
PerWordFee 在构造时校验单价,在计算时校验字数,再用 Math.multiplyExact 防止溢出。QuoteService 持有 FeePolicy,不检查具体类型,也不负责选择类型。选择放在应用装配边界,之后一次 quote 只体现算法执行。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.strategy,源码目录为 src/main/java/com/hanserwei/patterns/strategy/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
FeePolicy.java:
package com.hanserwei.patterns.strategy;
/** 可替换的文章校对计费算法契约. */
public interface FeePolicy {
/** 对非负字数计算以分为单位的非负费用;溢出时抛出异常. */
long fee(int words);
}
PerWordFee.java:
package com.hanserwei.patterns.strategy;
/** 按字数收取费用的策略. */
public final class PerWordFee implements FeePolicy {
/** 每字费用,单位为分. */
private final long centsPerWord;
/** 设置非负单价. */
public PerWordFee(long centsPerWord) {
if (centsPerWord < 0) {
throw new IllegalArgumentException("Negative rate");
}
this.centsPerWord = centsPerWord;
}
/** 对非负字数使用精确整数乘法,溢出时抛出 ArithmeticException. */
@Override
public long fee(int words) {
if (words < 0) {
throw new IllegalArgumentException("Negative word count");
}
return Math.multiplyExact(centsPerWord, words);
}
}
QuoteService.java:
package com.hanserwei.patterns.strategy;
import java.util.Objects;
/** 通过注入策略完成报价的业务上下文. */
public final class QuoteService {
/** 当前服务使用的计费策略. */
private final FeePolicy policy;
/** 在装配时选择算法. */
public QuoteService(FeePolicy policy) {
this.policy = Objects.requireNonNull(policy, "policy");
}
/** 委托策略计算费用,返回以分为单位的金额. */
public long quote(int words) {
return policy.fee(words);
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.strategy;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
System.out.println(new QuoteService(new FlatFee()).quote(600));
System.out.println(new QuoteService(new PerWordFee(2)).quote(600));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runStrategy
./gradlew test --tests 'com.hanserwei.patterns.strategy.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
1000
1200
用测试确认模式的行为
600 字按篇得到 1000 分,按每字 2 分得到 1200 分;负输入被所有策略拒绝;单价与字数乘积溢出时显式失败。
对应测试位于 src/test/java/com/hanserwei/patterns/strategy/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 排序、校验、路由、定价等存在多套可替换算法。
- 希望单独测试算法,并避免上下文不断增加条件分支。
- 同一业务流程要根据不同客户或配置装配不同计算方式。
适用边界与容易踩的坑
把 if 搬进一个名叫 Strategy 的类,并没有完成解耦。需要让上下文依赖接口,并让每个具体算法能够独立扩展。策略选择处仍可能需要一个有限分支,这与算法分散在所有业务方法中不同。
本例通过构造器固定策略,没有运行中切换的 setter。运行时可替换是常见能力,但不是必须公开可变依赖;按请求创建服务或在装配处选择,也能实现策略意图。
Java 函数式接口与 lambda 可以简化无状态策略,但学习阶段保留具名类,能更清楚地展示字段、构造校验和职责。金额示例采用整数分仅用于算法演示,不展开真实计费系统的币种、舍入、税费等规则。
与相近模式比较
模板方法通过继承替换固定算法中的步骤;策略通过组合替换某个完整算法。命令保存一次待执行请求,策略通常是可以反复用于不同输入的计算能力。
动手练习
增加阶梯计费策略,规定前 500 字每字 2 分,超出部分每字 1 分。先为 0、499、500、501 写预期值,再实现策略,保持 QuoteService 不变。
系列导航:Java 25 设计模式学习指南