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