良いコードに関しての本を最近読んでいました。その中から普段の仕事に生かしていきたいなと思った内容を例を使用しながらまとめます。
使用するサンプル
良くあるお金計算のロジックを例にして、お話しします。
package org.example;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 注文一覧 (値段, 量)
List<List<Integer>> orders = List.of(
List.of(500,5),
List.of(200,6),
List.of(100,4)
);
// 合計金額計算
int sum = 0;
for (List<Integer> order : orders) {
sum += CalcCommon.calcPrice(order.get(0), order.get(1));
}
System.out.println("送料なし " + sum);
// 送料追加
sum += CalcCommon.calcShippingPrice(sum);
System.out.println("送料こみ "+ sum);
}
}
package org.example;
/***
* お金計算の共通ロジック
*/
public class CalcCommon {
static int calcTax(int money) {
return (int) (money * 0.1);
}
static int calcPrice(int money, int quantity) {
return money * quantity;
}
static int calcShippingPrice(int price) {
if (price >= 5000) {
return 0;
}
return 500;
}
}
送料なし 4100
送料こみ 4600
このコードは正しく動きますが、以下のような問題点があります。
データとロジックが分離している。
消費税の計算や送料の計算を例のようにCommonクラスに集める設計のプロジェクトは多いと思います。ですがこのようにデータとロジックが分離していると、すでに実装しているロジックの存在に気づきにくく、複数のエンジニアが似た処理を実装することにつながります。結果として、同一処理が複数の箇所に書かれ修正の難易度が上がる恐れがあります。
不正な値で計算が行われる可能性がある
入力の料金や量にバリデーションが行われておらず、不正な値(負の値など)で計算が行われる可能性があります。
これらの問題を値オブジェクトという考えを使用することで、改善することが可能です。
値オブジェクトとは
アプリケーションで用いられる値をクラスとして作成する設計
アプリケーションでは金額、注文量、電話番号、住所など様々な値が使用されます。一般的には金額はint、電話番号はStringなどプリミティブ型で表現されることが多いです。ただのint型では負の値も扱えてしまいますし、注文量1億なども使用される可能性があります。そこでクラスとして宣言する事で業務的に有効な値のみを対象とすることが出来ます。またクラスとして設計し、データとロジックをまとめることで重複コードの排除にもつながります。
作成していく値オブジェクト
料金、注文量、送料、注文
料金オブジェクト
- コンストラクタで不正な値かをチェック
- finalで宣言しクラス外からの修正を防ぐ
- addでは新しいインスタンスを返す
package org.example;
public class Money {
final int value;
public Money(int value) {
if (value < 0) {
throw new IllegalArgumentException("0以上の値を入力してください");
}
this.value = value;
}
public Money add(Money other) {
return new Money(this.value + other.value);
}
}
注文量オブジェクト
package org.example;
public class Quantity {
static final int MINIMUM_QUANTITY = 1;
static final int MAXIMUM_QUANTITY = 100;
final int value;
public Quantity(int quantity) {
if (isIllegalArgument(quantity)) {
throw new IllegalArgumentException("不正な量です");
}
this.value = quantity;
}
private boolean isIllegalArgument(int quantity) {
return quantity < MINIMUM_QUANTITY || MAXIMUM_QUANTITY < quantity ;
}
}
package org.example;
public class ShippingCost {
final int NO_SHIPPING_COST_THRESHOLD = 5000;
final int REGULAR_SHIPPING_COST = 500;
final Money money;
public ShippingCost(Money money) {
this.money = money;
}
private Money calcShippingCost() {
if (money.value >= NO_SHIPPING_COST_THRESHOLD) {
return new Money(0);
}
return new Money(REGULAR_SHIPPING_COST);
}
public Money includeShippingCost() {
return money.add(calcShippingCost());
}
}
コンストラクタに先ほど作ったMoney,Quantityを使用。
package org.example;
public class Order {
final Money money;
final Quantity quantity;
public Order(Money money, Quantity quantity) {
this.money = money;
this.quantity = quantity;
}
public Money calcPrice() {
return new Money(money.value * quantity.value);
}
}
package org.example;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 注文一覧 (値段, 量)
List<List<Integer>> items = List.of(
List.of(500,5),
List.of(200,6),
List.of(100,4)
);
// 合計金額計算
Money sum = new Money(0);
for (List<Integer> item : items) {
Order order = new Order(new Money(item.get(0)), new Quantity(item.get(1)));
sum = sum.add(order.calcPrice());
}
// 送料なし
System.out.println("送料なし " + sum.value);
// 送料追加
ShippingCost shippingCost = new ShippingCost(sum);
Money includingShippingCost = shippingCost.includeShippingCost();
System.out.println("送料こみ "+ includingShippingCost.value);
}
}
package org.example;
import java.util.List;
public class Orders {
final List<Order> orders;
public Orders(List<Order> orders) {
this.orders = orders;
}
public Money calcSum() {
Money sum = new Money(0);
for(Order order: orders) {
sum = sum.add(order.calcPrice());
}
return sum;
}
}
package org.example;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 注文一覧 (値段, 量)
Orders orders = new Orders(List.of(
new Order(new Money(500), new Quantity(5)),
new Order(new Money(200), new Quantity(6)),
new Order(new Money(100), new Quantity(4))
));
// 合計金額計算
Money sum = orders.calcSum();
// 送料なし
System.out.println("送料なし " + sum.value);
// 送料追加
Money includingShippingCost = new ShippingCost(sum).includeShippingCost();
System.out.println("送料こみ "+ includingShippingCost.value);
}
}