🛡

Kotlinのsealed classで、複雑な医療ドメインをスッキリ管理!

2025/02/26に公開

はじめに

こんにちは!
医療業界のイノベーションを加速させる企業ispecでエンジニアをしている、ほりです!

今回は、弊社がサーバーサイドに採用しているKotlinのsealed classにスポットを当てて、その魅力や使い所を簡単に紹介したいと思います。

https://ispec.tech/

sealed classとは?

https://kotlinlang.org/docs/sealed-classes.html

特徴としては以下が挙げられます。

  1. 同ファイル内でサブクラスを定義
    sealed classのサブクラス (継承元) となるクラスやオブジェクトは、同じファイル内でのみ定義可能です。これにより、継承階層が見えやすく制限された形でまとまります。

  2. 型の安全性を高める
    when式でsealed classを扱う場合、すべてのサブクラスを網羅的に扱うことがコンパイラでチェックされます。そのため、万が一サブクラスを追加したり変更した場合にも漏れなく対応が必要となり、コードの保守性と安全性が高まります。

  3. Enum classとの比較
    enumは列挙子それぞれにプロパティを持たせることができますが、sealed classではそれぞれのサブクラスが自由にプロパティやメソッドを定義できます。より複雑な状態やデータ構造を持ちたい場合はsealed classが有利です。

医療ドメインでのメリット

医療系のソフトウェア、特に電子カルテのようなシステムは、さまざまな状態・種類のデータを安全かつ正確に取り扱う必要があります。例えば、入院時のカルテと外来のカルテは構造や項目が大きく異なる場合がありますし、下書きや承認済みなど状態にもバリエーションがあります。こういったケースにsealed classを使うことで、

  • 状態や種類ごとにクラスを分けつつ、それらが1つの「まとまり」であることを示す
  • すべての「レコードの種類」を網羅的に扱うwhen式を使うことで漏れを防止する
  • コードを追いやすくし、バグが起こりにくい設計を作りやすい

といったメリットがあります。

具体的なコード例

PatientStatusをsealed classで定義してみましょう。
(あくまで例なので、このようなコードは実際には存在しません。)

sealed class PatientStatus {
    // 在院中の患者: 病棟情報や入院日、治療内容などを管理
    data class InHospital(
        val patientId: UUID,
        val wardName: String,
        val admissionDate: LocalDate,
        val treatmentDetails: String? = null
    ) : PatientStatus()

    // 退院済みの患者: 退院日や退院時のサマリー情報などを管理
    data class Discharged(
        val patientId: UUID,
        val dischargeDate: LocalDate,
        val summary: String
    ) : PatientStatus()

    // 治療中の患者: 治療計画や治療開始日などを管理
    data class UnderTreatment(
        val patientId: UUID,
        val treatmentPlan: String,
        val treatmentStartDate: LocalDate
    ) : PatientStatus()

    // 観察中の患者: 観察日やメモなどを管理
    data class UnderObservation(
        val patientId: UUID,
        val observationDate: LocalDate,
        val notes: String? = null
    ) : PatientStatus()
}

when式での扱い

sealed classのサブクラスをwhen式で扱う場合、コンパイラが「すべてのサブクラスを扱えているか」をチェックし、未処理のケースがあれば警告してくれます。これによって、たとえば新しく状態(クラス)を追加したときに、既存のwhen式で処理を見落としていないかを早期に発見できます。

fun processPatientStatus(status: PatientStatus) {
    when (status) {
        is PatientStatus.InHospital -> {
            println("患者 ${status.patientId}${status.wardName} 病棟に在院中です。")
            println("入院日は ${status.admissionDate} です。")
            status.treatmentDetails?.let {
                println("治療内容: $it")
            } ?: println("治療内容の詳細はまだ登録されていません。")
        }
        is PatientStatus.Discharged -> {
            println("患者 ${status.patientId} は退院済みです。")
            println("退院日は ${status.dischargeDate} です。")
            println("退院時のサマリー: ${status.summary}")
        }
        is PatientStatus.UnderTreatment -> {
            println("患者 ${status.patientId} は治療中です。")
            println("治療開始日は ${status.treatmentStartDate} です。")
            println("治療計画: ${status.treatmentPlan}")
        }
        is PatientStatus.UnderObservation -> {
            println("患者 ${status.patientId} は観察中です。")
            println("観察日は ${status.observationDate} です。")
            status.notes?.let {
                println("観察メモ: $it")
            } ?: println("追加の観察メモは登録されていません。")
        }
    }
}

sealed classを使うと何が嬉しいか

  1. ドメインをまとめやすい
    同じドメインに属する複数の種類 (状態) を1つの親クラスで包含できるので、関連するロジックや型を近い場所にまとめられます。

  2. 安全性の高い状態分岐
    上述したwhen式での網羅性チェックのおかげで、抜け漏れのない実装が期待できます。

  3. 複雑な構造・振る舞いをそれぞれのサブクラスに持たせやすい
    enumクラスよりも柔軟にデータやメソッドを持つことができるため、医療系のように情報量が多いドメインで便利です。

  4. メンテナンス性の向上
    新しい種別のカルテや状態が発生しても、その定義と対応処理が近い場所に集約されるため、拡張や変更時に漏れが起こりづらくメンテナンスが容易です。

まとめ

医療系ソフトウェア開発では、ドメインの状態や種類を安全に取り扱うことが非常に重要になります。Kotlinのsealed classを活用すれば、種類が多いが同じドメインであるといったモデルを洗練されたコードで表現でき、漏れの少ない堅牢な実装が可能になります。

  • 同じドメインに属する状態や種類をsealed classにまとめる
  • when式による網羅チェックで漏れのない処理を実現
  • 変更や拡張が容易で安全
ispec inc.

Discussion