こちらは「Applibot Advent Calendar 2022」9日目の記事になります。
Domain Modeling Made Functional から学ぶドメイン駆動開発
個人的に関数型言語を使って開発している者です。
Domain Modeling Made Functional を読んでドメイン駆動開発(以下、DDD)に対する考え方が少し変わりました。本書はF#を使って関数型言語でDDDを実現するための手法を説いた本ですが、それだけでなくDDDに対する一般的な考え方についても解説しており、関数型言語向けの内容の中にもオブジェクト指向で参考になるものがありました。
この記事では、本書中からオブジェクト指向でも通用するような内容をいくつか紹介します。
問題を理解することは、安易に解決策を作ることでは無い
上流工程から"要求"が下りてきた時、エンジニアとしてまず何を作りますか?
ER図?それではデータ駆動開発です。ドメイン駆動開発ではありません。
クラス図?それではクラス駆動開発です。ドメイン駆動開発ではありません。
データベース定義やクラスは「実装方法」であり、要求に対する解決策として機能します。
しかし、実装前する前にまずやるべきことは要求を完全に整理することです。初期フェーズで実装方法を考え始めると、データ設計やクラス設計といった技術的な偏見が要求に紛れ込んでしまい、本質的に必要なものを見失ってしまうかもしれません。
本書では、技術的な偏見のない形で要件を整理することから始めています。データベースでもクラスでもなくロジック、つまり「何がしたいか」「何をするべきか」に注目し、要件に対してどのようなロジックが必要か、その入出力は何かを考えます。本書ではその入出力をデータ記述用のミニ言語で記述することから始めています。
システムにおける主役はロジックであり、データ定義はその入出力から自然に求められるはずです。本書は関数型言語による設計を前提にしているためクラス設計指南はありませんが、クラス設計も同じように自然と決められるでしょう。
レイヤードアーキテクチャの問題
多くの開発現場では、下図のように各レイヤーが内側のレイヤーに依存する「レイヤードアーキテクチャ(左図)」を採用していると思います。本書ではレイヤードアーキテクチャには問題があるとバッサリ切り捨てています。その理由は、一連の処理を修正しようとした時に複数レイヤーのコードを書き換える必要があるからです。これを解消するには、レイヤーではなく処理の流れでコードを分割する(右図)ほうが適切です。
また、このような実装をシーケンス図で表すと、次のようになります。
関数指向プログラミングでは、ロジックから副作用を排除して可能な限り純粋関数にするのが良いコードとされています。この観点で言うと、上記のパイプラインはデータベース処理が一番内側にあるためどのロジックからも副作用を排除することができずよくないコードであると言えます。これを解消するには、次の図のようにパイプラインを書き換えます。
(前提として、API Logic はリクエストのデシリアライズ・レスポンスのシリアライズだけをしているものとしています)
- I/Oなどの副作用を伴う処理をパイプラインの端のほうに追いやり、データはドメイン内で取得するのではなくドメインに引数として渡し、戻り値として返されるようにします。
- 真に依存している部分を除いて、ロジックが別のロジックを呼び出すのをやめ、可能な限り引数や戻り値でやりとりします。
- (ここで想定している設計では)サービスはドメインに真に依存するため、サービス内でドメインを呼び出します。
- サービスとAPI・データベースは相互に依存しません。サービスに必要なのはデシリアライズされたリクエストや取得したデータだけなので、これらは引数として渡します。結果保存やレスポンスについてはこの逆で、サービスから戻り値として返します。
このパイプラインでは、ドメインやサービスを純粋関数にすることができます。
突然現れたグレーの層が気になるかもしれません。この層がやっていることは前の関数の戻り値を次の関数の引数に渡しているだけです。これは関数型言語ならほとんど「関数の合成」で実現できるため、本質的には存在しません。オブジェクト指向においても順番に関数を実行するだけの関数として実装できます(Mediatorパターンと似たようなものだと考えてください)。
このような構成は関数型言語にフィットするものですが、オブジェクト指向でも導入するモチベーションがあります。それはテストが簡単になることです。
レイヤードアーキテクチャにおける各レイヤーは内側のレイヤーに常に依存します。依存関係があるとテストの際にモック化が必要になりますが、モック化は面倒な作業であり、テストを書くモチベーションの低下やコストの増加につながります。
これに対して純粋関数はその入出力にのみ依存するため、テストは単純に引数を変えながら実行して戻り値をチェックするだけで済みます。モック化は一切必要ありません。
まとめ
DDDを関数型言語で実装するという発想はあまりないものですが、本書を読むと決して実現しにくいものではなく、むしろDDDは関数型のほうが実現しやすいのではないかとさえ思えるようになります。実際の現場で関数型言語を導入するのは敷居が高いかもしれませんが、ここで紹介したような関数型の考え方を導入して管理しやすいコードを目指していきましょう。
純粋関数はいいぞ。
以上、「Applibot Advent Calendar 2022」 9日目の記事でした。