TL;DR

  • ビジネスルールを型システムだけで文書化すれば、ルールが守られていることをコンパイラが確かめてくれる。
  • フラグではなく選択型で状態を分け、プライベートなコンストラクタと組み合わせて、不正な状態を表現できないようにする。

内容

ビジネスルールを型システムだけで文書化すると、ルールが守られていることをコンパイラが確かめられる。コンパイル時に明らかになるので、そのための単体テストがいらなくなる。

検証済みのメールアドレス

顧客のメールアドレスが有効かどうかを、型システムで表す例を考える。メールアドレスには、検証済み(顧客が検証メールの確認リンクをクリックした)と未検証の 2 つの状態がある。そして、次のビジネスルールがある。

  • 検証メールは、未検証のメールアドレスにだけ送る
  • パスワードリセットのメールは、検証済みのメールアドレスにだけ送る

フラグで表すと、次のようになる。

type CustomerEmail = {
    EmailAddress: EmailAddress
    IsVerified: bool
}

この方法では、IsVerified がいつ何のために設定・解除されるのかが分からない。検証されていないメールアドレスに、誤って true を設定してしまうおそれもある。

そこで、検証済みと未検証のメールアドレスを別々のドメインの概念として扱い、選択型(判別共用体)で表す。

type CustomerEmail =
    | Unverified of EmailAddress
    | Verified of VerifiedEmailAddress

ポイントは、VerifiedEmailAddress のコンストラクタをプライベートにして、検証サービスだけが作れるようにすることである。

type VerifiedEmailAddress = private VerifiedEmailAddress of EmailAddress

こうすると、不正な状態を表現できなくなる。ドメインもより正確に文書化できる。たとえば「パスワードリセットのメールは、検証済みのメールアドレスにだけ送る」というルールは、関数の型にそのまま表れる。

type SendPasswordResetEmail = VerifiedEmailAddress -> ...

メールアドレスか住所のどちらかを持つ

「顧客はメールアドレスか住所を持っていなければならない」というルールを考える。このルールは、顧客が次のどれかを持つことを意味する。

  • メールアドレスだけ
  • 住所だけ
  • メールアドレスと住所の両方

これは選択型で表せる。

type BothContactMethods = {
    Email: EmailContactInfo
    Address: PostalContactInfo
}
 
type ContactInfo =
    | EmailOnly of EmailContactInfo
    | AddressOnly of PostalContactInfo
    | EmailAndAddr of BothContactMethods
 
type Contact = {
    Name: Name
    ContactInfo: ContactInfo
}

キーワード