#blog [https://fsharpforfunandprofit.com/series/designing-with-types/](https://fsharpforfunandprofit.com/series/designing-with-types/) F# を使って、型をどのように設計に取り入れるかという話を書いたシリーズ ここでいう設計のスコープはアーキテクチャレベルではなく、もっと詳細なクラスの実装やドメインモデリングの部分 本書の中では、以下のように書いてる > The series will be focused on the "micro level" of design. That is, working at the lowest level of indivisual types and functions 内容と目次はこんな感じ [1. Designing with types: Introduction](https://fsharpforfunandprofit.com/posts/designing-with-types-intro/) Making design more transparent and improving correctness [2. Designing with types: Single case union types](https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/) Adding meaning to primitive types [3. Designing with types: Making illegal states unrepresentable](https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/) Encoding business logic in types [4. Designing with types: Discovering new concepts](https://fsharpforfunandprofit.com/posts/designing-with-types-discovering-the-domain/) Gaining deeper insight into the domain [5. Designing with types: Making state explicit](https://fsharpforfunandprofit.com/posts/designing-with-types-representing-states/) Using state machines to ensure correctness [6. Designing with types: Constrained strings](https://fsharpforfunandprofit.com/posts/designing-with-types-more-semantic-types/) Adding more semantic information to a primitive type [7. Designing with types: Non-string types](https://fsharpforfunandprofit.com/posts/designing-with-types-non-strings/) Working with integers and dates safely [8. Designing with types: Conclusion](https://fsharpforfunandprofit.com/posts/designing-with-types-conclusion/) A before and after comparison F# は読みづらいので Kotlin で直して書く # Summary 大雑把には、3つのテーマを話している (3つの分類は自分で勝手にわけた) まずは、以下のような class を想定する kotlin ``` data class Contact( val firstName: String, val middleInitial: String, val lastName: String, val emailAddress: String, // `emailAddress` がユーザーによって確認済みであれば true val isEmailVerified: Boolean, val address1: String, val address2: String, val city: String, val state: String, val zip: String, // address service によりバリデーションが行われていれば true val isAddressVerfied: Boolean ) ``` 以下3つのテーマでこのクラスやこのクラスを取り巻く環境を改善していく ## 大きな構造は意味のある単位で分割せよ 例えば、 `Contact` クラスの `zip` と `address1` は同時に必ず更新されそうである。 一方で、 `emailAddress` と `firstName` の変更は独立していそうである。 他にも `isEmailVerfied` は `emailAddress` の変更と同じタイミングで初期化されないといけなさそうでさる。 こういった意味のある単位のものは、その単位がわかるような構造にすべき kotlin ``` data class Contact( val name: PersonalName, val emailContactInfo: EmailContactInfo, val postalContactInfo: PostalContactInfo ) data class PersonalName( val firstName: String, val middleInitial: String, val lastName: String ) data class EmailContactInfo( val emailAddress: String, // `emailAddress` がユーザーによって確認済みであれば true val isEmailVerified: Boolean ) data class PostalContactInfo( val address: PostalAddress, // address service によりバリデーションが行われていれば true val isAddressVerfied: Boolean ) data class PostalAddress( val address1: String, val address2: String, val city: String, val state: String, val zip: String ) ``` こうすることにより、変化の単位が明示的になるし、インスタンスレベルで切り替えるみたいな機構も用意しやすくなる。 Immutability と組み合わせるとよりこの辺の旨みが増しそう ## 状態の表現には直和型を利用せよ `Contact` クラスに「 `Contact` はメールアドレスもしくは住所を必ず持たないといけない」という新しい要求が出てきた場合には、このクラスで十分それが表現できているだろうか。 kotlin ``` data class Contact( val name: PersonalName, val emailContactInfo: EmailContactInfo, val postalContactInfo: PostalContactInfo ) ``` ↑では必ず メールアドレスも住所も持たないといけないので Nullable にしてみる↓ kotlin ``` data class Contact( val name: PersonalName, val emailContactInfo: EmailContactInfo?, val postalContactInfo: PostalContactInfo? ) ``` こうすると今度は、メールアドレスも住所も持たない状態を作れてしまうという問題を抱えることになった。 よくよく考えると要求上存在する状態は以下の3つしかないことに気づく - メールアドレスのみを持っている - 住所だけを持っている - メールアドレスと住所を持っている ここで直和型の登場である。 [https://qiita.com/ymtszw/items/dff02ad6350032688676#sum-types-直和型](https://qiita.com/ymtszw/items/dff02ad6350032688676#sum-types-直和型) も参考になる kotlin ``` // ContactInfo は、 EmailOnly | PostOnly | EmailAndPost しか存在しないことを表現してる // see also https://kotlinlang.org/docs/sealed-classes.html sealed class ContactInfo data class EmailOnly( val emailContactInfo: EmailContactInfo ): ContactInfo() data class PostOnly( val postalContactInfo: PostalContactInfo ): ContactInfo() data class EmailAndPost( val emailContactInfo: EmailContactInfo, val postalContactInfo: PostalContactInfo ): ContactInfo() ``` これで明示的にも論理的にも3つの状態しか取れないことを表現できた さらに状態遷移について考えると、この直和型が設計としてよいことがわかる `EmailContactInfo` は、 `isEmailVerfied` というフラグで状態を管理している。 kotlin ``` data class EmailContactInfo( val emailAddress: String, // `emailAddress` がユーザーによって確認済みであれば true val isEmailVerified: Boolean ) ``` フラグでの状態管理は、2つの状態が存在するということでもあり、ここにも直和型が使える。 今回の場合には、 `VerifiedData` と `UnverfiedData` の2つの型を作る kotlin ``` sealed class EmailContactInfo data class VerifiedData( val emailAddress: String, val verifiedDateTime: LocalDateTime ): EmailContactInfo() data class UnverfiedData( val emailAddress: String ): EmailContactInfo() ``` これで `ContactInfo` と同様に2つの状態を表現することができた。 さらにこの2つの状態には状態の遷移があることに気づく。 初期作成 -> `UnverfiedData` -> `VerifiedData` という形で一方通行な状態遷移がある。 こういった状態遷移の State Machine を 直和型 とパターンマッチに相当するような言語機能で実現することができる ※ Kotlin の場合には、 `when`式が sealed class の網羅性を保証してくれるのでそれを利用する。 kotlin ``` // 初期状態 fun create(emailAddress: String): EmailContactInfo { return UnverfiedData(emailAddress) } // 状態遷移関数 fun verfied(emailContactInfo: EmailContactInfo, now: LocalDateTime): EmailContactInfo { // when で return type が確定していると全てのパターンを網羅する必要がある return when(emailContactInfo) { is VerifiedData -> emailContactInfo // そのまま返す is UnverfiedData -> VerifiedData(emailContactInfo.emailAddress, now) } } ``` このような実装の場合には、新たに `EmailContactInfo` が増えた場合にはコンパイルエラーを引き起こすため安全で明示的になる。 こういった直和型の適応は、フラグ管理だけではなく、同じように switch/case や if での条件判定など幅広いところに適用できるパターン。 ## Primitive型ではなく限定した型を使う kotlin ``` data class PersonalName( val firstName: String, val middleInitial: String, val lastName: String ) ``` `firstName` を String としているが、これは本当にStringなのだろうか。 ビジネスドメイン観点から考えると、例えば、`firstName` の最大文字数は100かもしれない。タブや改行コードなどが入ってはいけないかもしれない。などなどよく考えると制約の観点でStringよりも狭いことがわかる。 こういった制約を満たす方法として、例えばDBヘの書き込みの直前に assertion などをすることもできるが、そうするとバリデーションロジックがいたるところに点在してしまうので使うところではなく作るところ (Construct時) で保証ですることは一般的によいプラクティスである。 kotlin ``` data class String100( val s: String ) { // イメージ init { require(s.length <= 100) } } data class String50( val s: String ) { // イメージ init { require(s.length <= 50) } } ``` しかしこのままだと `String100` と `String50` は比較などができなくなってしまうため包括的な Interface を用意するなどが必要になったりして、割とこのあたりはどこまでやるかは難しい。 また別のケースとして、ショッピングカートのアプリで `OrderId` と `CustomerId` がどちらも Int な時にそれぞれを明確に型として区別して比較や演算などを制限することもできる。 このあたりは、ドメインとして本当にこの辺が重要であるということが明確な場合に使うなど用法要領が大事。