介绍
小型包裹服务台需要比较不同运输规则给出的报价。对于一个 500 克的包裹,准备好的费率数据规定 Town 收费 600 分,Steps 收费 200 分。你将实现这些正常报价,再检查会引起价格变化的重量边界。
本实验要求你已掌握 Java 类、构造方法、方法、列表和基础多态。本课将介绍接口和明确的报价结果。解析器与启动脚本已提供;实际的资格判断和价格计算由你实现。作者估计的时长包含阅读和普通错误修正。
为固定价格规则定义公开契约
本步骤中,你将让 Town 规则返回真实的合格报价,并拒绝超重包裹。
打开 WebIDE Tab,在菜单中选择 Terminal → New Terminal,打开集成终端。以下命令都在这个 WebIDE 终端中执行。命令使用同一个 shell;cd 改变该 shell 的工作目录。如果重新打开终端,请再次执行 cd。环境已准备 JDK 和源码框架,无需安装构建工具。
cd /home/labex/project/parcels
cat 只显示文件内容,不修改文件。阅读简短的费率文件和接口:
cat data/rates.tsv
cat src/ShippingRule.java
费率文件采用 TSV 格式:字段用制表符分隔,没有表头。flat 行依次为 type、规则 ID、重量上限(克)、价格(分)。step 行在每阶梯价格前增加阶梯大小。本例中,Town 的上限是 2,000 克,收费 600 分;Steps 的上限是 5,000 克,每开始一个 500 克阶梯收费 200 分。
interface 描述调用方无需知道实现类就能使用的方法。ShippingRule 承诺提供规则 ID 和包裹报价。类通过 implements ShippingRule 声明实现该接口,并提供两个公开方法。调用 rule.quote(parcel) 时,运行时会选择该对象所属的实现。不需要强制类型转换,也不需要按承运商名称编写分支。
公开契约包括 ShippingRule.id()、ShippingRule.quote(Parcel),以及 Quote.eligible() 和 Quote.cents()。RuleFactory.flat(String, int, long) 与 RuleFactory.stepped(String, int, int, long) 创建规则对象;QuoteEngine.bestRules(Parcel, List<ShippingRule>) 返回所有获胜规则的 ID。这些方法签名让同一个调用方能够使用不同实现。你可以采用其他实现类组织方式,也可以修改 RuleFactory 来创建这些类。请保持这些公开方法以及提供的命令和输出格式可用;评分不要求工厂内部使用特定类名,也不要求特定编辑器。
包裹重量支持 1 到 1,000,000 之间的整数克。规则重量上限和阶梯大小也为 1 到 1,000,000 之间的整数;每个固定价格或每阶梯价格为 0 到 1,000,000 之间的整数分。符合资格的零分报价是合法的。它不同于 Quote.unavailable():后者的 eligible() 为 false,其内部保存的金额不能参与比较。计算金额使用 long,因此较大的合法乘积不会溢出 32 位 int。所有费率、人员和包裹名称都是合成练习数据,并非承运商的实时报价。
Parcel 已将 ID、显示名称和克数保存在私有字段中,并提供只读方法。Quote.priced(cents) 创建合格结果,Quote.unavailable() 创建不合格结果。FlatRule 的构造方法保存参数。在其 quote 方法中,先判断资格,再返回价格。return 立即结束方法;使用 > 可使恰好达到上限的包裹仍然合格。
下面的命令会替换一个 Java 源文件。cat 将两行 JAVA 之间的文本复制到 > 后面的路径;起始标记上的引号阻止 shell 对 Java 代码进行替换。请粘贴整个代码块,包括独占一行的最后一个 JAVA。也可以在 WebIDE 中编辑同一个文件,保存后返回 WebIDE 终端。如果粘贴不完整后出现续行提示,请按 Ctrl+C,再重新粘贴完整代码块。
cat > src/FlatRule.java <<'JAVA'
public final class FlatRule implements ShippingRule {
private final String id;
private final int maxGrams;
private final long cents;
public FlatRule(String id, int maxGrams, long cents) {
this.id = id; this.maxGrams = maxGrams; this.cents = cents;
}
public String id() { return id; }
public Quote quote(Parcel parcel) {
if (parcel.grams() > maxGrams) return Quote.unavailable();
return Quote.priced(cents);
}
}
JAVA
./quote.sh 运行当前目录中提供的启动脚本。它用 javac -encoding UTF-8 将 src/*.java 编译到临时目录,再以 UTF-8 文本编码启动 Java。每次运行都会重新编译已保存的源码,不会复用旧的类文件。编译错误会给出源文件名和行号:修正并保存该文件后,再执行相同命令。路径不正确会产生文件错误;重新执行文中的 cd 并检查文件名即可,无需重置环境。
./quote.sh quote 500
quote 命令接收以克为单位的重量,并显示每个规则 ID,以及以分为单位的金额或 UNAVAILABLE。Town 现在应显示 600。Steps 仍显示 0,因为其初始方法故意尚未完成;不要将它理解为真实的免费报价。
试试重量上限以及超过上限一克的情况:
./quote.sh quote 2000
./quote.sh quote 2001
Town 从 600 变为 UNAVAILABLE。你在不修改 data/rates.tsv 的情况下保留了包含端点的重量上限。检查本步骤后再继续。
按每个已开始的重量阶梯收费
本步骤中,你将通过相同接口实现第二种规则,使用整数运算处理价格,而不使用浮点货币。
继续在 WebIDE 终端的 /home/labex/project/parcels 中操作。每开始一个 500 克阶梯收费 200 分:1–500 克用一个阶梯,501–1,000 克用两个,1,001–1,500 克用三个。因此 500 克收费 200 分,而 501 克收费 400 分。普通整数除法会截断:501 / 500 等于 1,会少收费。
对于正整数克数,(grams + stepGrams - 1L) / stepGrams 将阶梯数向上取整。L 使运算使用 long;该阶梯数与 stepCents 相乘也使用 long。先检查重量上限,避免为服务范围以外的包裹返回误导性的价格。
使用前面相同的 here-document 写法,将整个阶梯规则文件替换为以下实现:
cat > src/StepRule.java <<'JAVA'
public final class StepRule implements ShippingRule {
private final String id;
private final int maxGrams, stepGrams;
private final long stepCents;
public StepRule(String id, int maxGrams, int stepGrams, long stepCents) {
this.id = id; this.maxGrams = maxGrams;
this.stepGrams = stepGrams; this.stepCents = stepCents;
}
public String id() { return id; }
public Quote quote(Parcel parcel) {
if (parcel.grams() > maxGrams) return Quote.unavailable();
long steps = (parcel.grams() + stepGrams - 1L) / stepGrams;
return Quote.priced(steps * stepCents);
}
}
JAVA
./quote.sh quote 500
预期输出如下,列之间为制表符:
Town 600
Steps 200
现在跨过一个阶梯边界和资格边界:
./quote.sh quote 501
./quote.sh quote 5001
501 克时,Steps 显示 400。5,001 克时,两条规则都显示 UNAVAILABLE;这是重量合法但没有报价,并不是包裹无效。接下来有意试用零克:
./quote.sh quote 0
此命令预期失败,显示 INVALID_WEIGHT SAMPLE,退出状态为 2。它不会修改源码或费率记录。只需提供一个合法重量即可恢复:
./quote.sh quote 1500
两条规则现在都显示 600。这是真实的并列报价,下一个实验将处理这种情况。资格和价格判断位于可替换的规则对象内部,而调用方仍只使用接口。
总结
你通过一个 Java 接口实现了两种规则,将资格与整数分金额分开,并检查了精确上限和已开始阶梯的向上取整。同一个调用方现在无需知道具体实现,也能获得有意义的报价。



