📝 はじめに
ソフトウェア開発において、「状態」を扱う場面は数多く存在します
- レスポンスの成功/失敗
- 注文処理のステータス
- アップロードの進行度
状態が次の状態へと遷移していくようなプロダクトも珍しくありません。
こうした状態管理をbooleanやString,intのフラグ値で行うと、
- ありえない状態がコード上で作れてしまう
- 値の打ち間違い・null漏れに気づきにくい
- if/switch だらけで処理が読みにくい
- 状態が増えたときに対応漏れが発生しやすい
……といった落とし穴が待っています。
そこで本記事では、enum × sealed class を組み合わせて、状態を型そのものとして表現する設計パターンを紹介します!
このアプローチを使うと、状態が増えてもロジックが複雑になっても、型が仕様を語り、コンパイルエラーがバグを防いでくれる世界を作れます。
「状態管理をもっと安全に、スマートにしたい」という方にぜひ読んでほしい内容です。
🔐 enum × sealed classで表現する "型安全な状態管理パターン"
この設計パターンでは、enum × sealed classの型表現を組み合わせることにより、
状態を型で表現できる安全なモデルを構築できます!
特に状態が複数あり、それぞれが異なるデータを持つような場合に有効です!
| 要素 | 役割 |
|---|---|
| enum | 状態を「値」として扱えるようにする (保存・比較・フィルタに便利) |
|
sealed interface / sealed class |
派生できる状態をコンパイル時に固定し、「ありえない状態」を排除 |
| record | 状態とデータをまとめて安全に保持 (改変されない不変オブジェクト) |
この3つを組み合わせることで、
- 状態の種類がコード構造として明示的になる
- 状態ごとに持つデータを型レベルで分けられる(不要なフィールドを持たない)
- 新しい状態を追加したときにコンパイルエラーが漏れを教えてくれる
- if/switchの分岐漏れを静的解析により防止できる
といったメリットが得られます!
🧪 実際の例
実際にどのように型で状態を表現し、状態管理をするのか見ていきましょう!
今回はレスポンスという文脈で、Success / Failure の大分類を持ちつつ、
さらにその中で細かい状態が分岐するケースを例にします!
はじめにenumでSuccess / Failure の状態を表現しましょう!
enum Status {
SUCCESS,
FAILURE
}
これでStatusという型には、Success / Failure の2つ値を表現できました!
次に状態を表すインターフェースを用意し、成功系と失敗系をそれぞれ派生させます。
sealed interface ApiResponse permits Success, Failure {}
sealed interface Success extends ApiResponse permits Ok, Created {}
sealed interface Failure extends ApiResponse permits NotFound, Unauthorized {}
次に、各状態を record で表現します。
成功と失敗では保持するデータが異なるため、「成功でもエラー用のメッセージの箱を用意する」などといった不要な項目を持つ必要がありません。
// --- Success系 ---
record Ok(String data) implements Success {
public Status status() { return Status.SUCCESS; }
}
record Created(String id, String location) implements Success {
public Status status() { return Status.SUCCESS; }
}
// --- Failure系 ---
record NotFound(String resource) implements Failure {
public Status status() { return Status.FAILURE; }
}
record Unauthorized(String reason) implements Failure {
public Status status() { return Status.FAILURE; }
}
Ok はレスポンスデータ、Created は新規作成したIDとLocation、
NotFound は見つからなかったリソース名、Unauthorized は理由メッセージを持ちます。
このように、状態ごとに保持する情報を型で分離できるのがポイントです!
ApiResponseを受け取り、ハンドリングする処理はswitch式でパターンマッチングを行います。
public void handle(ApiResponse response) {
switch (response) {
case Ok ok -> System.out.println("OK: " + ok.data());
case Created created -> System.out.println("Created: " + created.id());
case NotFound nf -> System.out.println("Not Found: " + nf.resource());
case Unauthorized una -> System.out.println("Unauthorized: " + una.reason());
// 全ての状態を網羅しているので、defaultは不要
}
}
これにより、新しい状態を追加した際に、分岐漏れがあればコンパイルエラーで気付くことができます!
意味のある型で分岐することで、可読性も高まります!
また、複雑な分岐ではなく、成功と失敗で分岐を分けたいときは、
Status status = switch (response) {
case Success s -> Status.SUCCESS;
case Failure f -> Status.FAILURE;
};
if (status == Status.SUCCESS) {
System.out.println("Success系のレスポンスです");
} else {
System.out.println("Failure系のレスポンスです");
}
statusの値でこのように表現することもできます!
状態が追加されても構造が破綻せず、漏れがあればコンパイルが教えてくれる
これが enum × sealed class の大きな強みです。
🏁 まとめ
enum と sealed class を活用することで、状態をただの値としてではなく、
型として明確に表現し、安全に扱うことができます。
特に今回のように状態が細分化されたり、保持するデータが各状態で異なるケースでは、
- 状態ごとのデータ構造が明確に分離できる
- 不可能な状態やフィールドの混在を型で防止できる
- 新しい状態追加時もコンパイルエラーで漏れに気付ける
- switch構文で分岐の網羅性が保証される
といったメリットがあり、状態管理がぐっと扱いやすくなります。
文字列・数値・boolean で状態を表現するよりも安全で拡張しやすい設計のため、
レスポンス処理に限らず、ステータス管理や状態遷移のあるドメイン全般で有効です!