「関数型ドメインモデリング」を読んだ

業務で複雑なドメインを持つソフトウェアをScalaで書いているので以前からとても読みたかったのだが、英語とは遠い昔に仲違いしてしまい読むことができなかった。 この度和訳されたことに感謝したい。

関数型ドメインモデリング - アスキードワンゴ

全体を通した感想

正直言って、この本に書いてある内容を全て実践するのは難しいと感じた。 まず、最初に出てくるドメインエキスパートを交えたイベントストーミングの実施が一番の壁というチームも多いのではないだろうか。 しかし、それでもなおこの本により新しく知った考え方は多く、読む価値があったと思う。

ScalaHaskellなど静的型付けの関数型言語をかじってみたがこれをどう実際のビジネスに関わるアプリケーションの開発に適用していくのかイメージが掴めないという人には特に勧められる。 F#でサンプルコードが書かれているが、静的型付けの関数型言語に触れたことがあれば読むのはそう難しくない。 少し難点があるとすればADTに関する説明は経験者からすると少し冗長に感じたが、それでもADTでドメインモデリングを行う際の手順や考えるべきことが書かれているので無駄ではなかった。 また、関数型プログラミングの作法について随所で説明されているのもよかった。

それらの経験が無い場合、ドメインモデリングの核を担う代数的データ型(Algebraic Data Type)についてまず理解する必要はあるものの、ADTの説明はかなり丁寧に行われているので十分読めるだろう。 モナドなどの概念はたしかに出てくることは出てくるのだが、この本では「実は今まで使っていたものの背景にモナドがある。アプリカティブというものもありこちらも便利なことができる。」というようなことが1ページちょっと解説されるに過ぎない。 あっさりしすぎに思えるかもしれないが、その後モナドという言葉はまたぱったりと出てこなくなり、モナドが何なのかろくすっぽ理解できなかったとしても1全く問題なく読み勧められるようになっており、むしろモナドにより理解が妨げられないよう配慮されていると感じた。 モナドやアプリカティブのことが気になるのであれば、その後CatsやHaskellにでも入門してみればいい。

1部の感想

関数型言語がどうとかではなく、DDDの戦略的設計を解説する和書として貴重ではないかと感じた。 まだDDDの入門書自体そこまで見ない印象だが2、私が今まで読んだものは最初にDDDはドメインエキスパートと話すことから始まるといったことを少し説明し、その後は大部分をエンティティや集約やリポジトリなどの戦術的設計の解説に費やすものが多かった。

この本の1部はドメインエキスパートとのイベントストーミングによりビジネス上で発生するイベントやワークフローを理解しドメインエキスパートが理解できる形で文書化しようという話から始まる。 エンティティや集約といった話は全くといっていいほど出てこない。 それどころかコードすらほとんど出てこず、イベントストーミングによりわかったビジネスルールをイベントとワークフローを中心に捉え、ドメインエキスパートが読める形で文書化する流れの例に大部分を割いている。 戦術的設計に関して今までイメージが湧かなかったのだが、このパートを読んだことで少しだけ理解が進んだ気がする。

ただ1つ気になった点として、DBのテーブルやクラスに落とし込む前提でドメインを捉えようとしてはいけないということをこの本は説くのだが、イベントストーミングによってできあがっていく文書はScalaHaskellをかじった者が見ればADTをほぼそのまま文書化したようなものだとわかる。 それこそ2部でドメインモデリングに入る際に、ここはこうするだろうなとある程度わかってしまうくらいまんまADTなのだ。 テーブルやクラスに落とし込むことを前提としてはいけないのであれば、ADTとほぼ全く変わらない文書に落とし込むのはどうして問題が無いのか、説明が欲しかった。 別にまんまADTであることが悪いと言いたいわけではない。 徒にプログラミングの概念から離れれば離れるだけよいというものではないし、むしろ離れすぎるとプログラムに落とし込むのが大変だと思う。 ただそれでも「ADTはテーブル定義やクラスよりもパワフルでドメインを表現する力が十分ある」とか「ADTはドメインエキスパートが理解できるくらいシンプルだ」とか、テーブルやクラスがダメでADTがいい理由を主張してほしかった。

一応2部で以下のような記述があるので、「ADTはドメインエキスパートが理解できるくらいシンプルだ」と筆者は考えているのかもしれない。

あなたが開発者ではない人だと想像してみてください。このコードをドキュメントとして理解するためには、何を学ばなければならないでしょうか。単純型(単一ケースの共用体)、AND型(中かっこつきのレコード)、OR型(縦棒つきの選択肢)、「プロセス」(入力、出力、矢印)の構文を理解する必要がありますが、それ以上のことはありません。しかも、C#Javaのような従来のプログラミング言語よりは間違いなく読みやすくなります。

『関数型ドメインモデリング ドメイン駆動設計とF#でソフトウェアの複雑さに立ち向かおう』, Scott Wlaschin, 株式会社ドワンゴ, 2024-06-28, 猪股健太郎 訳, p.97

少し気になる点はあったものの

  • データではなくイベントやワークフローに着目する
  • イベントから必要なデータをコマンドとして取り出してワークフローに渡し、ワークフローはまた新たなイベントを返す
  • イベントは境界付けられたコンテキストを出る時にDTOに変換された後にシリアライズされ、別の境界付けられたコンテキストはDTOを通してデシリアライズを行ってイベントを取り出すことで通信が行われる

といった戦略的設計の基礎がわかった。

2部の感想

この部では1部で作った文書からF#のADTにドメインモデルをおこしていくという流れが説明される。 ADTに関する解説は読み飛ばし気味だったが、それでも今まで意識していなかったことに気付かされた。

個人的に特に重要だと思ったのが、プリミティブ型をラップする単純型のパワフルさだ。 Scalaならopaque types、Haskellならnewtypeで定義するものだが、当初は「互換性の無い値を取り違えずにすむ」くらいの認識だった。 しかし、単純型を定義しそれをパーツとしてモデリングを行っていくことで、防御的プログラミングを行う機会がぐっと減ることが単純型の真価だということに気付かされた。

たとえば、以下のように商品の注文を表す型が定義されているとする。 DBやHTTPリクエストのパラメータなど外部からやってきた価格を表す値は必ずこのコンストラクタを経由することでバリデーションが行われる。 説明の簡略化のため雑に例外を投げてしまっているが、実際はEitherなりを返すコンストラクタが定義されるものと考えてほしい。

case class Order(name: String, price: Int) {
  require(price > 0)
}

通常、商品の注文は在庫の確認などのドメインのルールに則したバリデーションが必要であり、問題ないと判定された注文の型も欲しいので以下のように定義する。

case class ValidatedOrder(name: String, price: Int) {
  require(price > 0)
}

OrderValidatedOrderに変換することを考えると、既にOrderpriceが0より大きいことは検証されているので、バリデーションを両方に書くのは冗長ではないだろうか? ここに単純型を導入すると以下のようになる。

opaque type ProductName = String
// ProductNameのコンストラクタは説明の本筋でないため省略

opaque type Price = Int

object Price {
  def apply(i: Int): Either[String, Price] =
    Either.cond(i > 0, i, "Price must be positive.")
}

case class Order(name: ProductName, price: Price)
case class ValidatedOrder(name: ProductName, price: Price)

この場合、コンテキストの外からやってきた値はまず一度Priceのコンストラクタを通してしまえば、その後はコンテキストを出るまではこの値を有効な値として扱うことができる。 OrderからValidatedOrderに詰め替える時も既に検証が済んでいるPrice値をそのまま渡すので、それぞれの型で独自に価格として有効な値を検証する必要がなく、コードがすっきりする。 プリミティブ型をラップする単純型を用意することで、同じ型の値を取り違えるリスクを排除できるばかりか、このように防御的プログラミングを避けられるメリットまであるのだ。

この他にも、ワークフローが何らかの外部からの追加の入力を必要とするときは、外部からの追加の入力を使った計算を関数としてワークフローに渡すことでワークフローを純粋に保つなど、新しい考えを得られた。

ワークフローを関数型として定義することについては、取り入れてみたいとは思ったものの、Scalaだと関数として定義するのか状態の無いオブジェクトが持つメソッドとして定義すべきなのか少し悩んだ。 特に、DIコンテナを使っているのであれば、メソッドとして定義しておく方がよくあるやり方を外れず無難ではないかという気もする。

3部の感想

2部で型を定義したワークフローに、合成可能な小さな関数を組み立てるという関数型の手法で実装を用意していく部だった。 この部でモナドが登場するのだが、全体を通した感想に書いたようにここでモナドを理解できなくともこの本の残りの部分を読み進める上で全く問題がないので安心だ。 失敗する可能性があることを表すEitherResultと呼ばれる型の値を返す関数をどんどんつなげていく際にbindflatMapと呼ばれる操作が必要になるという具体的な話から始まり、ワークフローを実装するために小さな関数を合成していくための他のいくつかの道具となる高階関数について説明されたあと、実は今まで使ってきたResultのようなデータ構造の内、bindなどの操作が特定の規則を満たして提供されているのがモナドだと明かされ、その後モナドは説明からはぱったりと姿を消す。 これでモナドについていきなり理解できる人は少ないと思うが、かえってこの本を最後まで読むことを妨げず、気になる人は関数型言語に入門するという選択肢を与える良い塩梅ではないかと感じた。

個人的には、これまでモデリングに登場してきた直和型をどうやってシリアライズ/デシリアライズして複数のコンテキスト間でやりとりするのか、永続化する場合どういう選択肢がありどういうメリット/デメリットがあるのかといったことを解説する11章や12章が参考になった。 これまでモノリシックなアプリケーションを作ることが多く、コンテキスト間のやりとりを意識する機会が少なかったが、そのようなアーキテクチャのアプリケーションの開発に携わる際に改めて読み返したい。

余談

関数型言語とDDDによるソフトウェア開発というとFunctional and Reactive Domain Modelingという本もあり、こちらは今回のDomain Modeling Made Functionalよりもより進んだ内容になっているらしい。 こちらもいつか読みたい。


  1. この解説量で理解できたらなかなかセンスがあると思う。
  2. ありがたいことにこの本が発売された時期はDDD関連の本の発売が集中しており、[入門]ドメイン駆動設計ドメイン駆動設計をはじめようが後に続いた。