【Java学習|豆知識】Javaで実現する堅牢なドメインモデル:Sealed Classesとパターンマッチングの活用術

1. 導入:なぜSealed Classesが重要なのか

Java開発において、状態遷移やドメインモデルの表現は避けて通れない課題です。従来、継承を使って状態を表現する場合、予期せぬサブクラスが作成されるリスクがありました。Java 17で正式導入されたSealed Classes(封印されたクラス)と、Switch Expressions(switch式)を組み合わせることで、代数的データ型(ADT)の概念をJavaに取り入れることができます。これにより、網羅的な条件分岐が可能になり、バグの温床となりがちな「条件の漏れ」をコンパイル時に検知できるという大きなメリットが得られます。

2. 基礎知識:ADTとSealed Classes

ADTとは「直和型(Sum Type)」や「直積型(Product Type)」を組み合わせてデータを表現する手法です。JavaにおいてSealed Classesは、継承できるサブクラスを限定することで「直和型」を表現します。
これにSwitch Expressions(戻り値を持つswitch文)を組み合わせることで、全てのサブクラスを処理しているかどうかがコンパイラによって強制されます。もし新しいサブクラスを追加した際にswitch文でその処理を忘れると、コンパイルエラーになるため、大規模なリファクタリングも安全に行えます。

3. 実装/解決策

Sealed Classesを使用する際は、親クラスに`sealed`キーワードを付け、`permits`句で許可されたサブクラスを明示します。サブクラス側は`final`(継承を禁止)、`sealed`(さらに継承を制限)、または`non-sealed`(制限を解除)のいずれかを指定します。

4. サンプルプログラム

以下のコードは、支払い状態を表現するモデルの例です。

// 支払い状態を定義するSealed Interface
public sealed interface PaymentStatus permits Pending, Completed, Failed {}

// 各状態をレコード(直積型)で定義
record Pending(String orderId) implements PaymentStatus {}
record Completed(String transactionId, double amount) implements PaymentStatus {}
record Failed(String reason) implements PaymentStatus {}

public class PaymentProcessor {
    public String getStatusMessage(PaymentStatus status) {
        // switch式により、全てのケースを網羅しているかコンパイラがチェックする
        return switch (status) {
            case Pending p -> "注文 " + p.orderId() + " は処理待ちです。";
            case Completed c -> "決済完了: " + c.transactionId() + " (金額: " + c.amount() + ")";
            case Failed f -> "エラー発生: " + f.reason();
            // defaultを記述しなくても、全てのPermitsを網羅していればコンパイルが通る
        };
    }
}

5. 応用・注意点

網羅性の恩恵を最大限に受けるため、可能な限り`default`ラベルの使用は避けてください。`default`を書いてしまうと、新しいサブクラスを追加した際にコンパイルエラーが出ず、実行時に意図しない挙動になる可能性があります。

また、外部ライブラリから継承される可能性があるクラスには`non-sealed`を使用しますが、これは「設計上の安全装置」を外す行為であることを理解しておきましょう。堅牢なシステムを構築するには、ドメインの境界を`sealed`で守り、ビジネスロジックをパターンマッチングで記述するスタイルが、現代のJava開発におけるベストプラクティスです。

コメント

タイトルとURLをコピーしました