Javaのrecord型がもたらす設計ミス

Java 14で導入された record は、いわゆる“データ専用クラス”の記述を簡素化する機能として歓迎されました。
一方で、その構文的な簡潔さが設計判断を曖昧にし、責務の誤配置を誘発するケースも少なくありません。

本記事では record を巡る設計的な誤解を解き、レビューアーとしてどのような指摘が妥当かを整理していきます。

recordとは何か:構文と目的の整理

Javaのrecordは、値の集まり(Value Object)を安全かつ簡潔に表現するための言語構文です。

Record構文の基本
public record Point(int x, int y) {}

これは以下のようなコードを暗黙的に生成しています:

  • final なフィールド定義(x, y
  • コンストラクタ
  • ゲッター(x(), y()
  • toString(), equals(), hashCode() の実装
recordの役割

recordは「不変なデータの容れ物」としての責務に特化した構造です。副作用を持たず、状態を変更しない構造であることが設計前提となります。

OK設計:recordが本来持つべき責務とは

recordがうまく機能するのは以下のような用途です:

  • 外部APIとのデータ交換用DTO
  • enum的に使う識別子の集合(例:Coordinates
  • ビジネスロジックに依存しない不変データの転送・集約
OK設計例:DTOとしてのrecord
public record UserDto(String name, int age) {}

このように、recordは「構造が意味を持ち、振る舞いを持たない」場合に真価を発揮します。

誤った使い方と設計ミスの例

recordは簡潔な構文のため、過剰なロジックや設計上の責務を押し込まれがちです。以下に典型的なNG例を挙げます。

1. ビジネスロジックをrecordに押し込む

NG: 振る舞いを含むrecord
public record Order(int amount, String currency) {
    public boolean isFreeShipping() {
        return amount > 10000 && "JPY".equals(currency);
    }
}

「データ構造」に「条件ロジック」が混ざっており、record の責務逸脱と判断される設計です。
このような振る舞いはService層やユーティリティに移譲すべきです。

2. DI対象やドメインモデルへの流用

NG: DI対象にrecordを使用
@Component
public record AccountService(AccountRepository repo) {
    public void execute() {
        // 処理
    }
}

recordの構文でクラスを簡単に定義できるため、安易にコンポーネント化してしまう例です。

記法の簡潔さに惑わされない

recordを使うと「コンストラクタ・フィールド・getter」が自動で生成されるため、つい責務を誤認してロジックを混在させる傾向があります。

UMLでみる責務の混在構造

UML Diagram

このように、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の役割は「データ構造」に限定され、ユーティリティクラスがロジックを受け持ちます。

DTOとロジックを明確に分離

ロジックはServiceやUtilに、recordはDTOとして。それぞれの責務が混ざらない構造が理想です。

設計的補足:recordとイミュータブル設計

recordはイミュータブル(不変)であるがゆえに、変更操作が発生する箇所では適さないケースもあります。

例:

  • バリデーション後の状態変化(例:isVerifiedtrue
  • 一部フィールドだけを変更したいケース

このような場合には、クラスによる柔軟な構造やビルダー・パターンの方が設計的に適していることもあります。

まとめ:recordは“安易な万能型”ではない

recordは構文的に優れているがゆえに、責務の混乱を引き起こしやすい構造でもあります。
レビューアーは以下の3点に特に注意すべきです。

  • recordにロジックが入っていないか(構造と振る舞いの混同)
  • アーキテクチャ層を越えて使用されていないか
  • 変更に弱い構造になっていないか(不変であることの落とし穴)

recordを“構造のみ”に限定して活用することで、Java設計の明快さは保たれます。