配套代码: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 设计模式学习指南

上一篇:状态 · 下一篇:模板方法