Utilやめたい
アプリケーションを開発する際に、アプリ内で頻繁に使用するような汎用的な業務ロジック(数値の計算、日付の計算、文字列の加工など)が必要になった場合、とりあえずでUtilとかHelperのようなクラスを作りがち。
けど、最近の設計の潮流(ドメイン駆動設計界隈)を見ていると、Utilとかはあまり作らずドメインモデルとして作るのが良いのだろうと思う。
事業年度を取得したい
今関わっている案件で実際にあった要件を元に、サンプルのコードをみながら最善の方法を考えてみたい。
ちなみに言語はJavaです。
要件:指定された年月から、事業年度を取得したい
例えば、3月末決済の企業の場合、年度開始は4月になるので、事業年度の取得は以下のようになる想定。
- 2024年3月 → 2023年度
- 2024年4月 → 2024年度
- 2025年3月 → 2024年度
- 2025年4月 → 2025年度
Utilクラスで上記の要件を満たすように最低限の実装をしたのが以下。
public class FiscalYearUtil {
private static final int START_MONTH = 4;
public static int getFiscalYear(int year, int month) {
return month >= START_MONTH ? year : year - 1;
}
}
単体テストを作って実行したらクリアしたので、うまく動いているように見える。
class FiscalYearUtilTest {
@Test
public void 年と月を受け取ってその年度を返す() {
assertEquals(2023, FiscalYearUtil.getFiscalYear(2024, 3));
assertEquals(2024, FiscalYearUtil.getFiscalYear(2024, 4));
assertEquals(2024, FiscalYearUtil.getFiscalYear(2025, 3));
assertEquals(2025, FiscalYearUtil.getFiscalYear(2025, 4));
}
}
ということで、最低限の実装はできたけど、このような実装は色々と問題がありそう。
Utilの問題点
まず、メソッドの引数の型がint型のため、intの範囲内の整数を何でも受け取れてしまう。
極端な話、年や月として適切な値でなかったとしても実行できてしまう。
FiscalYearUtil.getFiscalYear(10, -5); // 結果:9
また、2つの引数の型がそれぞれint型なので、順番が入れ替わってしまってもエラーなく動いてしまう。
FiscalYearUtil.getFiscalYear(7, 2024) // 結果:7
このようなバグは実装時にはなかなか気づきにくい。
このメソッドを使うときにミスしないように注意しましょう、と注意書きを残すことはできるけれど、人間は絶対にミスをするので、潜在的なバグにつながりそうでリスクがある。
一応、以下のようにgetFiscalYear
メソッドにバリデーションチェックを入れて不正な値の時は例外を投げるようにしておけば、不正な値を入れた場合のミスはプログラム実行時にある程度気づける。
public static int getFiscalYear(int year, int month) {
// 引数の値のチェック
if(エラーの場合) {
throw new IllegalArgumentException("引数が不正です");
}
return month >= START_MONTH ? year : year - 1;
}
けど、値を取得するためだけのメソッドで様々な入力パターンのチェックロジックを入れるのはなんかイケてない。
良いメソッドのコードは、中身の実装を読まずともメソッド名を見ただけで何ができるのかを知れる方が良い。
getFiscalYear
というメソッド名の場合、メソッド名だけでチェックロジックが入っていることを推測する人はいないだろうと思われる。
仮にチェックロジックを入れる場合、単体テストである程度パターンを網羅できれば品質も保てそうだけど、パターンに漏れが出てしまいそうだし、メソッドの可読性も落ちそう。
うまく責務を分離させ、安全なプログラムになることを保証しつつ、可読性・保守性の高いコードにしたい。
引数用のクラスを作る
引数のミスをコンパイル時点で気づかせ、バグを減らすには、intではない専用の型(クラス)を定義すればよい。
recordクラスを使って、コンストラクタで値をチェックする。
文字列で受け取ってインスタンスを作るメソッドも用意。
年のクラス
public record Year(int value) {
// コンストラクタ
public Year {
// 最小値は要検討
if (value < 1) {
throw new IllegalArgumentException("年は1以上で指定してください");
}
}
public Year of(String year) {
return new Year(Integer.parseInt(year));
}
}
月のクラス
public record Month(int value) {
public Month {
if (value < 1 || value > 12) {
throw new IllegalArgumentException("月は1から12の間で指定してください");
}
}
public Month of(String month) {
return new Month(Integer.parseInt(month));
}
}
年度計算用のメソッドもそれぞれの型を引数に取るように修正。
public class FiscalYearUtil {
private static int START_MONTH = 4;
public static int getFiscalYear(Year year, Month month) {
return month.value() >= START_MONTH ? year.value() : year.value() - 1;
}
}
// 呼び出し
FiscalYearUtil.getFiscalYear(new Year(2024), new Month(7)); // 2024
これで引数の順番を間違えたり、不正な値が入っても気づかず動いてしまう潜在的なバグは減るだろうと思われる。
ただ、yearとmonthはオブジェクトになったことで、引数にnullが渡される可能性出てしまった。
仮にどちらかの引数にnullが渡された場合、NullPointerExceptionが発生する可能性があり、安全に使用されるようにするには結局チェックロジックの実装が必要になりそうである。
Utilをなくす
そもそもUtilでロジックを実装しようとするからチェックが必要になってしまう。
どうせならYearMonthクラスを作って、オブジェクト自身に事業年度を計算させることで解決できる。
また、そっちの実装の方がオブジェクト指向の良さが活かされている気がする。
public record YearMonth(Year year, Month month) {
public YearMonth {
if (year == null || month == null) {
throw new IllegalArgumentException("年と月を指定してください");
}
}
public Year getFiscalYear() {
return month.value() >= 4 ? year : new Year(year.value() - 1);
}
}
// 呼び出し
new YearMonth(new Year(2024), new Month(6)).getFiscalYear(); // Year(2024)
改善された点
- 実装時のミスが減る
- intではなく専用の型を作ったことで、不正な値が渡されたり、引数の順番の間違いを減らすことができる
- 責務が明確になり、個々の実装が簡潔になる
- Year, Month, YearMonthのそれぞれでチェックを実装したことで、個々のチェックが簡潔になり、かつ年度取得のメソッドではチェックが不要になった
Javaとドメインモデルの相性
Javaはオブジェクト指向言語の中でも型の概念がしっかりしているので、今回の例のようなモデリングには向いているなと思った。
特に、Java 16以降であればrecordクラスが使えるので、かなりドメイン駆動設計のモデリングがしやすくなったなと思う。
補足(Value Object)
ここでのサンプルで出てきたYearやMonthのような、値を1つだけ持つオブジェクトは一般にValueObject(略してVO)と呼ばれる。
VOはイミュータブル(不変性。一度オブジェクトが生成されたらそれ、以後値が変わらない)であることが重要。
イミュータブルであることが、コードの可読性向上や潜在的なバグを減らすことにつながる。
Javaの場合、recordクラスが使えるようになったことでイミュータブルなオブジェクトを簡単に作れるようになったので、積極的に活用していきたい。
Javaの課題
YearMonthクラスでnullチェックを入れているのは、やはりJavaの面倒なところだなと思う。
最近はデフォルトがnull安全になっている言語も多いので(Kotlinとか、TypeScriptとか)、そういう言語の方がより安全に、かつ簡単に開発ができそう。
また、recordクラスによってクラス定義の記述量をかなり減らせるようにはなったけれど、クラスごとにファイルを分けて作るのはファイル数が多くなるので、そこで認知コストが増えるのは悩ましいなと思う。Kotlinだと1つのファイルに複数のクラスを定義できるので、関連性の強いクラスは1つのファイルにまとめることもできる。そういう柔軟性があるとより楽に実装ができそうだなと思う。
それから、Javaではインスタンス生成時にnew
が必要になるので、VOを使った実装をするとコードにnew
が多くなって若干読みずらいなと思った。
AIによる解決
そんな感じで、Javaは最近の比較的新しい言語に比べると記述量やチェック観点(nullチェックなど)が多い感は否めない。
ただ、現在はAI補完により人間側のコーディングする量をかなり減らしつつ、チェック漏れにも気づきやすくなったので、記述量という観点ではAIのかつようによってほかの言語との差異をかなり埋められるようになった気もしている。
(とはいえ、Javaがもっと便利になってくれることに期待)
補足
Utilをやめたいと書きましたが、Utilクラスを完全になくして0にした方が良いとは思っていません。
ビジネスロジックとはあまり関係のない、プログラム側の都合になる処理で共通化したい部分に関しては、UtilやHelperのようなクラスも必要かなと思います。
あくまでビジネスロジックに関係するような特有の処理を実装する際は、Utilにあまり頼らず作っていきたい。