概要
業務システムにおいて「注文は処理中からキャンセルに遷移できるが、完了からは戻せない」といった状態遷移ルールは、仕様書には書かれていてもコード上では if 文の羅列になりがちです。遷移の可否判定が散在すると、条件の追加や変更でバグが入り込みやすくなります。Enum の canTransitionTo メソッドとして遷移ルールを集約すれば、「どの状態からどの状態へ遷移できるか」が一箇所で把握でき、テストも書きやすくなります。この記事では、支払方法のコード値管理、注文ステータスの状態遷移バリデーション、取引種別の借方・貸方分類という3つの業務パターンを Enum で実装します。Java 8 の if-else チェーンと Java 17 の switch 式の違いを対比しながら、業務ロジックを Enum に閉じ込める設計の利点と注意点を整理します。
使いどころ
受注管理画面で注文ステータスの遷移ボタンを表示する際、canTransitionTo で遷移可能な次ステータスだけを選択肢として出す
経理システムで取引種別(購入・返金・振替・調整)ごとに借方・貸方を自動判定し、仕訳データを生成する
決済 API のレスポンスに含まれる支払方法コードを fromCode で Enum に変換し、後続の業務処理に型安全に渡す
コード例
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());
}
}
}Version Coverage
switch 式で遷移可否を1行ずつ宣言的に記述でき、複数ケースのグルーピング(case A, B -> ...)で終端状態の扱いも簡潔になる。
// 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
注意点
状態遷移の canTransitionTo は Enum 側に書くが、実際の遷移実行(DB 更新)はサービス層で行う。Enum は判定だけに留め、副作用を持たせないこと
switch 式で全ケースを列挙する場合、default を書かないほうが要素追加時にコンパイルエラーで検出できる。安易に default を入れると新要素の考慮漏れが隠れる
終端状態(COMPLETED, CANCELLED)から遷移しようとした場合に false を返すか例外を投げるかは業務要件で決める。false を返す設計のほうが呼び出し側の制御が柔軟になる
コード値に String を使う場合、equals での比較が必須。== で比較すると intern されていない文字列で不一致になる
FAQ
全状態 x 全状態のマトリクスをテストするのが確実です。ParameterizedTest で「現在の状態、次の状態、期待結果」の組み合わせを網羅すると漏れを防げます。
default を書いていなければコンパイルエラーになるので、修正箇所を自動検出できます。これが Enum + switch 式の大きな利点です。
Enum の canTransitionTo 内ではなく、サービス層で遷移を実行する箇所でログを出すのが適切です。Enum は純粋な判定に徹させ、副作用を持たせない設計を推奨します。