比较可替换的运输规则

JavaBeginner
立即练习

介绍

包裹服务台已经能获得价格,但比较方法仍然不返回任何获胜规则。你将让它比较规则对象,保留所有最低价并列项,并为多个包裹保存完整报价文件。

本实验独立初始化,已准备好上一 guided 实验完成的两种规则实现。前置是该实验中的接口、资格和整数阶梯计价知识。你将增加已有规则的一个新参数实例,而不发明另一种收费算法。所有费用均为合成数据。时长为作者估计。

通过接口比较报价

本步骤中,你将实现一个适用于任意已提供 ShippingRule 的比较方法,包括比较器从未见过的新实现。

打开 WebIDE Tab,在菜单中选择 Terminal → New Terminal,打开集成终端。以下命令都在这个 WebIDE 终端中执行。命令使用同一个 shell;cd 改变该 shell 的工作目录。如果重新打开终端,请再次执行 cd。环境已准备 JDK 和源码框架,无需安装构建工具。

cd /home/labex/project/parcels
./quote.sh quote 1500

Town 和 Steps 都显示 600。这种正常并列说明,只保留一条最低价规则会丢失合法选择。完整的公开契约和输入契约如下:

公开契约包括 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。所有费率、人员和包裹名称都是合成练习数据,并非承运商的实时报价。

比较器接收 List<ShippingRule>。每个元素都可能使用不同实现;增强 for 循环会访问每个对象。调用 quote(parcel),不要检查规则名称,也不要用 instanceof 在比较器中重写规则的业务逻辑。

以空的获胜列表开始,并将最大的 long 值 Long.MAX_VALUE 用作比较哨兵。用 continue 跳过不合格报价。更低的价格会替换先前的最低价,因此用 clear() 移除旧的获胜项。相等的价格则添加其 ID。这是两个独立的 if:刚建立的新最低价也必须加入列表。如果没有合格报价,列表保持为空。

下面的命令会替换一个 Java 源文件。cat 将两行 JAVA 之间的文本复制到 > 后面的路径;起始标记上的引号阻止 shell 对 Java 代码进行替换。请粘贴整个代码块,包括独占一行的最后一个 JAVA。也可以在 WebIDE 中编辑同一个文件,保存后返回 WebIDE 终端。如果粘贴不完整后出现续行提示,请按 Ctrl+C,再重新粘贴完整代码块。

cat > src/QuoteEngine.java <<'JAVA'
import java.util.ArrayList;
import java.util.List;
public final class QuoteEngine {
    public static List<String> bestRules(Parcel parcel, List<ShippingRule> rules) {
        List<String> winners = new ArrayList<>();
        long lowest = Long.MAX_VALUE;
        for (ShippingRule rule : rules) {
            Quote quote = rule.quote(parcel);
            if (!quote.eligible()) continue;
            if (quote.cents() < lowest) {
                lowest = quote.cents();
                winners.clear();
            }
            if (quote.cents() == lowest) winners.add(rule.id());
        }
        return winners;
    }
}
JAVA
./quote.sh batch data/parcels.tsv data/rates.tsv

batch 接收包裹 TSV 路径和费率 TSV 路径。包裹行依次为 ID、显示名称和整数克数。输出将每个包裹、其全部报价和一个 BEST 行组织在一起。UNAVAILABLE 表示某条规则不能承运该包裹;BEST NONE 表示所有规则都不合格。没有合格报价的合法批次仍成功退出,状态为 0。

对于 P02,Town 和 Steps 都必须出现在 BEST 中;P03 重一克,只有 Town 获胜。P01 和 P02 都名为 Gift,但不同 ID 使它们的报价保持独立。P04 没有合格规则。保留所有包裹和所有规则,包括不可用的结果。

注册新规则并保留完整报价

本步骤中,你将在不修改比较器的情况下注册一个新的固定价格规则实例,再保存服务台以后可以读取的结果。

继续在 WebIDE 终端的 /home/labex/project/parcels 中操作。费率读取器用 RuleFactory 创建相应实现。因此,另一条 flat 行会创建参数独立的另一个对象。保持原始 data/ 文件不变。cp 创建工作副本;printf 写入文本,\t 表示制表符,\n 表示行结束。>> 向文件追加内容,而不是替换文件。

cp data/rates.tsv work/rates.tsv
printf 'flat\tRelay\t6000\t600\n' >> work/rates.tsv
./quote.sh batch data/parcels.tsv work/rates.tsv

Relay 在包含 6,000 克的范围内合格,收费 600 分。P02 现在有三个并列获胜项,而 6,500 克的 P04 仍没有报价。你改变了注册规则集合,原比较代码继续正常工作。

输出就是实际交付的报价。> 将标准输出保存到文件,替换已有同名文件。只保存合法批次,因为在 Java 验证输入之前,shell 就会打开输出文件:

./quote.sh batch data/parcels.tsv work/rates.tsv > work/quotes.tsv
cat work/quotes.tsv

每个 ID 都必须有一个 PARCEL 行、每条已注册规则对应的一个 QUOTE 行,以及一个 BEST 行;其中包含用逗号分隔的所有合格最低价 ID,或 NONE。金额仍为整数分。规则和获胜项可以遵循注册列表顺序;重要的是成员及其关系完整正确。

非法重量不同于重量合法但没有报价的包裹。提供的无效样例包含零克的 BAD。解析器先验证整个批次,再打印报价;非法批次以状态 2 退出,将解释性错误写入标准错误,不产生任何报价行。有意运行该失败命令时,不要将它重定向到已有的正确报价文件:

./quote.sh batch data/invalid-parcels.tsv work/rates.tsv

预期显示 INVALID_WEIGHT BAD。返回合法包裹文件即可恢复:

./quote.sh batch data/parcels.tsv work/rates.tsv

原始包裹、费率记录和正确的 work/quotes.tsv 都保持完整。遇到编译或路径错误时,修正并保存指定文件,或重新执行 cd /home/labex/project/parcels,然后重试。启动脚本每次都会重新构建已保存的源码,并像上一实验一样明确使用 UTF-8。

检查在临时样例中编译未修改的源码副本,覆盖其他合法参数和接口实现。它们不会补齐现场缺失的结果,也不会重写工作区。这是训练环境中的功能评分,并非防止管理员伪造文件的机制。

总结

你在不编写承运商专属分支的情况下比较接口对象,保留所有并列最低价,并明确表示合法的无报价结果。增加配置规则复用了相同流程。保存的报价文件保留了每个包裹的身份和完整选择集合。