java-recipes

ホーム Record › Rec-03

Rec-03: Record vs Class vs Enum の使い分け

Java には「データを保持する」「ビジネスロジックを実行する」「定数を列挙する」という3つの異なる目的に対して、 それぞれ適した仕組みがあります。注文管理の例を使って、Record・Class・Enum の役割分担を理解しましょう。

3つの仕組みの使い分け基準

それぞれの仕組みには明確な「得意なこと」があります。 「どれを使えばいいか迷ったとき」は、下の表の「主な用途」を確認してください。

仕組み主な用途状態典型的な例
record(Java 16+)値の保持・DTO・値オブジェクト不変(イミュータブル)OrderDto、ApiResponse、Point
通常クラス(class)ビジネスロジック・サービス可変状態を持てるOrderService、UserRepository
enum固定の定数セット種類が決まっているOrderStatus、DayOfWeek、Season

判断フロー

  • 「取りうる値が決まっていて、追加されることがほとんどない」 → Enum(PENDING / PROCESSING / SHIPPED など)
  • 「データを保持するだけで、作成後に値を変える必要がない」 → record(Java 16+)または final クラス
  • 「カウンターや状態を持ち、メソッドで変化させる必要がある」 → 通常クラス

サンプルコード

RecordVsClassVsEnumSample.java
import java.util.Objects;

public class RecordVsClassVsEnumSample {

    // === Enum: 定数セット・固定値 ===
    enum OrderStatus {
        PENDING("受付中"),
        PROCESSING("処理中"),
        SHIPPED("発送済み"),
        DELIVERED("配達済み"),
        CANCELLED("キャンセル");

        private final String label;

        OrderStatus(String label) {
            this.label = label;
        }

        public String getLabel() {
            return label;
        }
    }

    // === Class(通常クラス): ビジネスロジック・可変状態 ===
    static class OrderService {
        private int processedCount = 0; // 状態を持つ

        public OrderStatus advance(OrderStatus current) {
            processedCount++;
            if (current == OrderStatus.PENDING) return OrderStatus.PROCESSING;
            if (current == OrderStatus.PROCESSING) return OrderStatus.SHIPPED;
            return current;
        }

        public int getProcessedCount() {
            return processedCount;
        }
    }

    // === Java 8 での record 相当: 値オブジェクト(DTO)===
    static final class OrderDto {
        private final String orderId;
        private final String product;
        private final int quantity;
        private final OrderStatus status;

        public OrderDto(String orderId, String product, int quantity, OrderStatus status) {
            this.orderId = orderId;
            this.product = product;
            this.quantity = quantity;
            this.status = status;
        }

        public String orderId() { return orderId; }
        public String product() { return product; }
        public int quantity() { return quantity; }
        public OrderStatus status() { return status; }

        // 新しい状態の DTO を作成(イミュータブル更新)
        public OrderDto withStatus(OrderStatus newStatus) {
            return new OrderDto(orderId, product, quantity, newStatus);
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof OrderDto)) return false;
            OrderDto dto = (OrderDto) o;
            return quantity == dto.quantity
                && Objects.equals(orderId, dto.orderId)
                && Objects.equals(product, dto.product)
                && status == dto.status;
        }

        @Override
        public int hashCode() {
            return Objects.hash(orderId, product, quantity, status);
        }

        @Override
        public String toString() {
            return "OrderDto{orderId=" + orderId + ", product=" + product
                + ", quantity=" + quantity + ", status=" + status.getLabel() + "}";
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Enum: 注文ステータス ===");
        for (OrderStatus s : OrderStatus.values()) {
            System.out.println(s.name() + " → " + s.getLabel());
        }

        System.out.println("\n=== Class: ビジネスロジック ===");
        OrderService service = new OrderService();
        OrderStatus status = OrderStatus.PENDING;
        status = service.advance(status);
        status = service.advance(status);
        System.out.println("現在のステータス: " + status.getLabel());
        System.out.println("処理回数: " + service.getProcessedCount());

        System.out.println("\n=== DTO: 値オブジェクト ===");
        OrderDto order = new OrderDto("ORD-001", "ノートPC", 2, OrderStatus.PENDING);
        System.out.println(order);
        OrderDto updated = order.withStatus(OrderStatus.PROCESSING);
        System.out.println(updated);
        System.out.println("元の order は変わらない: " + order.status().getLabel());
    }
}

Java 8 では record が使えないため、値オブジェクト(DTO)は final フィールド・コンストラクタ・アクセサ・equals/hashCode/toString を手書きする必要があります。Java 16+ の record ではこれらがすべて自動生成されます。

よくあるミス・注意点

⚠️ すべてを record にする(可変状態が必要な場合は通常クラス)

record は不変(イミュータブル)なので、フィールドの値を後から変更できません。 「処理件数をカウントする」「途中でステータスを更新する」など可変状態が必要な場合は、 通常のクラスを使ってください。record と通常クラスを区別する基準は「作成後に状態が変わるか」です。

⚠️ Enum にビジネスロジックを詰め込みすぎる

Enum はラベル(getLabel())や単純な属性を持つことができますが、 「どのステータスに遷移できるか」のような複雑なビジネスロジックを Enum に追加すると肥大化します。 遷移ルールが複雑になった場合は、OrderService や専用のサービスクラスに切り出しましょう。 Enum は「定数の種類と簡単な属性」に留めるのがすっきりした設計です。

テストする観点

  • withStatus() を呼び出した後、元の OrderDto のステータスが変わっていないこと(不変性の確認)
  • withStatus() が返す新しいインスタンスが元と別オブジェクト(!=)で、ステータスが更新されていること
  • OrderStatus.getLabel() が各ステータスに対して正しい日本語ラベルを返すこと(境界値: PENDING・CANCELLED の両端)
  • OrderService.advance() が PENDING → PROCESSING → SHIPPED の順で遷移すること
  • OrderService.getProcessedCount()advance() を呼ぶたびに正確にカウントアップすること

GitHub でソースコードを見る →