料金計算のような業務ロジック
このレッスンでは、条件がいくつも絡む計算を、後から読んでも仕様が追える形に書く方法を身につけます。
業務ルールは仕様書と同じ順番で書きます
料金計算やポイント付与のようなルールは、条件が3つ4つと重なるとすぐに読めなくなります。次のような書き方は、動いていても直せません。
Java
int price = amount - (type.equals("gold") ? (months >= 12 ? amount * 20 / 100 : amount * 10 / 100)
: type.equals("silver") ? (months >= 12 ? amount * 10 / 100 : amount * 5 / 100) : 0);何が起きているかを読み解くには、括弧を目で数える必要があります。しかも「ゴールドで12か月以上なら20パーセント引き」という仕様の1行が、コードのどこに対応するのかが分かりません。
読める形にする方法は単純です。仕様の一文をコードの一文に対応させることです。
Java
int discountRate(String type, int months) {
if ("gold".equals(type)) {
return months >= 12 ? 20 : 10;
}
if ("silver".equals(type)) {
return months >= 12 ? 10 : 5;
}
return 0;
}行数は増えましたが、仕様の表と1対1で見比べられるようになりました。業務ロジックで優先すべきは短さではなく、仕様との照合しやすさです。
名前を付けた中間変数を惜しまない
もう1つの武器が中間変数です。式の途中に意味のある名前を置くと、その名前自体が説明になります。
Java
int rate = discountRate(type, months);
int discount = amount * rate / 100;
int payable = amount - discount;
int point = payable / 100;payable という名前があるおかげで、ポイントが割引前ではなく割引後の金額から計算されていることが一目で分かります。これを1行にまとめると、その事実がコードから消えます。
端数の扱いは必ず明文化します
金額計算で最も揉めるのが端数です。amount * rate / 100 を int で書くと切り捨てになります。切り上げたいのか、四捨五入したいのか、これは技術の話ではなく業務の決めごとです。曖昧なまま書くと、後で「1円合わない」という調査に何日も溶かすことになります。
| 書き方 | 結果 |
|---|---|
a / b (int どうし) | 0 方向へ切り捨て |
Math.floorDiv(a, b) | 負の値でも小さい方へ切り捨て |
(a + b - 1) / b (正の数) | 切り上げ |
実務では通貨計算に BigDecimal と RoundingMode を使い、丸め方を明示するのが定石です。
境界値を必ずテストに入れます
条件に >= が出てきたら、その境目ちょうどの値を必ずテストします。12 か月以上という仕様なら、11 と 12 の両方を確かめます。> と >= の取り違えは、実装した本人には絶対に見つけられない種類の誤りで、境界値のテストだけが捕まえられます。
Java
@Test
void ゴールド会員は12か月ちょうどで20パーセント() {
assertEquals(20, discountRate("gold", 12));
}
@Test
void ゴールド会員は11か月なら10パーセント() {
assertEquals(10, discountRate("gold", 11));
}この2本が揃っていれば、> に書き換えた瞬間に片方が落ちます。
Spring での置き場所
こうした計算は @Service を付けたクラスに置きます。Controller に置くとテストが重くなり、エンティティに置くと外部の料金表を参照しづらくなることが多いためです。Service に置いておけば、テストは new して呼ぶだけで済みます。フレームワークの機能を1つも使っていないことが、そのまま利点になります。
演習で書くもの
演習では、会員種別と継続利用月数と利用金額から、請求額と付与ポイントを計算します。境界値がテストに含まれているので、>= と > の違いに注意してください。
要件
- 戻り値は rate=<割引率>,payable=<請求額>,point=<付与ポイント> の形にすること
- 割引率は gold と silver のみに適用し、12 か月ちょうどは上位の率が適用されること
- ポイントは割引前ではなく請求額を 100 で割った商とし、継続 24 か月以上なら 2 倍にすること。amount が 0 以下なら全項目 0 とすること
入出力例
charge("gold", 12, 10000) → "rate=20,payable=8000,point=80"
charge("gold", 11, 10000) → "rate=10,payable=9000,point=90"
charge("silver", 12, 10000) → "rate=10,payable=9000,point=90"
charge("silver", 11, 3000) → "rate=5,payable=2850,point=28"
charge("bronze", 30, 5000) → "rate=0,payable=5000,point=100"
charge("gold", 24, 10000) → "rate=20,payable=8000,point=160"
charge("silver", 23, 1050) → "rate=10,payable=945,point=9"
charge("gold", 36, 0) → "rate=0,payable=0,point=0"