📓

「関数型ドメインモデリング」から学ぶモデリングの勘所

2024/12/20に公開

はじめに

関数をベースにドメインモデリングをすることのメリットを、自分なりにまとめています。関数型プログラミングにはあまり馴染みがなく、今までのドメインのモデリングもオブジェクトをベースとしたモデリングを実施していました。「関数型ドメインモデリング」の書籍を読んで「なるほどな!」と感じた学びがあったので言語化してまとめています。元々は関数型プログラミングは低レイヤの処理には向くが、ドメインが複雑な領域においては転用が難しそうだな…という印象を持っていましたが、考え方を改めるきっかけにとなったため、その理由などをまとめています。

記事を読むメリット

  • 関数をベースにしたモデリングの一例をざっくり知ることができる。
  • 「モデリングって難しそうだな…」という印象から、これならできるかも!…と感じられる!(かも)。
  • 今までなんとなく行なっていた「モデリング」に対して、アプローチの仕方を整理することができる。

結論

  • 関数をベースにしたモデリングを活用することで、「モデリング」の「むずかしさ」を解消できる可能性がある。
  • 特にモデリングの習熟度が高くないチームにとっては、関数をベースにしたモデリングの知見を積極的に取り入れていくのも良さそう。

経緯

私自身、開発時にはイベントストーミングをして、ドメインを整理してから開発をするのですが、イベントストーミングでの整理をどう実装に表現すれば良いかわからない…という声をよく聞きます。(少なくとも私のチームではよく発生しています。)特に集約をどの単位にするか、どのような概念を割り当ててあげるか…というのが特に難しいように感じます。

またモデリングをやろうとすると、考慮すべき観点が多く、試行錯誤が必要で実装し始めるまで時間がかかってしまう…という問題も発生しやすいように感じます。

https://zenn.dev/bitkey_dev/articles/0220513ab0569d

記事の構成

「関数をベースにしたモデリング」の「良さ」を3つまとめています。

それぞれお題をもとに「オブジェクトをベースにしたモデリング」/「関数をベースにしたモデリング」を比較する形式でまとめています。「オブジェクトをベースにしたモデリング」は私自身の今までのモデリングの仕方をもとに記載しています。「関数をベースにしたモデリング」は書籍「関数型ドメインモデリング」を読んで私なりにモデリングの考え方を解釈して記載しています。

1. 観測しやすい「結果」から始めることができる

関数をベースにしたモデリングでは「抽象的な概念」からではなく「具体的に観測しやすい結果」から考えることができます。これにより「モデリング」が直感的になり特に初学者にとって理解しやすくなります。

お題(その1)

「車が止まっている状態から走る」を実現するためのモデリングを考えてみます。前提としてシンプルにするため必要最低限の部分の内容のみモデリングするとします。

オブジェクトをベースにしたモデリング

まずはオブジェクトをベースにしてモデリングをする際のプロセスについて考えてみます。

  1. まず着目するオブジェクトを決めます。
    実際にモデリングする際のもっとも難しい課題の一つかなと思いますが、まず着目するオブジェクトを決めます。実際にはこの着目するオブジェクトを決められるようにするためにイベントストーミングなどを用いてドメインの整理をすると思いますが、ここでは「車」というオブジェクトに着目するとします。

  2. ゴール(車が止まっている状態から走る)を実現するために必要なオブジェクトの操作を洗い出します。
    今回は「車」が「走る」ために必要な操作(アクション)として、「アクセルを踏む」という操作をピックします。

  3. 操作の結果としてオブジェクトがどのような状態になるかを整理します。
    今回の場合は「アクセルを踏む」という操作の結果として「車」が「速度が10km/h」になるとします。

オブジェクトをベースにモデリングする場合、このような思考プロセスになるかと思います。私自身は今まで概ねこの方法で考えることが多かったです。

  1. 着目するオブジェクトを決める
  2. ゴールを実現するために必要なオブジェクトの操作を洗い出す
  3. 操作の結果としてオブジェクトがどのような状態になるかを整理する

「車が止まっている状態から走る」という実現したい事象に対して、まず「車」という抽象度が高い概念で考える必要があります。抽象度が高いがゆえに難しく慣れ親しんでいない人にとって難しいと感じさせてしまう要因のひとつであると考えます。

関数をベースにしたモデリングの場合

次に関数をベースにしたモデリングを考えます。

特定のデータから特定のデータに変換する処理 (=関数) をベースにしてモデリングを進めます。

  1. 変換前後の状態を定義します。
    今回の場合を開始状態を「止まっている車」とし、終了状態を「速度10km/hで走っている車」とします。

  2. 変換する処理を定義します。
    今回は「アクセルを踏む」という処理が該当します。

関数型をベースにモデリングをする場合、このような思考プロセスになるかと思います。

  1. 変換前後の状態を定義する
  2. 変換する処理を定義する

ここでは単に「車」という抽象度の高い概念ではなく「止まっている車」と「速度10km/hで走っている車」という具体的で、かつ外から観測しやすい状態をもとに整理を進めることができます。

お題(その2)

お題その1の延長で「車が止まっている状態から走る」際に、エンジンをかけて、サイドブレーキを解除してからでないとアクセルを踏めない…という制約も合わせて考えるとします。

オブジェクトをベースにしたモデリング(その2)

  1. まず着目するオブジェクトを決めます。
    お題その1と同等に「車」とします。

  2. ゴールを実現するために必要なオブジェクトの操作を洗い出します。
    「アクセルを踏む」の他に、「エンジンをかける」「サイドブレーキを解除する」を追加します。

  3. 操作の結果としてオブジェクトがどのような状態になるかを整理します。

    • 「エンジンをかける」結果、「エンジンがかかった状態になる」
    • 「サイドブレーキを解除する」結果、「サイドブレーキが解除された状態となる」
      …という状態変化が追加されます。

    ここで着目すべきは「車」という一つのオブジェクトに対して考慮すべき状態が増えていることです。考えるべき状態が増えて混乱を招く要因になりえます。もちろん丁寧に整理すれば問題ないかと思いますが、モデリングに慣れ親しんでいないと複雑化させてしまいやすくなる要因となってしまうことも多いかと思います。

    また…「サイドブレーキがかかっている状態でアクセルを踏むことができない」といった制約が追加で必要となったりします。

    これらはすべて「車」という抽象度の高い概念で物事を捉えていることに起因しており、これが「モデリングは難しい」にいう印象を与えてしまう要因のひとつであると考えています。

関数をベースにしたモデリングの場合 (その2)

  1. 変換前後の状態を定義します。
    今回の場合は以下の4つの状態を定義します

    • エンジンがかかっていない状態
    • エンジンがかかった状態 (サイドブレーキはかかっている)
    • サイドブレーキが解除された状態
    • 10km/hで走っている状態
  2. 変換する処理を定義します。
    それぞれ、以下の処理が該当します。

    • エンジンをかける
    • サイドブレーキを解除する
    • アクセルを踏む

関数型をベースにモデリングをする場合、具体的に状態を決めた上で、具体的な状態をベースにモデリングを進めることができるため「モデリング」の「むずかしさ」を低減できると考えます。

2. 具体をもとにするから「分解」がしやすい。

関数をベースにしたモデリングでは、抽象的な概念からではなく、具体的な状態をベースにモデリングを進めることができます。そのため具体をベースになるため、イメージしやすく分解もしやすく、詳細なモデリングの難易度を低減することができます。

お題(その3)

「自宅から車を使って近くのスーパーまで行く」
なお道順は決まっており、信号や他車両は考えないものとします。

オブジェクトをベースにしたモデリング

  1. ゴールを実現するために必要なオブジェクトの操作を洗い出します。
    今回の場合は以下のような操作が洗い出されるかと思います。

    • エンジンをかける
    • サイドブレーキを解除する
    • アクセルを踏む
    • ハンドルを右に切る
    • ブレーキをかける
    • サイドブレーキをかける
    • エンジンを止める
  2. 操作の結果としてオブジェクトがどのような状態になるかを整理します。

  3. 操作を組み合わせて目的を達成できるか確認する

  4. 不足があれば1〜3を見直して修正する

モデリングの中心は「車」という抽象度の高いオブジェクトに対して、全体を俯瞰しつつ必要と思しき操作を洗い出す必要があります。また操作を当てはめて目的が実現可能か確認しつつ適宜、操作の追加や修正が必要となります。

そのため特に以下観点で難しいと考えます。

  1. 高い抽象度でのモデリングが求められること
  2. 全体を俯瞰しつつモデリングが必要となること
  3. 具体↔︎抽象の行き来が必要となること

関数をベースにしたモデリングの場合

  1. 目的実現までの過程の状態を定義する
    状態1:「自宅の前で止まっている状態」
    状態2:「交差点の前で10km/hで走っている状態」
    状態3:「交差点を曲がって10km/hで走っている状態」
    状態4:「スーパーの前で止まっている状態」

  2. 状態と状態との変換処理を定義します。
    状態1→2:「エンジンかけてサイドブレーキ解除してアクセルを踏む」
    状態2→3:「ハンドルを右にきる」
    状態3→4:「ブレーキかけて止まったらサイドブレーキかけてエンジンをきる」

  3. 処理は適宜分解します。
    例えば状態1→2の処理は以下のように分解することができます。

関数型の場合には「自宅から車を使って近くのスーパーまで行く」という目的を実現するための過程を分解することから始めることができ、

具体的な状態をもとに整理を進めることができます。

最初は荒目に分解をし、必要に応じで詳細に分解する方法が取りやすいという点においてもとっつきやすいと考えます。

3. 分解されているから「沼」にハマりにくい

モデリングにおいてどこまで考えるか…という点で悩んだことがある人も多いのではないかと思います。私自身も風呂敷を広げすぎて直接的に関係ないことに悩んで時間を費やしてしまったことも多々あります。関数をベースにしたモデリングにおいては具体をベースに分解された上で進めることができるので、風呂敷を広げすぎる「沼」にはまりにくく結果として限られた時間内でモデリングを実施し切りやすくできるという観点で、モデリングの「むずかしさ」を低減してくれます。

お題(その4)

先ほどと同じ「自宅から車を使って近くのスーパーまで行く」で考えます。

オブジェクトをベースにしたモデリング

オブジェクトをベースにモデリングする場合「車」という抽象度も範囲も広いオブジェクトが対象となるので、いろいろな観点で考える必要が出てきます。

今回の修正で達成したい目的だけでなく、他の文脈において「車」がどうあるべきか…についても考えたくなります。

実施できる操作もいろいろ考える必要があります。

状態によってはできる操作とできない操作がありえます。

そもそも「車」とは何であって、何でないか…というより風呂敷を広げて整理したくなることもあります。

これらはすべて高い抽象度での整理を実施しようとしていることに起因した難しさであり、モデリングが好きだったり、具体↔︎抽象のいききが得意でない場合にはかなり難易度が高く感じてしまうと思います。また考えるべきことをでかくしがちで本来実現すべきこと以上に風呂敷を広げてしまうことも発生しやすいかと思います。

関数をベースにしたモデリングの場合

関数をベースにモデリングする場合「自宅から車を使って近くのスーパーまで行く」という目的を実現するための過程を容易に分解することができ、分解された具体的な状態ごとに変換するための処理を考えることができます。

例えば「エンジンがかかっていない状態」であれば、「エンジンがかかった状態」に「エンジンをつける」ことで変換することができます。

「エンジンがかかっている状態」であれば、以下のいずれかを実施することができます。

  1. 「エンジンがきれている状態」に「「エンジンをる」ことで変換できる
  2. 「サイドブレーキがかかっていない状態」に「サイドブレーキを解除する」ことで変換できる

関数をベースにしたモデリングは銀の弾丸か?

関数をベースにしたモデリングの良い箇所を記載してきましたが、関数最強….と思っているわけではないのでデメリットについても私なりの所感をまとめます。

汎用性が低い

オブジェクトをベースにする場合と比較して、具体をベースにするため汎用性は低めになります。

例えばお題その4を自宅からコンビニに行く…と変更した場合、オブジェクト指向の場合にはハンドルを切るタイミングなどを変更するだけで目的地に到着させることができます。

関数をベースにする場合には、コンビニの行く時の道順に応じて途中の状態を定義し、それらの変換処理を別途かく必要があるかもしれません。

もちろん関数をベースにする場合でも変換処理を車のドメインに準拠してアクセルを踏む、エンジンをかける..のように分解されていれば高い汎用性の状態を維持することができると思います。ただしすでに記載している通り、関数をベースにする際の利点は「事前に抽象化し過ぎずに前に進めることができる」点だと考えています。そのため「スーパーに行く」という要求だけの場合には無理に汎用化せずに、「コンビニに行く」という別の要求が追加発生した場合に既存の整理見直して汎用的にすべき部分を汎用的にすることができれば良いと考えています。

結果として、機能追加発生時の対応コストはオブジェクト型の方が低く、関数型の方が高くつきやすいといえると考えています。ただしこれは、抽象度と汎用性の高いレベルでの整理をどのタイミングで実施するか…という話で、習熟度が高い場合は別ですがそうでない場合にはスモールスタートできる関数をベースにした方が適しているのではと考えています。

カプセル化しづらい

汎用性と同様に、オブジェクトをベースにする場合では事前に「車」というオブジェクトについて整理し、「車」ができるべきこと/できないことを「車」のオブジェクト(≒クラス)に集約します。そのためカプセル化が実現しやすく、ソースリーディングにおいて影響度も確認しやすく、高凝集/低結合を実現しやすいと思います。

関数をベースにする場合は、初期は特定の具体的なケースにおいて対応するため、要素が抽象度高く整理しきれていないことを許容することになります。そのため適切にカプセル化することが難しいです。複数のケースを対応するにつれ、モデルの理解を深めつつ整理を繰り返す必要があります。

誤った抽象レベルでモデリングをして後から修正(変更)するよりは、具体で実装を進めつつ後から抽象レベルを決めて既存の具体のモデルのファサードとなるように抽象的なモデルを用意する方がコストは低く抑えられると思うので、この観点からは関数をベースにした方が良いと考えますが、最初から高い精度で整理できる場合には初期から高い抽象度で進めた方がコスト対効果は高くなりやすいと考えます。

プログラミング言語と習熟難易度

モデリングの思考プロセスとプログラミングの手法は一致していた方が認知負荷は少ないと思います。

関数型をベースにしたモデリングをするとしても、全てのプログラミング言語が関数型で記述できるわけではありませんし、関数型で記述できるとしても関数の合成などの関数型ならではの処理を記述しようとした時の書きやすさは大きく異なると思います。また開発メンバーが関数型プログラミングに慣れるのもなかなか大変であると思います。

そのため関数型プログラミングを採用していない場合には、モデリングの思考プロセスとプログラミングの手法は一致せずに認知負荷が高まりやすいという課題はあると思います。

しかしながら個人的にはこの課題については優先度は高くなく、モデリングに壁を感じて取り組みにくさを感じることに比べたら無視できる課題であると考えています。

まとめ

関数をベースにしたモデリングは「具体的な状態をベースに進めることができる」という点で優れており、考慮すべき範囲を限定しながら進めることができるので「モデリングってむずかしいな…」と感じる人にとっては有効な方法たりえると考えます。

一方で具体的な事象のみしかフォーカスしていないと、本質的には同じ要素を分けて管理することになったりして高凝集・低結合の原則に反しやすくなるリスクもあると考えます。

モデリングに不安を抱えていないのであれば方法はなんでも良いと思いますが、そうでないならまずは関数型をベースに考えてみて対応を積み重ねる過程で経験を積み、知見を溜め、抽象度を高めつつモデルにフィードバックする…という手順じ実施しても良いのではと考えています。

Bitkey Developers

Discussion