医療保険を関数型ドメインモデリングしてみよう - エムスリーテックブログ

エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

医療保険を関数型ドメインモデリングしてみよう

【デジカルチーム ブログリレー 4 日目】

こんにちは、デジカルチームでソフトウェアエンジニアをしている穴繁です。 私は、医療ドメインへのディープダイブが時に求められるクラウド型電子カルテというプロダクトの開発に携わっています。

そのため、この複雑な医療ドメインをどうやったら美しく、安全にコードに落とし込めるのだろうかと悩む機会が多いです。 そんな中、関数型ドメインモデリング という書籍と出会い、これはいいアプローチかもしれないと感じました。

ということでこの記事では、比較的皆さんも馴染みがあるであろう「医療保険」を題材に、書籍の中でも特に型によるドメインモデリングに焦点を当ててそのメリットを簡単にお伝えできればと思います。 なお、採用言語としては関数型のアプローチが取り入れられているものであれば何でも良いのですが、書籍に倣って F# で挑戦してみようと思います。本番導入はされていないですし、自分自身書いたこともないのですが、AIの補助に頼りながらやっていきます。

医療保険ってなんだろう

では早速、医療保険について考えていきましょう。 医療保険って一体何なのでしょうか。

まずは、マイナポータルで健康保険証の資格情報を実際に確認してみるのが一番手っ取り早いかもしれません。 そこには、保険者番号・記号・番号・枝番・被保険者の区分といった情報が表示されているはずです。例えば、保険者番号は加入している健康保険の運営元を識別する番号です。

ここまでの情報を元にF#で表現してみるとこんな感じでしょうか。

type HealthInsurance =
    { InsurerNumber: string
      Symbol: string option
      Number: string
      BranchNumber: string option
      InsuredPersonType: string }

値オブジェクトを抽出する

何となく医療保険に必要そうなフィールドは分かりました。 ただ、もちろんこのままでは医療保険のドメインモデルとしては貧弱です。ということでフィールド単位で満たすべきルールについて掘り下げてみましょう。

例えば、保険者番号には次のようなルールがあります。

  • 数値文字列であること
  • 桁数が6桁または8桁であること
  • チェックディジット(番号の入力誤りを検出するための検査用の数値)が妥当であること
  • 8桁かつ先頭が"39"であれば後期高齢者の保険であると判定できる

これらの情報を元に保険者番号を値オブジェクトとして抽出するとこんな感じでしょうか。発生しうるエラーも InsurerNumberError として定義することで、不正な値の投入時に何が起こりうるか型から一目で分かります。

type InsurerNumberError =
    | NotNumeric
    | InvalidLength of actual: int
    | InvalidCheckDigit

module InsurerNumber =

    type T = private | InsurerNumber of string

    let private allowedLengths = Set.ofList [ 6; 8 ]
    let private checkFactors = [ 2; 1; 2; 1; 2; 1; 2; 1 ]

    /// 各桁に係数 [2,1,2,1,...] を掛け、各積の十の位と一の位の和が10の倍数なら妥当
    let private isCheckDigitValid (value: string) =
        let checksum =
            Seq.zip (value |> Seq.map (fun c -> int c - int '0')) checkFactors
            |> Seq.sumBy (fun (digit, factor) ->
                let product = digit * factor
                (product / 10) + (product % 10))

        checksum % 10 = 0

    let create (value: string) : Result<T, InsurerNumberError> =
        if not (value |> Seq.forall (fun c -> c >= '0' && c <= '9')) then
            Error NotNumeric
        elif not (allowedLengths.Contains value.Length) then
            Error(InvalidLength value.Length)
        elif not (isCheckDigitValid value) then
            Error InvalidCheckDigit
        else
            Ok(InsurerNumber value)

    let value (InsurerNumber v) = v

    let isAdvancedElderly (InsurerNumber v) =
        v.Length = 8 && v.StartsWith "39"

他にも、被保険者の区分のように本人または家族の2値しか取らないフィールドも存在します。 この手のフィールドは直和型(Discriminated Union)で表現してあげると良さそうです。

type InsuredPersonType = Self | Family

徐々にそれっぽくなってきましたね。

資格不明な状態を掘り下げる

次に医療保険の状態について考えてみましょう。実は、医療保険では資格不明な状態の考慮が必要です。

例えば、システムトラブルで一時的にオンライン資格確認が困難、災害時など様々なケースが考えられそうです。 電子カルテだと、資格不明なケースにおいてもその理由が求められます。加えて、レセプト(医療機関が保険者に対して医療費を請求するために発行する診療報酬の明細書)の提出時には代替の保険種別(社保・国保・後期高齢者のいずれか)に応じた対応が必要になるため、その把握も必要です。

これはコードでどう表現するのがいいでしょうか? パッと思いつくところだと保険者番号が空の場合には資格不明と見なすとかでしょうか。ただ、そのアプローチでは資格不明かどうかの判定が暗黙的だなとも感じます。

そこで、こういったケースでは先ほども出てきた直和型で次のように書いてあげるといいかもしれません。 F#のような言語ですと資格の分岐処理がパターンマッチで網羅的に書けますし、保険者番号も資格不明理由も同時に入力されているような不正なデータの混入を型で防ぐことができるといったメリットもありそうです。

type UnknownReason = Disaster | VerificationFailed
type FallbackInsuranceType = Social | National | AdvancedElderly

type Qualification =
    | Known of insurerNumber: InsurerNumber.T
    | Unknown of reason: UnknownReason * fallbackType: FallbackInsuranceType

// コンパイラが全ケースの処理を強制する
let handleQualification = function
    | Known n -> printfn "保険者番号 %s で請求" (InsurerNumber.value n)
    | Unknown(r, t) -> printfn "資格不明(理由: %A, 代替: %A)で仮請求" r t

まとめ

この記事では医療保険を簡単にF#で関数型ドメインモデリングしてみました。

最終形はこちらです。

type InsurerNumberError = NotNumeric | InvalidLength of int | InvalidCheckDigit

module InsurerNumber =
    type T = private | InsurerNumber of string
    let create (value: string) : Result<T, InsurerNumberError> = ...

type InsuredPersonType = Self | Family

type UnknownReason = Disaster | VerificationFailed
type FallbackInsuranceType = Social | National | AdvancedElderly

type Qualification =
    | Known of insurerNumber: InsurerNumber.T
    | Unknown of reason: UnknownReason * fallbackType: FallbackInsuranceType

type HealthInsurance =
    { Qualification: Qualification
      InsuredPersonType: InsuredPersonType
      Symbol: InsuranceCardSymbol option
      Number: InsuranceCardNumber
      BranchNumber: InsuranceCardBranchNumber option }

電子カルテへの投入をゴールにする場合にはまだまだ考えなければいけないことは山ほどありますが、最初のプリミティブな型で列挙されていた状態よりはコードから医療保険とは何なのか読み取りやすくなったかなと感じます。

また、状態を直和型で表現することで不正なデータの混入を型レベルで防ぐことができるといったメリットもありました。AIでガンガンコード生成する時代だからこそ、堅牢なドメインモデルのニーズは高まっていると言えるかもしれません。

関数型ドメインモデリングの考え方はF#といった言語に関わらず応用可能かと思います。皆様の参考になれば幸いです。

We are hiring!

エムスリーでは複雑な医療ドメインに飛び込んでみたい方もそうでない方も絶賛大募集中です!

興味を持っていただけた方、カジュアル面談や採用へのご応募をお待ちしています。

jobs.m3.com