詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Domain Modeling Made Functional を読んだ

最近フロントエンドに限らず TypeScript を書くことが多くなって、これでそれなりの規模のサーバサイドアプリケーションを書くときどうなるんだろう、と気になって読んでみた。いわゆる普通のオブジェクト指向ではなく関数指向な書き方でいきたいとき、どうするのが好ましいのか、というような観点。

名前的にそのものずばり、という本があったので購入した。日本のウェブを検索してみてもいくらか言及があるので価値はありそうだという判断で、大人なので円安でも強行する。

自分は PDF で読みたかったので Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F# by Scott Wlaschin で買った。Kindle 版とどっちが安いかはタイミングによりそう。

この本ではドメイン駆動設計(DDD)を F# で実践するとどうなるのか、ということを語っている。F# は読み書きしたことがないけど、文法は見覚えのある感じだし、特別な言語機能は出てこないので、F# で詰まることはなかった。本の構成が「ドメインの分析」→「モデリング」→「実装」となっていて、F# を使った詳細に入っていくのがだいぶ後半だけになっているのもあるかも。

ドメインの分析はイベントストーミングで行うべし、というところから始まっていて、DDD の実践書としてコンパクトながらよくまとまっている。DDD の入門という観点でもよさそう。

結局この本で言っている Functional とは

  • 静的な代数的データ型を駆使してモデリングすることと、
  • 不変なデータと副作用のない関数に基づいてワークフローを実装すること

ということのようである。

代数的データ型によるモデリング

分析を経て得られたドメインモデルを、型を通じてモデリングしていく。ここではとくに直和型が重要な働きを持っていて、これを使うことで違うものを違うものとして表現できる。「ありえない状態はそもそも表現できないようにする(Making Illigal States Unrepresentable In Our Domain)」というのが大方針だ。

Capturing Business Rules in the Type System: 守るべきビジネスルールがあるなら、実行時のチェックではなく型で表現する。たとえば

type CustomerEmail = {
  EmailAddress : EmailAddress
  IsVerified : bool
}

という表現では EmailAddress が変更されたときに IsVerified がどう変化すべきかが(ロジックを読まないと)分からないし、将来の変更でバグを生む余地があるが、

type CustomerEmail =
| Unverified of EmailAddress
| Verified of VerifiedEmailAddress

として、かつ VerifiedEmailAddress の型をもつ値が自由に生成できなくなっていれば(たとえば検証ロジックを通過しないと生成できないなど)よりうまくドメインの事情を反映して、間違いにくくなる。

つまるところ、性質を名前に寄せていくことだと思う。そうすればコードによるチェックから型によるチェックへと場を移すことができる。

本では、いきなり F# を書くのではなくてドメインにおける状態を擬似コード的に表してみるとアラ不思議、F# のコードにそのまま変換できる、というストーリーになっているけれどそこまでうまくいくのかは疑問。まあ、Persistence Ignorance を強調して、終盤になるまで永続化のことを気にせずにモデリングしてるのはいいと思う。完全にデータストレージから切り離された実装をすることは、少なくとも自分が見聞きする規模のアプリケーションでは非現実的だとも思うけど。

ワークフローをパイプラインとして実装する

ワークフローを複数のステップによる値の受け渡しに分解して、それぞれのステップは副作用を持たない状態遷移としてモデリングする。ここでいう状態遷移とは、モデリングを通じて定義したデータ型を入出力として持つような関数だ。状態が変わるというのはデータの持つ値が変わることではなくて、あたらしい型になる、というのを原則として考えておくとよさそう。何らかのビジネスロジックを経て、データは別のものになる。前の例だと、「メールアドレスを検証する」ではなくて「検証されていないメールアドレスを検証されたメールアドレスに変換する」というような形だ。

Pushing Persistence to the Edges: そしてオニオンアーキテクチャ的な流儀で、ワークフロー内のステップ(群)を I/O で挟み込む形に実装する。場合によっては I/O がワークフローの両端だけでなく中に登場することもある。ともあれ純粋なビジネスロジックと I/O が別個の処理として連なっていることが重要。

ドメインロジックを副作用のない関数としてを実装することで、言うまでもなくテストがしやすくなるのはもちろんだけど、ドメインの事情をよりうまく表現できそうにも感じた。ステップの途中で I/O に逃げることができない(= Unit を返せない)ので、モデリングで定義した型のあいだの変換を行わざるを得ない、というイメージ。あとリポジトリは登場しないとか言ってるが I/O 部分がそうでしょと思った(まあいわゆる Java の DDD で言うリポジトリとはちょっと様態が違うんだと思うけど)。

まとめ

ほかにもモナドがどうこうとか Result 型がどうこうみたいな細かい話もあるけど、まあこのへんは近年の静的型をもつ言語に触れたことがあれば飛ばして読めそう。あと DI 周辺は言語によって扱いが違いそうなので真面目に読んでない(そもそもここを深堀りしないようであった)。

読んだらわりと普通だね、となったけど、いかに純粋な世界を関数型・静的型の世界で実現するか、ということに気を払っていないと容易に境界が崩れてしまうことも想像できる。一種の理想的な例として読んでおくと、現実の問題に取り組むときの足がかりにできそうである。

あと TypeScript でこれを直接実践できるかは分かっていない。とはいえ、この原則はどの言語でも役に立ちそうだった。

はてなで一緒に働きませんか?