Closed17

関数型ドメインモデリング写経(Haskell)

ぱんだぱんだ

関数型ドメインモデリングが気になりすぎたので読んだところ大変感動しました。読んで終わりにするのはもったいないと思い写経することにしたけど、F#新たに学ぶよりは前からいつか入門しようと思ってたのでHaskellで写経してみることに。以下はその前準備としてのHaskell入門

https://zenn.dev/jy8752/scraps/47a88a2e367f2a

そもそもHaskellで本書のような業務アプリケーション書けるのかわかってないけど。たぶん大丈夫でしょう

ぱんだぱんだ

関数型ドメインモデリングを読んだ人の感想

写経する前にけっこう関数型ドメインモデリング実践してみた系の記事が出てたのが気になったので目を通してみる

https://speakerdeck.com/tomohisa/guan-shu-xing-domeinmoderinguwo-fei-guan-shu-xing-nopuroguraminguyan-yu-de-yatutemita

C#で関数型ドメインモデリングを実践したという内容。関数型ドメインモデリングというよりはC#で関数型プログラミングやってみた系な気がしたけど、思ってたよりだいぶ関数型プログラムしててすごいってなった。

https://qiita.com/mkin/items/94f78ea71064ff178519

これはTSと関数型ドメインモデリングのエッセンスでデータベース駆動な設計をどうにかしようという内容。記事の内容はそんなに多くなく、非常に簡潔でわかりやすく関数型ドメインモデリングの良さと現状よく見るデータベース駆動な設計の良くないところが説明されているいい記事だと思った。あと、これができるのTSだからだよなになって現状非関数型言語で関数型プログラミングやるのTSが一番現実的だなと思った。Goではこうはできない。Rustも知らないからわからないけどやりやすいのかもしれない

https://qiita.com/hanaokatomoki/items/cf09dcfb69b1ae0c6e0a

TSでドメインモデリング。関数型の要素はあまりなく、型でドメインをモデリングするが記事のテーマだと思う。前述した通り現状TSが一番関数型ドメインモデリングを実践するのに適していそうと思っているのでモデリングしたあとのワークフローの実装とかを見てみたいなと思った。あと記事の中で

export type unvalidatedBukken = {
  id: number;
  name: string;  
}

export type validatedBukken = {
  id: number;
  name: string;
}

export type validatedOrderdBukken = {
  id: number;
  name: string;
  orderDate: Date;
}

こういうコード例があったけどTSの場合、unvalidatedBukkenとvalidatedBukkenって同じ構造の型だから関数の引数とかでどっちの型の値も受け付けられちゃうよね?と思った。ここらへんはどうするのがいいのだろうね

https://zenn.dev/coconala/articles/2a885527bf2f32

Rubyで関数型ドメインモデリング。Result方を使って副作用のない処理フローの実現を目指した記事。ちょっとさすがに毛色がだいぶ違う気配を感じたのでこれをリアルにやろうとするとウってなる気もしなくもない。そもそも、関数型ドメインモデリングでは型によってドメインをモデリングすることでその型と関数型プログラミングの性質による流れるようなパイプライン処理でエレガントにワークフローの実装ができるよという内容の話だと思うので、この記事の内容とはまた話が変わってくる。

以下、ざっと読んでの感想。

多少やったことあるので思うのだけどGoやKotlin、Rubyみたいな言語でも関数型プログラミングをするためのライブラリがだいたいOSSで作られていてやろうと思えばできるのだけど、Haskellみたいな純粋関数型言語がやっていることを真似してやろうとしてもなんかそれっぽくはなるけど「これは本当に関数型プログラミングできているのか?」みたいな感じになると思っている。

関数型プログラミングを採用してやりたいことは関数をカリー化することでもないし、関数を合成可能にして合成することでもないというかDDDやクリーンアーキテクチャでよく言われるような形をそれっぽく真似ることが目的化してしまいがちというやつ。

もちろん、関数の合成を意識することで自然とカリー化や純粋関数みたいなことは意識することになると思うし、その先にモナドみたいな概念も出てくるし、それは関数型プログラミングの旨味につながるのだろうけど関数型プログラミングの理解が浅いうちはそこの理解までたどりづらいというかそもそも手続き型の言語で関数型プログラミングをするハードルが高いという話もでてくる。

関数型ドメインモデリングの理解のためにやるのはとても良いことで素晴らしいことだとは思うが実践で使えるかというと難しいよなという感想を持った。

実践で関数型ドメインモデリングができるのかという話をもう少しするとまずRubyやPHPのような動的型付け言語で関数型プログラミングというか型を使ったドメインモデリングは厳しい気がしてる。

Goは確か関数型プログラミングをやるためのOSSがあったと思うが、これもけっこう厳しい気もしている。Goの型表現そこまで表現豊かにできない気がしててGoの型でドメインをモデリングするイメージが沸かない。

Javaはrecordでドメインをモデリングすればいい感じにできそうだけど最終的にclassベースの実装に引っ張られると思うのとクリーンアーキテクチャがチラつくことになりそう。Kotlinならまだましかもしれない

となるとTSが現状一番可能性あると思っててTSの型の柔軟性はドメインをモデリングするにはもってこいだし、もともとclassの使用があんまり推奨されてないような言語なので関数主軸の開発も受け入れられそうだし。あとはTSの型は構造的部分型なのでその点が型によるドメインモデリングに影響しないのかなという疑問があるがそれはまた機会があったら考えたい

Rustは知らない

ぱんだぱんだ

ゴールについて

写経する目的

  • 関数型ドメインモデリングの内容をちゃんと理解したい
  • 型でドメインをモデリングする方法を実践的に学びたい
  • モデリングしたドメインの型を使ってどうやってワークフロー組み立てるのかを手を動かして学びたい
  • 最終的に副作用をIOモナドに持ち上げてmain関数付近で処理をすると思ってるのだけどそこら辺をもう少し深く学びたい
  • 純粋関数型言語で業務をする未来はなかなか想像できないし、手続き型言語に関数型ライブラリ入れて業務する未来も想像できないが、 TSならけっこう関数型プログラミングに近いことができる気がしているのでそのイメージを写経を通して持ちたい

こんな感じだろうか

ぱんだぱんだ

1. ドメイン駆動設計の紹介

この章ではドメイン駆動設計について具体的な組織、業務プロセスを例に紹介している。DDDにおけるコード以外の部分の話でユビキタス言語とかイベントストーミングとか境界づけられたコンテキストとかワークフローみたいな用語が登場するところ。

ここらへんの話はDDDの入門書などで繰り返し説明されてきたところではあるが、本書はなるべくわかりやすいように噛み砕いて解説してくれている感じがしたので気づきになったところは書いておく。

ワークショップやイベントストーミングの話はドメインエキスパートの人たちといっしょに業務のワークフローやユビキタス言語を洗い出すことだと理解しているが本書のワークフローの説明がわかりやすかった。

ワークフローについて

  • コマンドドメインイベントをトリガーする
  • ドメインイベントはワークフローを開始する
  • ワークフローはドメインイベントを発行する
  • ドメインイベントがコマンドをトリガーすることもある

コマンドは現在形で表現する。ドメインイベントは過去形で表現する。「請求書を発行する」というコマンドがワークフローをトリガーするとそれに対応するドメインイベントは「請求書が発行された」になる。

このビジネスプロセスを入力と出力を持つパイプラインとして考える方法が、本書のテーマである関数型プログラミングの仕組みとマッチすることになる。

逆にいうとこのビジネスワークフローのパイプラインをプログラミングで表現できる言語でないと関数型ドメインモデリングのエッセンスを完全には取り入れられない。

とはいえ、これは純粋関数型言語でないと難しいのではないかと思ったりする

ドメインとは

ドメインエキスパートが専門にしていこと。それがドメインだそうです。なるほど、わかりやすい。ドメインエキスパートがちょっと知っていること。でも、他のもっと詳しい人に聞いたほうがよいという内容はサブドメインとなる。なるほど

問題空間と解決空間

繰り返し説明されているが現実界のドメインは曖昧で複雑なものであり、この現実界のドメインをすべてプログラムで表現したいわけではない。ドメインの中でも必要なものだけを抽出したものを扱うべきであり、そのためには問題空間としてドメインやサブドメインを定義し、それらを解決空間の中で本当に必要なものだけを抽出して扱うようにするといいそう。

そして、このとき、解決空間に抽出したドメインやサブドメインが境界づけられたコンテキストとなる。

境界づけられたコンテキストについて

境界づけられたコンテキストはなぜコンテキストなのか?
→それぞれのコンテキストは解決手段における専門的な知識を有しているから。コンテキスト内では共通した言語が用いられ、首尾一貫した設計で統一されている。そのため、コンテキストの外にでるとそのコンテキスト内の情報は陳腐化する。

なぜ境界づけられているのか?
→現実世界のドメインは曖昧なものだが、システムにおいてはある程度それぞれのコンテキストは疎結合にしておいたほうが都合が良い。また、1つのドメインが複数の境界づけられたコンテキストに分割されることもある。これは複数の境界づけられたコンテキストを一緒に扱っているようなレガシーシステムを扱うときに起こりがち。

コンテキスト間の関係を図に表したものをDDDの用語でコンテキストマップという。

ぱんだぱんだ

2. ドメインの理解

技術的な実装の先入観を持たないようにする

データベース駆動

ドメインや処理のワークフローがイメージできてくるとすぐにDBスキーマの設計をしたくなるのをこらえましょう。データベース設計を優先して設計しているとデータベースに合わせて設計を歪ませてしまうことが多いからです。

クラス駆動設計

Javaみたいなクラス指向の言語でOrderBaseみたいな基底クラスを作りたくなるかもしれない。でも、そのような基底クラスはドメインエキスパートには理解できない、ドメインの歪曲です。

  • DDDにおいて設計中は実装の詳細に入りこまないこと
ぱんだぱんだ

3. 関数型アーキテクチャ

  • 関数型アーキテクチャに限らず優れたアーキテクチャとはコンテナ、コンポーネント、モジュールといったもので境界が定義されており、新しい要件が発生したときの変更コストを最小限に抑えたものである
  • 変更コストが最小限とはつまり変更容易性のことであり、システムの変更が容易であるとリファクタリングができ、常に成長するシステムになる。リファクタリングができるようにするにはテストがちゃんと存在する必要があり、そもそもテストが書けるようにシステムが設計されている必要がある。
  • また、境界づけられたコンテキストのように自律的明確に定義された境界を持つシステムも変更容易性の高いシステムとなる。依存しているモジュールやパッケージが少なければ変更は容易だし、逆に依存関係が多くモジュール同士が密だと変更はしにくくなる
  • また、境界づけられたコンテキストは独立していることが重要だがそれ自体がデプロイ単位である必要はない。コンテキスト単位でデプロイ可能となるとそれはマイクロサービスになるが、モジュラーモノリスのようなアーキテクチャでモジュールとして境界づけられたコンテキストを定義することも可能だ。マイクロサービスを本当に構築するのは大変。マイクロサービスの1つが停止したら他も動かなくなるならそれはマイクロサービスではなく単なる分散モノリス。

ここまでの話を踏まえて

  • とにかく良いアーキテクチャとは自律的で疎結合なモジュールが定義されていること
  • DDDにおいて境界づけられたコンテキストをソフトウェアで表現できた場合、それがまさに自律的で疎結合なモジュールとなる
  • だから、境界づけられたコンテキストはコンテキスト内で閉じているべきで他のコンテキストとは疎であるべきでその境界は明確に定義されるべき。

では境界づけられたコンテキスト間のやり取りはどうなるかというとそれはイベントを使うことになる。イベントを使うことで互いに自律的で疎結合に作られたモジュール間のやり取りを可能とする。

また、境界づけられたコンテキストの境界を保つために外から入ってくるデータ、つまり別のワークフローが発行したイベントをトリガーにワークフローが実行される場合、トリガーとなったイベントにそのドメインオブジェクトと近しいデータがすべて含まれることになるがこれはDTOなどの転送オブジェクトとJSONなどのシリアライズ/デシリアライズを通して入ってくるべきである。

これは境界づけられたコンテキストの境界を保つため、ドメインを外界と切り離しきれいな状態のドメインで保つためであると思う。

具体的に上記のような境界づけられたコンテキスト内のドメインをきれいに保つためにゲート(入力ゲート、出力ゲート)や腐敗防止層といったものを通してデータが出入りするようにしたりする。

オニオンアーキテクチャの例が本書に記載されているが、これは関数型アーキテクチャの目的が予測可能で理解しやすい関数を扱うことで、それにはI/Oのような副作用はできるかぎり外側に追いやるべきで、それはまさにオニオンアーキテクチャの話だからだ。

具体的に関数型プログラミングでこれらのオニオンアーキテクチャや境界づけられたコンテキスト、ドメインオブジェクト、ワークフローを実現するかを次の章から見ていく。

ぱんだぱんだ

ここからコードでドメインをモデリングしていくがまず文書化したドメインやワークフローを確認してみる。

Bounded context: Order-Taking

// ------------------------
// 単純型
// ------------------------

// 製品コード
data ProductCode = WidgetCode OR GizmoCode
data WidgetCode = string starting with "W" then 4 digits
data GizmoCode = string starting with "G" then 3 digits

// 注文数量
data OrderQuantity = UnitQuantity OR KilogramQuantity
data UnitQuantity = integer between 1 and 1000
data KilogramQuantity = decimal between 0.05 and 100.00

// ------------------------
// 注文のライフサイクル
// ------------------------

// 未検証の状態
data UnvalidatedOrder = 
  UnvalidatedCustomerInfo
  AND UnvalidatedShippingAddress
  AND UnvalidatedBillingAddress
  AND list of UnvalidatedOrderLine

data UnvalidatedOrderLine =
  UnvalidatedProductCode
  AND UnvalidatedOrderQuantity

// 検証済みの状態
data ValidatedOrder =
  ValidatedCustomerInfo
  AND ValidatedShippingAddress
  AND ValidatedBillingAddress
  AND list of ValidatedOrderLine

data ValidatedOrderLine =
  ValidatedProductCode
  AND ValidatedOrderQuantity

// 価格計算済みの状態
data PricedOrder =
  ValidatedCustomerInfo
  AND ValidatedShippingAddress
  AND ValidatedBillingAddress
  AND list of PricedOrderLine
  AND AmountToBill

data PricedOrderLine =
  ValidatedOrderLine
  AND LinePrice

// 出力イベント

// 注文確認を送ったイベント
data OrderAcknowledgementSent = ...
// 注文が確定したイベント(発送用)
data OrderPlaced = ...
// 請求可能な注文が確定したイベント(請求用)
data BillableOrderPlaced = OrderId
  AND BillingAddress
  AND AmountToBill

// ------------------------
// ワークフロー
// ------------------------
workflow "Place Order" =
  input: UnvalidatedOrder
  output (on success):
    OrderAcknowledgmentSent
    AND OrderPlaced (to send to shipping)
    AND BillableOrderPlaced (to send to billing)
  output (on error):
    InvalidOrder

詳しいドメインの説明は省くがこの注文に関するコンテキストでは注文というドメインモデルが「未検証」、「検証済み」、「価格計算済み」の3つの状態を持ち遷移していく。状態を持って変化していくのでOrderはDDDにおけるエンティティとなる。

ワークフローを見てみると未検証の注文を入力に最終的に3つのイベントのセットを出力する。

この文書化されたドメインをHaskellの型でモデリングしてみる。

ぱんだぱんだ
gh repo create --public --add-readme functional-domain-modeling-haskell-demo
ghq get functional-domain-modeling-haskell-demo
cd /Users/yamanakajunichi/ghq/github.com/JY8752/functional-domain-modeling-haskell-demo
stack new functional-domain-modeling-demo --bare --force

stack newでプロジェクト作るとghcupを通してインストールしたghcのバージョンではないバージョンをVSCodeのHaskell拡張が参照してエラーが出ることがある。(HLSが最新のghcに対応してないよエラー)

あんまりわかってないがstack.yamlのsnapshotのところにghcのバージョンを直接指定したりするとエラーを回避できる。

stack.yaml
- snapshot:
-  url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/18.yaml
+ snapshot: ghc-9.4.8

hie.yaml(Haskell IDE Eなんたらの略らしい)も必要かも

ちょいちょいコンパイルエラーというか型チェックの警告出て再起動してなおるみたいなこと繰り返してるから体験悪い(ちゃんとセットアップできてない説もある

ぱんだぱんだ

Haskellで値オブジェクトを作る

type CustomerId = Int

という別名が真っ先に思いつくがこれだとCustomerIdはIntのただの別名なので型の強制力があまりない。値オブジェクトとして型を定義するならnewtypeを使ったほうが良いでしょう。

Haskellではレコード構文というのがあって、この書き方を使うと複数のフィールドを持ったデータ構造みたいなのを書くのが楽になるし、getter関数が自動で作られる。

data User = User { name :: String, age :: Int}
newUser :: User
newUser = User{name = "user", age = 20}

みたいに手続き型言語でよくやるような書き方ができる。値オブジェクトに関してはnewtypeで以下のように宣言してあげるだけで良い気がする。

-- カスタマーID
newtype CustomerId = CustomerId Int
  deriving (Show, Eq)
ぱんだぱんだ

Haskellのコンストラクタについて

ちょっと書いててわからなかったのでメモ。
以下、sonnetの回答。

Haskellでは、データ型と関連するコンストラクタの2つの概念があります:
型コンストラクタ(Type Constructor):
型の名前で、例えばCustomerIdやMaybeなど
型レベルで使用される
データコンストラクタ(Data Constructor):
値を作るための関数、例えばCustomerIdやJustなど
値レベルで使用される

例えば

data Bool = True | False

の例で見るとBoolは型コンストラクタでTrueFalseはデータコンストラクタ。
Maybeの例を見るとわかるがデータコンストラクタは引数を取ることができる。

data Maybe a = Nothing | Just a

プログラミングHaskellではデータコンストラクタのことを構成子と記載していた。

Haskellにおける型のエクスポートには以下のようなパターンがある。

-- 型コンストラクタのみエクスポート
module Order (CustomerId) where

-- データコンストラクタもエクスポート
module Order (CustomerId(CustomerId)) where

-- すべてのデータコンストラクタをエクスポート
module Order (CustomerId(..)) where

データコンストラクタをエクスポートせず型コンストラクタのみ公開することで外部のモジュールからそのデータ型のインスタンスを生成することを禁止することができる。ただ、そうするとインスタンスの生成ができないので代わりにファクトリ関数を公開する。

これはそのインスタンスを生成するのにバリデーションのような処理を必ず通したい場合などに有効なパターンでHaskellではスマートコンストラクタパターンと呼んでいるらしい。

このスマートコンストラクタパターンはDDDにおける値オブジェクトやエンティティの作成に役立つと思われる。

しかし、データコンストラクタをエクスポートしないとパターンマッチングにその型を使えないなどの不都合もあるようでそれには以下の記事にあるようなLANGUAGE PatternSynonymsという言語拡張を定義することで外部モジュールにデータコンストラクタを公開せずにパターンマッチングを可能とする方法もあるらしい。

https://logicoffee.hatenablog.com/entry/2018/12/14/212521

ぱんだぱんだ

上記を踏まえて

newtype CustomerId = CustomerId Int deriving (Show, Eq)

-- Either型を使ったカスタマーID作成関数
mkCustomerId :: Int -> Either String CustomerId
mkCustomerId n
  | n > 0 = Right (CustomerId n)
  | otherwise = Left "CustomerId must be greater than 0"

こんな感じで書いてみたがけっこうめんどくさい。CustomerIdのデータコンストラクタをエクスポートしてしまうとファクトリ関数を通らないでCustomerIdの値が作れてしまうのでバリデーションを通らないから、コンストラクタはエクスポートしないようにしないといけないし。

本書でもこのような単純型で作られた値オブジェクトの生成過程でバリデーション処理を挟むようなことはしてなかったので単純に型だけ定義すればいいかなとも思った。ていうかドメインエキスパートが見ても理解できるようなドメインの文書化を目指しているのに読みづらくなる気がするし

ぱんだぱんだ

単純型のモデリング

ということでintやstringといったプリミティブな値のラッパー型をドメインモデルとして定義する場合は以下のようにしてみた。

newtype CustomerId = CustomerId Int deriving (Show, Eq)

単純型をドメインとしてモデリングする利点として本書では以下のように記載されている。

  • ドメインエキスパートが見てもわかりやすい
    • ドメインの文章化
  • 値の比較、関数の引数にわたすときに厳密に区別することができる
-- これはコンパイルエラー
main = do
  let cid = CustomerId 5
  let oid = OrderId 10
  print (cid == oid)

もし、ラップされた値を取り出して使いたい場合は以下のように簡略化したパターンマッチで取り出せそう

main = do
  let cid = CustomerId 5
  let innerValue = n
        where
          (CustomerId n) = cid
  print innerValue
-- > 5

本書では単純型でラップすることによるパファーマンス問題とその回避策が記載されているが今回は省略。雑にいうとパフォーマンスにシビアなシステムやパフォーマンス問題が顕在化したときに初めて考えるでも問題なさそう。

ぱんだぱんだ

複雑なデータのモデリング

本書の例で言うと

data Order =
    CustomerInfo
    AND ShippingAddress
    AND BillingAddress
    AND list of OrderLines
    AND AmountBill

このようなドメインをモデリングする場合。これをHaskellの型で表現するには以下のように新しい型を作ってあげると良い。

data Order = Order CustomerInfo ShippingAddress BillingAddress [OrderLine] AmountToBill

ちなみに、本書ではまだ定義できない型(例えばOrderIdがintなのかstringなのかuuidなのかまだわからないといったとき)Undefinedという型を定義してそれを仮で指定しておけば良いとしている。Haskellで同じようなことをするにはいろいろ手はありそうだったが一番カンタンそうだったので以下のようにVoid型のエイリアスとして定義して使ってみた。

import Data.Void (Void)

type Undefined = Void

type CustomerInfo = Undefined
type ShippingAddress = Undefined
type BillingAddress = Undefined
type OrderLine = Undefined
type AmountToBill = Undefined

また、前述しているがHaskellでdata型を作るとき以下のようなレコード構文を使うことができる。

data Order = Order {
  orderCustomerInfo :: CustomerInfo,
  orderShippingAddress :: ShippingAddress,
  orderBillingAddress :: BillingAddress,
  orderOrderLines :: [OrderLine],
  orderAmountToBill :: AmountToBill
}

選択肢が複数ある型も以下のように定義することができる。

data ProductCode = Widget WidgetCode | Gizmo GizmoCode
  deriving (Show, Eq)
ぱんだぱんだ

ドメインの完全性と整合性

前述したがHaskellにおいてコンストラクタを非公開にしてファクトリ関数を作ってバリデーションを行うスマートコンストラクタパターンがまさに紹介されていたので、やっぱりやるべきみたいです。

ただ、本書ではスマートコンストラクタを使っていい感じに実装する手法が記載されているので真似してみる。

基本は以下

  • コンストラクタを非公開
  • ファクトリ関数でドメインモデルの制約を表現

また、コンストラクタを非公開にしても同じモジュール内ではアクセスできてしまうのでサブモジュールを作成する方法が本書では記載されていたので真似してみる。

src/SimpleType/UnitQuantity.hs
module SimpleType.UnitQuantity
  ( UnitQuantity,
    create,
    value,
  )
where

-- 注文数量(個数)の型
newtype UnitQuantity = UnitQuantity {unUnitQuantity :: Int}
  deriving (Show, Eq)

-- ファクトリ関数
create :: Int -> Either String UnitQuantity
create qty
  | qty < 1 = Left "数量は1以上である必要があります"
  | qty > 1000 = Left "数量は1000以下である必要があります"
  | otherwise = Right $ UnitQuantity qty

-- 値を取得する関数
-- レコード構文でgetter関数が生成されているのでこれはなくても良い
value :: UnitQuantity -> Int
value = unUnitQuantity

良さそう。これをOrderモジュールで再エクスポートして使う。

src/Order.hs
module Order
  ( CustomerId (..),
    ProductCode (..),
    WidgetCode (..),
    GizmoCode (..),
    OrderQuantity (..),
    module SimpleType.UnitQuantity,
    KilogramQuantity (..),
    OrderId (..),
    UnvalidatedOrder (..),
    UnvalidatedOrderLine (..),
    ValidatedOrder (..),
    ValidatedOrderLine (..),
    PricedOrder (..),
    PricedOrderLine (..),
    OrderAcknowledgementSent (..),
    OrderPlaced (..),
    BillableOrderPlaced (..),
  )
where

import Data.Void (Void)
import SimpleType.UnitQuantity

...

以下のようにimportして使える

app/Main.hs
module Main (main) where

import Order
import SimpleType.UnitQuantity as UnitQuantity

main :: IO ()
main = do
  -- UnitQuantity.create関数の使用
  case UnitQuantity.create 5 of
    Right qty -> do
      putStrLn $ "数量: " ++ show qty
      putStrLn $ "値: " ++ show (UnitQuantity.value qty)
      -- putStrLn $ "値: " ++ show (unUnitQuantity qty)
    Left err -> putStrLn $ "エラー: " ++ err

  -- 範囲外の値をテスト
  case UnitQuantity.create 0 of
    Right qty -> putStrLn $ "数量: " ++ show qty
    Left err -> putStrLn $ "エラー: " ++ err

これがHaskellやF#のような関数型言語で値オブジェクトのような単純型を表現するテクニックのようだ。

また、ドメインの不変条件を型システムで強制することもできる。本書では以下のような必ず要素が1つ以上あるリストの型を定義していた。

type NonEmptyList<'a> = { 
  First: 'a
  Rest: 'a list
}

このような型を使うことでドメインの不変条件が型によって文書化され、自動的に適用されるのでユニットテストも書く必要がなくなる。

型でドメインを表現する例でいうと検証済みで有効なメールアドレスと検証前のメールアドレスが存在することを考えてみる。

data CustomerEmail = CustomerEmail {emailAddress :: EmailAddress, isVerified :: Bool}

これでは以下のような課題がある

  • フラグがいつ設定されていつ変更されるかなどが明確ではない
  • フラグの変更忘れが容易に発生する

これはドメイン知識としてはメールアドレスは検証済みアドレス未検証アドレスの2つであるということがわかっているので以下のように表現する。

data CustomerEmail = Unverified EmailAddress | Verified EmailAddress

これでも、検証済みのケースを作成するときに未検証のアドレスを誤って渡してしまうことを防ぐことができない。この問題を解決するには検証済みのアドレスと未検証のアドレスの型を完全に分けることです。

data CustomerEmail = Unverified EmailAddress | Verified VerifiedEmailAddress

そして、ここが重要と本書ではしていたがVerifiedEmailAddressのコンストラクタを公開しないことで検証済みのメールアドレスは検証サービスだけが作れるようにすること。

つまり、検証済みのアドレスを作るには未検証のアドレスを検証サービスを通して作る以外に方法はない。

これは、型システムによって不正な状態を表現できないようにしており、ビジネスルールをプログラムで完全に表現している。

別の例でメールアドレスと郵便番号を持つ連絡先をドメインとしてどう表現するかという例がある。先に結論を言ってしまうと以下のようにすると良い。

data ContactInfo = EmailOnly EmailContactInfo | AddrOnly PostalContactInfo | EmailAndAddr BothContactInfo

これをoptionalなフィールドのレコードで表現しようとするとメールアドレスも郵便番号もない状態が作れてしまう。ありえるのは住所のみと郵便番号のみと住所と郵便番号の3パターンのみなのでその3パターン以外は表現できないように型で強制する。

そうすることで、ドメインの表現だけでなく純粋に不要なユニットテストのテストケースを1つ削れる。

あとは整合性の話もあったけどこれはマイクロサービスの話でサービス間をまたいだ処理の整合性でも同じような話があった気がする。

ぱんだぱんだ

パイプラインによるワークフローのモデリング

以下のような注文確定ワークフローをHaskellで表現していく。

  1. Validation(注文の検証)
  2. PriceOrder(注文の価格計算)
  3. AcknowledgeOrder

これらのステップをサブパイプとしてつなげてパイプラインを構築し、それがワークフローとなる。このワークフローの入力はドメインモデルである必要があり、今回の注文確定ワークフローの入力は未検証の注文である。そして、出力は注文確定などの3つのイベントとなる。

入力がドメインモデルであるということは重要で、ある境界づけられたコンテキストから別のコンテキストのワークフローをトリガーしたい場合にイベントを使うと前述したが、イベントはJSONなどのスキーマからデシリアライズされドメインオブジェクトに変換されたものを入力として使う。

ちょっとめんどくさくなってきたので概要だけまとめる

  • ワークフローを関数型プログラミングで表現すると各サブパイプを関数で定義しそれを合成してワークフローを表現する
  • 関数合成するためには各関数の出力と入力の型が合わないことがしばしばある
  • OptionとかResultとかAsyncとか
  • これに関しては型を合わせる必要があるがたぶんこの工程が型の持ち上げとか言われてるやつだと思う
  • 本書ではモナドとかファンクターとかの難しい部分ではなくカリー化や関数の部分適用、全域関数の説明がわかりやすく記載されており関数型プログラミングの入門書として良書だなと思った
  • 何にせよこのワークフローを関数同士をパイプ演算子などでつなぎ合わせることでわかりやすくエレガントに表現できることが本書の最も言いたいことの1つだと思うが正直これは純粋関数型言語でないと難しい
  • F#やHaskellはデフォルトでカリー化されてて関数合成が容易だから。TSやGoでこんなにエレガントに表現はできない。
  • 関数型ライブラリや自前のカリー化をほどこせばいけるかもしれないがあんまり受け入れられることでもない気がする。少なくとも自分がこれやりましょうと言われたらちょっと落ち着けとなると思う。
  • 本書の第3節からは実装の話になってくるがここはもう関数型言語でどう実装するかの話な気もするがこれから読むのでちょっとわからない
  • 今の段階での感想はTSやGoといった非関数型言語で関数型ドメインモデリングのエッセンスをどうにか活用しようとするのならば第2節までのドメインの文書化であり、そのドメインを型システムにどう落とし込むかといった部分な気がする
  • ワークフローの構築は素直に手続き的に記述すればいんでなかろうか
  • ドメインの文書化、型システムへの落とし込み、すべての状態を型で宣言することによるステートマシンの実装、副作用を端に追いやることによるコアのドメインをクリーンに保つこと、イベントを使ったコンテキストの境界づけなどなどワークフローの構築以外に本書から学ぶことは非常に多くここらへんは非関数型言語でも取り入れることは可能なんじゃなかろうか
  • これは実際にやってみないとまだイメージしきれないのでGoやTSでやってみたいとも思うが余力があれば
  • とりあえず本書に戻り読み進めていく
ぱんだぱんだ

実装のポイント

  • 関数合成するにあたって入力と出力があわないことがしばしばある
  • 依存関係に関しては引数に指定することで関数型プログラミングでは依存性を注入する
  • 依存関係はあらかじめ部分適用して新しい関数を作成しておくことで関数合成しやすくなる
  • 関数型プログラミングではエラーをtry-catchすることなくResultを使う
  • そうすることで処理が煩雑になるのを防ぐことができる
  • このResultやAsyncなどのエフェクトを伴う関数を合成するのにはさすがに関数型のテクニックが必要
  • 具体的にはモナドやアプリカティブなどの用語が登場するがあまり深く考える必要はない
  • 要は関数が合成できるように型を変換するために必要なことをやっているだけ
  • 本書ではスイッチ関数やアダプター関数といった名称で登場する
  • ドメインやワークフローを型システムに落とし込んだあとの実装は関数型プログラミングの話に深く入り込んでいくことになり、重要なことは入力と出力が異なる関数をどう合成していくかという話、だと思う
  • デシリアライズとシリアライズ
  • ワークフローの出力はイベントを伴うことがあり、このイベントがワークフローをトリガーする
  • イベントはJSONやprotobufのようなスキーマに変換されるため再びワークフローに入るにはDTOに変換されドメインオブジェクトに変換される必要がある
  • なぜなら、ドメインオブジェクトは非常に複雑で制約のある構造になることがほとんどなのでデシリアライズ、シリアライズしやすいようにDTOに変換する必要があるから
  • DTOへのシリアライズはエラーの発生はしないが、DTOからドメインオブジェクトへのデシリアライズはドメインエラーが発生する
  • 永続化について
  • 永続化のポイントは
    • 端に追いやること
    • コマンドとクエリを分離すること
    • 境界づけられたコンテキストはそれぞれ独自のデータベースを持つこと
  • 読み取りと書き込みでは必要となる情報が異なることが多い、加えてそれぞれ別の方向に進化していくことが多いためコマンドとクエリは別れていたほうが嬉しい
  • なのでCQRS
  • CQRSをやるとなるとイベントソーシングが出てくる
  • ここでCQRS + ESの概念が必要となるのだろう
ぱんだぱんだ

まとめ

  • ドメイン駆動開発の最も重要なことはドメインの文書化であり、文書化したドメインをコード、関数型ドメインモデリングにおいては型システムに落とし込むこと
  • そのため、ドメインエキスパートがコードを見ても理解できることが望ましい
  • そのためにはドメインを理解することが重要でイベントストーミングやコンテキストマップ、ユビキタス言語などはそのための手法とツール
  • ここをやらずに実装から入ると軽量DDDとか戦術的DDDと呼ばれるものになり、しばしば議論を呼ぶことになる
  • 戦術的DDDがありかなしかは議論になりがちだが個人的にはありよりではある
  • 何がハードルが高いかってドメインエキスパートを巻き込むことにあり、それは組織レベルで人を巻き込む力が求められ、だいぶハードルが上がる
  • が、個人レベルでドメインエキスパートにslackとかでちょこちょこ確認作業をして、ドメインを理解しそれをコードに落とし込むことは容易だし、むしろ全員やらないとだめなことで、それはもうDDDだろうという気持ちで戦術的DDDありよりの意見
  • 型システムでドメインを表現することには選択型、レコード型、Result, Option, Asyncなどの型があると便利
  • ここらへんが言語レベルでない言語だとけっこう悩むことになる
  • TSは型が柔軟なので型を表現する力はかなりあると思ってる
  • なぜ関数型でドメインモデリングなのかというとここらへんの表現力が要因としてあがりそう
  • また、一連のビジネスワークフローをコードに落とし込むにあたって、関数型プログラミングの関数合成も非常に有効なツールとなる
  • 複雑なドメインモデルを型で表現し、それを扱うビジネス処理を小さなパイプ処理として関数で定義できるとそれらをつなぎ合わせるだけでワークフローを表現できるという大変エレガントな状態になる
  • これが、非関数プログラミング言語では難しいポイントだと思っていたが一休さんのTSの例をみると見事にTSでこのワークフローを再現しているようだ、すごい
  • だいぶHaskellやRustのコードを見ているような気分になったが
  • 関数の合成に関しては入力と出力の型が噛み合っている必要があり、ここをうまく噛み合わせるテクニックがいくつも登場する
  • 型の持ち上げとかswitch関数、アダプター関数として本書では登場する
  • ここらへんの実装の話は関数型プログラミングの手法の話にだいぶなるし、F#固有の話もけっこうあった
  • あとは、ドメインとその外の世界をきっちり分けるのでシリアライズやデシリアライズ、DTOの話もけっこうされていた
  • 永続化の話ではリポジトリパターン不要と本書では言っておりこの意見に関して懐疑的な人が多そうな印象を受けている
  • これはIO処理を完全に端に追いやっているのでドメイン部分のテストが容易かつIO処理はテストするほど複雑なケースがそれほどなく、全体の処理をテストすれば十分だよねという意見であり、モックとか必要としてないからリポジトリパターンみたいに永続処理の抽象化は不要だよねという意見だと思ってる
  • 単体テストの考え方でもこれと似た意見でユースケースの結合テストを書けばインフラストラクチャ層の実装テストはいらんよねみたいな
  • あと、ORMを使わないみたいなのも若干関係してそうな気もする
  • そもそもなんでリポジトリパターンがほしいかというとモックに差し替えてテストしやすくしたいという意見とORMとか(生のSQL操作でもいいけど)の処理をどっかに隠蔽しときたいみたいな気持ちがあるからじゃなかろうか
  • 自分はけっこう後者の意見でGoでいうならGORMとかsqlcの処理とか構造体はあんまり別のレイヤーにいてほしくない
  • クリーンアーキテクチャとかでプログラミング書いてるとリポジトリパターンがDB処理を別のレイヤーと切り離すのに簡単だからまあみんなリポジトリパターン必要じゃね?ってなってるのかなと思ってる
  • しかも、関数型のスタイルだと関数の引数に依存性を注入するからモック差し込むのも容易みたいな背景もありそう
  • 個人的には本書を読んで、そのスタイルでやるならたしかにリポジトリパターンは不要そうとも思う。
  • モック差し込まないで全体のワークフローをE2E的にテストできるし、ワークフローの中が完全に副作用のない世界だからテスト容易だし、IO処理が登場するのがmain関数の呼び出し付近のみのはずだから
  • 現実的にはIO処理とドメイン処理がだいたいクリーンアーキテクチャでいうusecase層あたりで混ざってることが多く、usecaseより上位のレイヤーでテストを書きたいならIO処理をモックに差し替えたくなることがたぶん多そうで、その場合リポジトリパターンのように抽象化されていたほうが嬉しいと思う
  • 本書を完全に再現したり、純粋関数型言語で仕事する未来はあんまり現実的ではない
  • ので、TSやGoで本書のエッセンスをいかに活かせるかを考え実行するのがネクストのステップだと思ってる
  • 繰り返しになるがドメインの文書化、型システムへの落とし込み、ドメインと外界を完全に切り離す手法は関数型言語でなくても実現可能で重要なことであり、ここらへんを学べたのは非常に良かった
  • DDDと関数型プログラミングの入門書としてもかなりの良書でした
このスクラップは25日前にクローズされました