Javaのrecord型がもたらす設計ミス
Javaのrecord型がもたらす設計ミス
Java 14で導入された record
は、いわゆる“データ専用クラス”の記述を簡素化する機能として歓迎されました。
一方で、その構文的な簡潔さが設計判断を曖昧にし、責務の誤配置を誘発するケースも少なくありません。
本記事では record
を巡る設計的な誤解を解き、レビューアーとしてどのような指摘が妥当かを整理していきます。
recordとは何か:構文と目的の整理
Javaのrecordは、値の集まり(Value Object)を安全かつ簡潔に表現するための言語構文です。
public record Point(int x, int y) {}
これは以下のようなコードを暗黙的に生成しています:
final
なフィールド定義(x
,y
)- コンストラクタ
- ゲッター(
x()
,y()
) toString()
,equals()
,hashCode()
の実装
recordは「不変なデータの容れ物」としての責務に特化した構造です。副作用を持たず、状態を変更しない構造であることが設計前提となります。
OK設計:recordが本来持つべき責務とは
recordがうまく機能するのは以下のような用途です:
- 外部APIとのデータ交換用DTO
- enum的に使う識別子の集合(例:
Coordinates
) - ビジネスロジックに依存しない不変データの転送・集約
public record UserDto(String name, int age) {}
このように、recordは「構造が意味を持ち、振る舞いを持たない」場合に真価を発揮します。
誤った使い方と設計ミスの例
recordは簡潔な構文のため、過剰なロジックや設計上の責務を押し込まれがちです。以下に典型的なNG例を挙げます。
1. ビジネスロジックをrecordに押し込む
public record Order(int amount, String currency) {
public boolean isFreeShipping() {
return amount > 10000 && "JPY".equals(currency);
}
}
「データ構造」に「条件ロジック」が混ざっており、record
の責務逸脱と判断される設計です。
このような振る舞いはService層やユーティリティに移譲すべきです。
2. DI対象やドメインモデルへの流用
@Component
public record AccountService(AccountRepository repo) {
public void execute() {
// 処理
}
}
recordの構文でクラスを簡単に定義できるため、安易にコンポーネント化してしまう例です。
recordを使うと「コンストラクタ・フィールド・getter」が自動で生成されるため、つい責務を誤認してロジックを混在させる傾向があります。
UMLでみる責務の混在構造
このように、record内部に判定ロジックが存在するだけで、責務の視点からは密結合に近づいてしまいます。
なぜrecordが“設計ミス”につながるのか
構文上の「簡潔さ」が責務設計を曖昧にする
recordの構文はあまりにも書きやすいため、「データを集めるためのクラス」以外の目的でも乱用されがちです。これはクラス設計の粒度を見誤らせ、以下のような問題につながります:
- 本来レイヤ分離されるべきロジックの集約
- 機能追加時の責務の集中(God Object化)
- テストが難しい(副作用が想定しづらい)
recordがService・Entity・DTOの役割を横断し始めると、アーキテクチャ全体に混乱を招きます。
レビューでの指摘ポイント
recordの使用に対して、レビューアーとして注目すべき観点を以下に示します。
チェックリスト:record設計レビュー観点
recordにビジネスロジックが含まれていないかをまず疑ってください。
設計改善パターン
record + ユーティリティ
public record Order(int amount, String currency) {}
public class ShippingUtils {
public static boolean isFreeShipping(Order order) {
return order.amount() > 10000 && "JPY".equals(order.currency());
}
}
責務を明示的に分離することで、recordの役割は「データ構造」に限定され、ユーティリティクラスがロジックを受け持ちます。
ロジックはServiceやUtilに、recordはDTOとして。それぞれの責務が混ざらない構造が理想です。
設計的補足:recordとイミュータブル設計
recordはイミュータブル(不変)であるがゆえに、変更操作が発生する箇所では適さないケースもあります。
例:
- バリデーション後の状態変化(例:
isVerified
→true
) - 一部フィールドだけを変更したいケース
このような場合には、クラスによる柔軟な構造やビルダー・パターンの方が設計的に適していることもあります。
まとめ:recordは“安易な万能型”ではない
recordは構文的に優れているがゆえに、責務の混乱を引き起こしやすい構造でもあります。
レビューアーは以下の3点に特に注意すべきです。
- recordにロジックが入っていないか(構造と振る舞いの混同)
- アーキテクチャ層を越えて使用されていないか
- 変更に弱い構造になっていないか(不変であることの落とし穴)
recordを“構造のみ”に限定して活用することで、Java設計の明快さは保たれます。