概要

業務システムにおいて「注文は処理中からキャンセルに遷移できるが、完了からは戻せない」といった状態遷移ルールは、仕様書には書かれていてもコード上では if 文の羅列になりがちです。遷移の可否判定が散在すると、条件の追加や変更でバグが入り込みやすくなります。Enum の canTransitionTo メソッドとして遷移ルールを集約すれば、「どの状態からどの状態へ遷移できるか」が一箇所で把握でき、テストも書きやすくなります。この記事では、支払方法のコード値管理、注文ステータスの状態遷移バリデーション、取引種別の借方・貸方分類という3つの業務パターンを Enum で実装します。Java 8 の if-else チェーンと Java 17 の switch 式の違いを対比しながら、業務ロジックを Enum に閉じ込める設計の利点と注意点を整理します。

使いどころ

受注管理画面で注文ステータスの遷移ボタンを表示する際、canTransitionTo で遷移可能な次ステータスだけを選択肢として出す

経理システムで取引種別(購入・返金・振替・調整)ごとに借方・貸方を自動判定し、仕訳データを生成する

決済 API のレスポンスに含まれる支払方法コードを fromCode で Enum に変換し、後続の業務処理に型安全に渡す

コード例

EnumFinancialExample.java
import java.util.Arrays;

public class EnumFinancialExample {

    // 支払方法: コード値とラベルを持つ Enum
    enum PaymentMethod {
        CREDIT("01", "クレジットカード"),
        BANK_TRANSFER("02", "銀行振込"),
        E_MONEY("03", "電子マネー"),
        CASH("04", "現金");

        private final String code;
        private final String label;

        PaymentMethod(String code, String label) {
            this.code = code;
            this.label = label;
        }

        public String getCode() { return code; }
        public String getLabel() { return label; }

        public static PaymentMethod fromCode(String code) {
            return Arrays.stream(values())
                .filter(m -> m.code.equals(code))
                .findFirst()
                .orElseThrow(() ->
                    new IllegalArgumentException("不明な支払コード: " + code));
        }
    }

    // 注文ステータス: 状態遷移バリデーション付き
    enum OrderStatus {
        PENDING("受付中"),
        PROCESSING("処理中"),
        COMPLETED("完了"),
        CANCELLED("キャンセル");

        private final String label;

        OrderStatus(String label) { this.label = label; }
        public String getLabel() { return label; }

        // switch 式で遷移可否を判定
        public boolean canTransitionTo(OrderStatus next) {
            return switch (this) {
                case PENDING    -> next == PROCESSING || next == CANCELLED;
                case PROCESSING -> next == COMPLETED  || next == CANCELLED;
                case COMPLETED, CANCELLED -> false;
            };
        }
    }

    // 取引種別: 借方・貸方の分類
    enum TransactionType {
        PURCHASE("PUR", "購入", true),
        REFUND("REF", "返金", false),
        TRANSFER("TRF", "振替", true),
        ADJUSTMENT("ADJ", "調整", false);

        private final String code;
        private final String label;
        private final boolean debitSide;

        TransactionType(String code, String label, boolean debitSide) {
            this.code = code;
            this.label = label;
            this.debitSide = debitSide;
        }

        public String getCode() { return code; }
        public String getLabel() { return label; }
        public boolean isDebitSide() { return debitSide; }
    }

    public static void main(String[] args) {
        // コード値からの逆引き
        var method = PaymentMethod.fromCode("02");
        System.out.println("支払方法: " + method.getLabel());

        // 状態遷移バリデーション
        var status = OrderStatus.PENDING;
        if (status.canTransitionTo(OrderStatus.PROCESSING)) {
            status = OrderStatus.PROCESSING;
            System.out.println("→ " + status.getLabel());
        }
        // 完了後は遷移不可
        var completed = OrderStatus.COMPLETED;
        System.out.println("完了→処理中: "
            + completed.canTransitionTo(OrderStatus.PROCESSING)); // false

        // 取引種別の表示
        for (var type : TransactionType.values()) {
            System.out.printf("code=%s label=%s debit=%b%n",
                type.getCode(), type.getLabel(), type.isDebitSide());
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

switch 式で遷移可否を1行ずつ宣言的に記述でき、複数ケースのグルーピング(case A, B -> ...)で終端状態の扱いも簡潔になる。

Java 17
// Java 17: switch 式で宣言的に記述
public boolean canTransitionTo(OrderStatus next) {
    return switch (this) {
        case PENDING    -> next == PROCESSING || next == CANCELLED;
        case PROCESSING -> next == COMPLETED  || next == CANCELLED;
        case COMPLETED, CANCELLED -> false;
    };
}

Library Comparison

標準 Enum(状態遷移メソッド)状態の種類が固定的で、遷移ルールがコンパイル時に確定しているとき。遷移ルールの変更にはコード修正と再デプロイが必要。動的なワークフロー定義には不向き。
Spring Statemachine状態遷移が複雑で、イベント駆動のワークフローが必要なとき。学習コストとライブラリ依存が大きい。単純な遷移判定だけなら過剰。
DB テーブルによる遷移マトリクス遷移ルールを運用中に変更したい、または権限別に遷移可否を制御したいとき。コンパイル時チェックが効かず、不整合の検出がテストまで遅延する。

注意点

状態遷移の canTransitionTo は Enum 側に書くが、実際の遷移実行(DB 更新)はサービス層で行う。Enum は判定だけに留め、副作用を持たせないこと

switch 式で全ケースを列挙する場合、default を書かないほうが要素追加時にコンパイルエラーで検出できる。安易に default を入れると新要素の考慮漏れが隠れる

終端状態(COMPLETED, CANCELLED)から遷移しようとした場合に false を返すか例外を投げるかは業務要件で決める。false を返す設計のほうが呼び出し側の制御が柔軟になる

コード値に String を使う場合、equals での比較が必須。== で比較すると intern されていない文字列で不一致になる

FAQ

状態遷移のテストはどう書くべきですか。

全状態 x 全状態のマトリクスをテストするのが確実です。ParameterizedTest で「現在の状態、次の状態、期待結果」の組み合わせを網羅すると漏れを防げます。

Enum の要素を追加したとき、switch 式は修正が必要ですか。

default を書いていなければコンパイルエラーになるので、修正箇所を自動検出できます。これが Enum + switch 式の大きな利点です。

状態遷移のログはどこで出すべきですか。

Enum の canTransitionTo 内ではなく、サービス層で遷移を実行する箇所でログを出すのが適切です。Enum は純粋な判定に徹させ、副作用を持たせない設計を推奨します。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます