DDDの戦術的なデザインパターンのほとんどはオブジェクト指向のパラダイムに属し、貧弱なモデルは、すべてのビジネスロジックがオブジェクトではなくサービスに置かれ、一種のDTOになっている状況を表します。言い換えると、貧血モデルは手続き型の同義語であり、複雑なモデルにはお勧めできません。
純粋な関数型プログラミングの経験はあまりありませんが、DDDがFPパラダイムにどのように適合し、その場合に「貧血モデル」という用語がまだ存在するかどうかを知りたいです。
DDDの戦術的なデザインパターンのほとんどはオブジェクト指向のパラダイムに属し、貧弱なモデルは、すべてのビジネスロジックがオブジェクトではなくサービスに置かれ、一種のDTOになっている状況を表します。言い換えると、貧血モデルは手続き型の同義語であり、複雑なモデルにはお勧めできません。
純粋な関数型プログラミングの経験はあまりありませんが、DDDがFPパラダイムにどのように適合し、その場合に「貧血モデル」という用語がまだ存在するかどうかを知りたいです。
回答:
「貧血モデル」問題の記述方法は、そのままではFPにうまく変換されません。まず、適切に一般化する必要があります。本質的に、貧血モデルとは、モデル自体によってカプセル化されていない、適切な使用方法に関する知識を含むモデルです。代わりに、その知識は関連するサービスの山に広がっています。これらのサービスはモデルのクライアントのみである必要がありますが、貧血のために彼らはそれに対して責任を負います。たとえば、Account
クラスを介して処理されない限り、アカウントのアクティブ化または非アクティブ化、またはアカウントに関する情報の検索にも使用できないクラスを考えますAccountManager
。アカウントは、外部マネージャークラスではなく、その基本操作を担当する必要があります。
関数型プログラミングでは、データ型がモデル化すべきものを正確に表していない場合に同様の問題が存在します。ユーザーIDを表す型を定義する必要があるとします。「貧弱な」定義は、ユーザーIDが文字列であることを示します。技術的には実現可能ですが、ユーザーID は任意の文字列のようには使用されないため、大きな問題に直面します。それらを連結したり、それらの部分文字列を切り取ったりすることは意味がありません。Unicodeは実際には重要ではなく、文字や形式の制限が厳しいURLやその他のコンテキストに簡単に埋め込むことができます。
この問題の解決は通常、数段階で行われます。簡単な最初のカットは、「まあ、a UserID
は文字列と同等に表されますが、それらは異なるタイプであり、一方を期待する場所で使用することはできません」と言うことです。Haskell(およびその他の型付き関数型言語)は、この機能をnewtype
次の方法で提供します。
newtype UserID = UserID String
これは、型システムによってaのように扱われるが、まだ実行時のままである値を作成するUserID
関数を定義します。これで、関数は文字列の代わりにが必要であることを宣言できます。以前に文字列を使用していた場所でs を使用すると、2つのsを連結するコードから保護されます。型システムは、テストが必要ないことを保証します。String
UserID
String
UserID
UserID
UserID
ここでの弱点は、コードはまだ任意のを取ることができるということですString
ように"hello"
して構築しUserID
、それから。さらなるステップには、「スマートコンストラクター」関数の作成が含まれます。この関数は、文字列が与えられると、いくつかの不変式をチェックしUserID
、それらが満たされた場合のみaを返します。そして、「ダム」UserID
クライアントが望む場合は、コンストラクタは、プライベートそうに作られてUserID
、彼らがしなければならない、それによって存在に入ってくるから不正なユーザIDを防ぐ、スマートコンストラクタを使用します。
さらに別の手順では、単に定義によって、不正な形式または「不適切な」形式を作成できUserID
ないようにデータ型を定義します。例えば、a を数字のリストとして定義する:UserID
data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine
data UserID = UserID [Digit]
UserID
数字のリストを作成するには、提供する必要があります。この定義を考えるとUserID
、URLで表現できないものが存在することは不可能であることを示すのは簡単です。Haskellでこのようなデータモデルを定義することが多いような高度な型システムの機能によって支援されたデータの種類と一般化代数データ型(GADTs)型システムは、コードの詳細不変条件を定義し、証明することができ、。データが動作から切り離されている場合、動作を強制する必要がある唯一の手段はデータ定義です。
大部分は、不変性により、OOPの支持者として関数をデータと密接に結合する必要がなくなります。元のデータ構造が予期せずに変更されることを恐れずに、元のコードとはかけ離れたコードで、好きなだけコピーを作成し、派生データ構造を作成することもできます。
ただし、この比較を行うためのより良い方法は、おそらくモデル層とサービス層に割り当てている関数を調べることです。OOPの場合と同じようには見えませんが、複数レベルの抽象化を1つの関数に詰め込もうとすることは、FPでよくある間違いです。
私の知る限り、それを貧血モデルと呼ぶ人はいません。これはOOPの用語ですが、効果は同じです。必要に応じて汎用関数を再利用できますが、より複雑またはアプリケーション固有の操作の場合は、モデルを操作するためだけに豊富な関数セットも提供する必要があります。適切な抽象化レイヤーを作成することは、どのようなパラダイムでも優れた設計です。
OOPでDDDを使用する場合、ドメインオブジェクト自体にビジネスロジックを配置する主な理由の1つは、通常、オブジェクトの状態を変更することによってビジネスロジックが適用されることです。これはカプセル化に関連しています。Employee.RaiseSalary
おそらくsalary
、Employee
インスタンスのフィールドを変更します。
FPでは、突然変異は回避されるためRaiseSalary
、既存のEmployee
インスタンスを取得し、新しい給与で新しい Employee
インスタンスを返す関数を作成することにより、この動作を実装します。したがって、突然変異は含まれません。元のオブジェクトから読み取り、新しいオブジェクトを作成するだけです。このため、このようなRaiseSalary
関数はEmployee
クラスのメソッドとして定義する必要はありませんが、どこにでも存在できます。
この場合、データを振る舞いから分離するのが自然になります。1つの構造はEmployee
asデータ(完全に貧弱)を表し、1つ(または複数)のモジュールにはそのデータを操作する関数が含まれます(不変性を保持します)。
DDDのようにデータと動作を組み合わせると、一般に単一責任原則(SRP)に違反することに注意してEmployee
ください。給与の規則が変更された場合、変更する必要があります。ただし、EOYボーナスの計算ルールが変更された場合は、変更が必要になる場合があります。分離アプローチでは、これは当てはまりません。複数のモジュールがあり、それぞれに1つの責任があるためです。
そのため、通常のFPアプローチは、より優れたモジュール性/構成性を提供します。
問題の本質は、モデル上で動作するサービス内のすべてのドメインロジックを含む貧血モデルが基本的に手続き型プログラミングであると思うことです。しかし、データと最も密接に結びついているロジックもあります。
また、関数型プログラミングにも同じコントラストがあります。「実際の」FPは、関数をファーストクラスのエンティティとして使用することを意味し、パラメーターとして渡され、その場で構築され、戻り値として返されます。しかし、そのすべてのパワーを使用できず、それらの間でやり取りされるデータ構造を操作する関数しか持っていない場合、同じ場所にいることになります。つまり、基本的に手続き型プログラミングを行っています。
DDDがFPパラダイムにどのように適合するかを知りたい
私はそれを行うと思いますが、主に不変の値オブジェクト間を移行するための戦術的なアプローチとして、またはエンティティでメソッドをトリガーする方法としてです。(ロジックのほとんどがまだエンティティに存在する場合。)
その場合、「貧血モデル」という用語がまだ存在するかどうか。
「従来のOOPに似た方法で」という意味であれば、通常の実装の詳細を無視して基本に戻ると役立ちます。ドメイン専門家はどの言語を使用していますか?ユーザーからどのような意図でキャプチャしますか?
彼らがプロセスと機能を一緒に連鎖することについて話しているとすると、機能(または少なくとも「do-er」オブジェクト)は基本的にあなたのドメインオブジェクトのようです!
したがって、そのシナリオでは、「関数」が実際に実行可能ではなく、代わりに実際の作業を行うサービスによって解釈されるメタデータの単なる星座であるときに、「貧弱なモデル」がおそらく発生します。