発送ルールに共通のインターフェースを設ける

JavaBeginner
オンラインで実践に進む

はじめに

小さな発送窓口では、異なる配送ルールの見積もりを比較する必要があります。用意された料金データでは、500 グラムの荷物は Town で 600 セント、Steps で 200 セントです。この通常の見積もりを実際に動くようにし、次に料金が変わる重量の境界を調べます。

この実験は Java のクラス、コンストラクタ、メソッド、リスト、基本的なポリモーフィズムを前提とします。新たにインターフェースと、適用の可否を明示する見積もり結果を学びます。パーサーと起動スクリプトは提供済みで、実際の適用条件と料金判断は自分で実装します。作成者の想定所要時間には、読解と通常の修正が含まれます。

固定料金ルールに公開契約を設ける

このステップでは、Town ルールが条件を満たす荷物の実際の料金を返し、重量超過の荷物を拒否するようにします。

WebIDE タブを開き、メニューから Terminal → New Terminal を選んで統合ターミナルを開きます。以下のコマンドはこの WebIDE ターミナルで実行してください。各コマンドは同じシェルで動き、cd はそのシェルの作業ディレクトリを変更します。ターミナルを開き直した場合は、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 は 2 行の JAVA の間のテキストを > の後のパスへ書き込みます。開始マーカーの引用符は、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 は現在のディレクトリにある起動スクリプトを実行します。src/*.java を javac -encoding UTF-8 で一時ディレクトリへコンパイルし、UTF-8 のテキストエンコーディングで Java を起動します。毎回、保存したソースを再コンパイルするため、古いクラスファイルは再利用しません。コンパイルエラーにはファイル名と行番号が出ます。該当ファイルを修正・保存し、同じコマンドを再実行します。パスが間違っていればファイルエラーになります。環境をリセットせず、示された cd をやり直し、ファイル名を確認してください。

./quote.sh quote 500

quote コマンドはグラム単位の重量を受け取り、各ルール ID とセント金額、または UNAVAILABLE を表示します。Town は 600 になるはずです。Steps は初期メソッドが意図的に未完成なので、まだ 0 を表示します。実際の無料見積もりと解釈しないでください。

上限重量と、それより 1 グラム重い場合を試します。

./quote.sh quote 2000
./quote.sh quote 2001

Town は 600 から UNAVAILABLE に変わります。data/rates.tsv を変更せず、上限を含む境界を維持できました。このステップのチェック後に進んでください。

開始した重量段階ごとに料金を計算する

このステップでは、浮動小数点の金額ではなく整数演算を使い、同じインターフェースを通して第 2 のルールを実装します。

引き続き WebIDE ターミナルの /home/labex/project/parcels で操作します。開始した 500 グラムの段階ごとに 200 セントかかります。1–500 グラムは 1 段階、501–1,000 は 2 段階、1,001–1,500 は 3 段階です。そのため 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

次に、1 つの段階境界と適用可能な重量の境界を越えてみます。

./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 を表示します。次の実験で扱う、実際の同額最安値ができました。条件と料金の判断は交換可能なルールオブジェクト内にあり、呼び出し側は引き続きインターフェースだけを使っています。

まとめ

1 つの Java インターフェースの背後に 2 種類のルールを実装し、適用可否と整数セント金額を分離して、正確な上限と開始した段階の切り上げを確認しました。同じ呼び出し側が実装を知らずに有用な見積もりを得られるようになりました。