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
}