リッチvs貧血ドメインモデル[終了]


93

貧血ドメインモデルではなくリッチドメインモデルを使用するかどうかを決定し、その2つの良い例を探しています。

私は、サービス->リポジトリ->ストレージレイヤーシステムに支えられた貧血ドメインモデルを使用してWebアプリケーションを構築し、BL検証にFluentValidationを使用して、すべてのBLをサービスレイヤーに配置しています。

私はエリックエヴァンのDDDの本を読みました、そして彼は(ファウラーと他の人たちと一緒に)貧血ドメインモデルはアンチパターンであると考えているようです。

だから私は本当にこの問題についていくつかの洞察を得たいと思っていました。

また、私は本当にリッチドメインモデルの良い(基本的な)例と、それが提供する貧血ドメインモデルに対する利点を探しています。



14
DDD> ADMADM> DDDDDD> ADMADM> DDDADM + DDD ... DDD / ADM、またはソフトウェアの設計について同意する方法ではありません
sp00m

貧血ドメインモデルを回避する方法の例を次に示します。medium.com
Vadim Samokhin

11
この質問が、実際の組織によって資金提供された実際のプロジェクトへの単一のリンクで回答できたのはおかしいです。5年後、良い答えはありません、IMO。口で言うだけなら簡単です。コードを見せて。
Mateusz Stefek

回答:


57

違いは、貧血モデルがロジックをデータから分離することです。ロジックは、多くの場合、名前のクラスに配置され**Service**Util**Manager**Helperとになります。これらのクラスはデータ解釈ロジックを実装するため、データモデルを引数として使用します。例えば

public BigDecimal calculateTotal(Order order){
...
}

一方、リッチドメインアプローチは、データ解釈ロジックをリッチドメインモデルに配置することでこれを逆にします。したがって、ロジックとデータを組み合わせて、リッチドメインモデルは次のようになります。

order.getTotal();

これはオブジェクトの一貫性に大きな影響を与えます。データ解釈ロジックはデータをラップするため(データにはオブジェクトメソッドを介してのみアクセスできます)、メソッドは他のデータの状態変化に反応できます->これを動作と呼びます。

貧血モデルでは、データモデルは、それらがリッチドメインモデルである一方で、合法的な状態であることを保証できません。リッチドメインモデルは、カプセル化、情報の隠蔽、データとロジックの統合などのOOの原則を適用するため、貧血モデルはOOの観点からの反パターンです。

より深い洞察のために私のブログを見てくださいhttps://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/


15
注文の合計価格の計算に次のことが含まれているとします。2)ストアが実行している現在のマーケティングキャンペーンに応じて、特定のアイテムのグループを一緒に含む注文に割引を適用します。3)税額は注文の特定の項目ごとに異なります。あなたの意見では、このすべてのロジックはどこに属しますか?簡単な疑似コードの例を教えてください。ありがとうございました!
Nik 2016年

4
@Nikリッチモデルでは、OrderにはCustomerオブジェクトへの参照があり、Customerオブジェクトにはロイヤルティプログラムへの参照があります。したがって、注文は、サービスやリポジトリなどの情報をフェッチするための明示的な参照を必要とせずに、必要なすべての情報にアクセスできます。ただし、循環参照が発生している場合に遭遇するのは簡単に思えます。つまり、注文は顧客を参照し、顧客はすべての注文のリストを持っています。これが、今や人々が貧血を好む理由の一部かもしれないと思います。
2017年

3
@crushあなたが説明するアプローチは本当にうまくいきます。一つの落とし穴があります。おそらく、エンティティをDBに格納します。したがって、注文の合計を計算するには、DBから注文、顧客、ロイヤルティプログラム、マーケティングキャンペーン、税金テーブルをフェッチする必要があります。また、顧客には注文のコレクションがあり、ポイントプログラムには顧客のコレクションがある、などと考えてください。これらすべてを単純にフェッチすると、DB全体がRAMにロードされます。これはもちろん実行可能ではないので、DBから関連データのみをロードすることにします... 1/2
Nik

3
@Nik「これらすべてをネイティブでフェッチすると、DB全体がRAMにロードされます。」これも、私の心にあるリッチモデルの主な欠点の1つです。ドメインが大きく複雑になり、インフラストラクチャの制限に達し始めるまで、リッチモデルは適切です。ただし、レイジーロードORMが役立ちます。良いモデルを見つければ、必要なのが20分の1であるときにDB全体をメモリに読み込まなくても、豊富なモデルを保持できます。とはいえ、貧血とリッチの間を何年も行ったり来たりして、私はCQRSで貧血モデルを使用する傾向があります。
クラッシュ

2
考慮すべきもう1つのことは、ビジネスドメインロジックが存在する場所です。私の意見では、ますます多くの開発者がデータベースから、そしてそれが属するアプリケーションにシフトしています。しかし、会社がビジネスロジックをデータベースレイヤー(ストアドプロシージャ)に残すことを要求している状況で行き詰まっている場合、リッチドメインモデルにそのロジックを追加してもメリットはほとんどありません。実際、ストアドプロシージャがアプリケーションのドメインレイヤーとは異なるルールを持っている場合、競合が発生するように設定しているだけかもしれません...
クラッシュ

53

Bozhidar Bozhanovは、このブログ投稿で貧血モデルを支持して主張しているようです。

ここに彼が提示する要約があります:

  • ドメインオブジェクトは、Spring(IoC)で管理されるべきではなく、DAOやインフラストラクチャに関連するものが注入されてはいけません。

  • ドメインオブジェクトには、依存するドメインオブジェクトがhibernate(または永続化メカニズム)によって設定されています。

  • ドメインオブジェクトは、DDDのコアアイデアと同様にビジネスロジックを実行しますが、これにはデータベースクエリやCRUDは含まれません。オブジェクトの内部状態に対する操作のみが含まれます

  • DTOが必要になることはほとんどありません–ほとんどの場合、ドメインオブジェクト自体がDTOです(これにより、ボイラープレートコードが節約されます)。

  • サービスはCRUD操作の実行、電子メールの送信、ドメインオブジェクトの調整、複数のドメインオブジェクトに基づくレポートの生成、クエリの実行などを行います。

  • サービス(アプリケーション)レイヤーはそれほど薄くはありませんが、ドメインオブジェクトに固有のビジネスルールは含まれていません

  • コード生成は避けてください。抽象化、設計パターン、DIを使用して、コード生成の必要性を克服し、最終的にはコードの重複を取り除く必要があります。

更新

私が最近この記事を読んだとき、著者は一種のハイブリッドアプローチに従うことを提唱しています。ドメインオブジェクトは、その状態のみに基づいてさまざまな質問に答えることができます(完全に貧血モデルの場合は、おそらくサービスレイヤーで行われます)。


11
その記事から、Bozhoが貧血ドメインモデルを支持して主張しているように思われることはありません。サービス(アプリケーション)レイヤーはそれほど薄くはありませんが、ドメインオブジェクトに固有のビジネスルールは含まれていません。私が理解していることは、ドメインオブジェクトには、それらに固有のビジネスロジックが含まれている必要がありますが、他のインフラストラクチャロジックは含まれていてはなりません。このアプローチは、私には貧血ドメインモデルのようには思えません。
Utku 2017年

8
また、これは、ドメインオブジェクトがビジネスロジックを実行することです。これは、DDDの中核となる考え方ですが、これには、データベースクエリやCRUDは含まれず、オブジェクトの内部状態に対する操作のみが含まれます。これらのステートメントは、貧血ドメインモデルをまったく支持していないようです。インフラストラクチャロジックをドメインオブジェクトに結合するべきではないと述べているだけです。少なくともそれは私が理解していることです。
Utku 2017年

@Utku私の見解では、Bozhoが2つのモデル間の一種のハイブリッドを提唱していることはかなり明らかです。ハイブリッドは、リッチモデルよりも貧血モデルに近いと言えます。
ジオおよび2017年

41

私の見解はこれです:

貧血ドメインモデル=オブジェクトにマッピングされたデータベーステーブル(フィールド値のみ、実際の動作なし)

リッチドメインモデル=動作を公開するオブジェクトのコレクション

単純なCRUDアプリケーションを作成したい場合は、おそらく古典的なMVCフレームワークを備えた貧血モデルで十分です。しかし、ある種のロジックを実装したい場合、貧血モデルはオブジェクト指向プログラミングを行わないことを意味します。

*オブジェクトの動作は永続性とは関係がないことに注意してください。別のレイヤー(データマッパー、リポジトリなど)がドメインオブジェクトの永続化を担当します。


5
私の無知に申し訳ありませんが、エンティティに関連するすべてのロジックをクラスに配置した場合、リッチドメインモデルはどのようにSOLIDプリンシペに従うことができますか。これは、SOLID原則に違反します。「S」は、単一の責任を表します。つまり、クラスは1つのことだけを実行し、正しく実行する必要があるということです。
redigaffi

5
@redigaffi「1つのこと」をどのように定義するかによって異なります。2つのプロパティと二つの方法を持つクラスを考えてみましょうxysumdifference。それは4つのことです。または、それは加算と減算(2つのこと)であると主張できます。または、それは数学(1つ)であると主張することもできます。SRPを適用することでバランスをとる方法については、多くのブログ投稿があります。こちらが1つです。hackernoon.com
Rainbolt、

2
DDDでは、単一の責任は、クラス/モデルがシステム全体の残りの部分に副作用を引き起こすことなく、それ自体の状態を管理できることを意味します。それ以外の定義は、私の経験において面倒な哲学的議論を招くだけです。
ZombieTfk

12

まず、この記事の回答を貼り付けてコピーします http://msdn.microsoft.com/en-gb/magazine/dn385704.aspx

図1は、Anemic Domain Modelを示しています。これは、基本的にはゲッターとセッターを持つスキーマです。

Figure 1 Typical Anemic Domain Model Classes Look Like Database Tables

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}

このよりリッチなモデルでは、単に読み書きされるプロパティを公開するのではなく、Customerのパブリックサーフェスは明示的なメソッドで構成されています。

Figure 2 A Customer Type That’s a Rich Domain Model, Not Simply Properties

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    ShippingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = companyName;
  }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status { get; private set; }
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}

2
オブジェクトを作成するメソッドと、新しく作成されたオブジェクトにプロパティを割り当てるメソッドの両方に問題があります。コードの拡張性と柔軟性が低下します。1)コードの利用者AddressExtendedAddress、からAddressではなく、から継承され、いくつかの追加のプロパティを作成したい場合はどうなりますか?2)またはCustomerCreditCardBankID代わりに取るようにコンストラクタパラメータを変更しますBankNameか?
ライトマン

アドレスを作成するのに、オブジェクトを構成するものよりも追加のサービスが必要ですか?これらのサービスを取得するためのメソッドインジェクションが残ります。それがたくさんのサービスだとしたら?
2017年

8

リッチドメインクラスの利点の1つは、任意のレイヤーのオブジェクトへの参照があるたびに、その動作(メソッド)を呼び出すことができることです。また、共同作業を行う小規模で分散したメソッドを作成する傾向があります。貧血ドメインクラスでは、通常はユースケースによって駆動される脂肪質の手続き型メソッド(サービスレイヤー内)を記述する傾向があります。これらは通常、リッチドメインクラスと比較してメンテナンス性が低くなります。

振る舞いのあるドメインクラスの例:

class Order {

     String number

     List<OrderItem> items

     ItemList bonus

     Delivery delivery

     void addItem(Item item) { // add bonus if necessary }

     ItemList needToDeliver() { // items + bonus }

     void deliver() {
         delivery = new Delivery()
         delivery.items = needToDeliver()
     }

}

メソッドneedToDeliver()は、ボーナスを含め、配信する必要があるアイテムのリストを返します。クラス内、別の関連クラス、または別のレイヤーから呼び出すことができます。あなたが渡した場合たとえば、Orderビューに、あなたが使用することができneedToDeliver()、選択のOrder彼らが持続するために保存ボタンをクリックする前にユーザに確認する項目の表示リストにOrder

コメントへの返答

これは私がコントローラーからドメインクラスを使用する方法です:

def save = {
   Order order = new Order()
   order.addItem(new Item())
   order.addItem(new Item())
   repository.create(order)
}

Orderとその作成はLineItem1つのトランザクションで行われます。いずれかをLineItem作成できない場合、作成されませんOrder

私は、次のような単一のトランザクションを表すメソッドを持つ傾向があります。

def deliver = {
   Order order = repository.findOrderByNumber('ORDER-1')
   order.deliver()       
   // save order if necessary
}

内部のものdeliver()はすべて1つのトランザクションとして実行されます。単一のトランザクションで多くの無関係なメソッドを実行する必要がある場合は、サービスクラスを作成します。

遅延読み込みの例外を回避するために、JPA 2.1名前付きエンティティグラフを使用します。たとえば、配信画面のコントローラで、などのdelivery属性を読み込んで無視するメソッドを作成できます。ボーナス画面では、私はその負荷に別のメソッドを呼び出し属性と無視するなど、。私はまだボーナス画面内で呼び出すことができないので、これには規律が必要です。bonusrepository.findOrderByNumberFetchDelivery()bonusdeliveryrepository.findOrderByNumberFetchBonus()deliver()


1
トランザクションのスコープはどうですか?
kboom 2014

5
ドメインモデルの動作には、永続化ロジック(トランザクションを含む)を含めないでください。それらは、データベースに接続せずに(単体テストで)テスト可能でなければなりません。トランザクションスコープは、サービスレイヤーまたは永続化レイヤーの責任です。
jocki 14

1
では、遅延読み込みはどうですか?
kboom 2014

単体テストでドメインクラスインスタンスを作成すると、それらはプレーンオブジェクトであるため、管理状態にはなりません。すべての動作を適切にテストできます。
jocki 14

そして、サービスレイヤーからのドメインオブジェクトを期待している場合はどうなりますか?それは管理されていませんか?
kboom 14

8

モノリシックデスクトップアプリの作成に使用していたとき、リッチドメインモデルを作成し、それらの作成を楽しんでいました。

現在、私は小さなHTTPマイクロサービスを作成しています。貧血DTOを含めて、コードはできるだけ少なくします。

DDDとこの貧弱な議論は、一体型のデスクトップまたはサーバーアプリの時代にさかのぼると思います。私はその時代を覚えており、貧血モデルは奇妙であることに同意します。私は大きなモノリシックFX取引アプリを構築しましたが、モデルはありませんでした。本当に、それは恐ろしいことでした。

マイクロサービスを使用すると、動作が豊富な小規模サービスは、間違いなく、構成可能なモデルであり、ドメイン内で集約されます。したがって、マイクロサービスの実装自体は、さらにDDDを必要としない場合があります。マイクロサービスアプリケーションがドメインである可能性があります。

注文マイクロサービスには、RESTfulリソースとして、またはSOAPを介してなど、非常に少数の機能しかありません。マイクロサービスコードの注文は非常に簡単な場合があります。

大規模でモノリシックな単一(マイクロ)サービス、特にモデルをRAMに保持するサービスは、DDDの恩恵を受ける可能性があります。


現在の最先端技術を表すHTTPマイクロサービスのコード例はありますか?あなたが何かを書くようにあなたに頼むのではなく、あなたが指すことができる何かがあればリンクを共有してください。ありがとう。
Casey Plummer

3

問題の根本は誤った二分法にあると思います。これらの2つのモデル(リッチで「貧血」)を抽出して、それらを互いに対比するにはどうすればよいでしょうか。クラスとは何かについて間違った考えを持っている場合にのみ可能だと思います。よくわかりませんが、YouTubeのBozhidar Bozhanovの動画で見つけたと思います。クラスは、このデータのデータ+メソッドではありません。これは完全に無効な理解であり、クラスを2つのカテゴリに分類します。データのみ、貧血モデルデータ+メソッド -非常に豊富なモデル(より正確には、3番目のカテゴリ:メソッドのみです)。

真実は、クラスがオントロジーモデルの概念、単語、定義、用語、アイデアであり、それがDENOTATであることです。そして、この理解は誤った二分法を排除します:それはあなたのモデルが適切でないことを意味するので、あなたは貧血モデルまたは豊かなモデルだけを持つことができません、それは現実に関連していません:いくつかの概念はデータのみを持ち、いくつかはメソッドのみを持ち、いくつかはそれらの混合されます。この場合、一部のカテゴリ、オブジェクトセット、関係、クラスの概念を記述しようとするため、一部の概念はプロセスのみ(メソッド)であり、一部は属性のみのセット(データ)であり、一部はそれらは属性(混合)との関係です。

適切なアプリケーションにはあらゆる種類のクラスが含まれている必要があり、狂ったように1つのモデルだけに制限されないようにする必要があります。とにかく、ロジックがどのように表されているかに関係なく:とにかく、コードまたは解釈可能なデータオブジェクト(Free Monadsなど)を使用して、プロセス、ロジック、関係、属性、機能、データなどを表すクラス(概念、denotats)が必要です。それらのいくつかを回避しようとするか、それらすべてを1種類のみに減らします。

したがって、別のクラスにロジックを抽出して、元のクラスにデータを残すことができますが、一部の概念には属性と関係/プロセス/メソッドを含めることができ、それらを分離すると、2つの名前で概念を複製できるため、意味がありませんパターンに縮小:「OBJECT-Attributes」と「OBJECT-Logic」。手続き型および関数型言語では制限があるため問題ありませんが、あらゆる種類の概念を記述できる言語には過度の制約があります。


1

貧血ドメインモデルはORMとネットワーク(すべての商用アプリケーションの生命線)を介した簡単な転送に重要ですが、オブジェクト指向はコードのカプセル化と「トランザクション/処理」部分の簡素化に非常に重要です。

したがって、重要なのは、特定の世界から別の世界に変換できることです。

AnemicUserやUserDAOなどのAnemicモデルに名前を付けて、開発者がより適切なクラスがあることを確認し、none Anemicクラスに適切なコンストラクターを用意する

User(AnemicUser au)

トランスポート/永続性のための貧血クラスを作成するためのアダプターメソッド

User::ToAnemicUser() 

トランスポート/永続性以外のあらゆる場所でnone Anemic Userを使用することを目指します


-1

役立つ例を以下に示します。

貧血

class Box
{
    public int Height { get; set; }
    public int Width { get; set; }
}

非貧血

class Box
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public Box(int height, int width)
    {
        if (height <= 0) {
            throw new ArgumentOutOfRangeException(nameof(height));
        }
        if (width <= 0) {
            throw new ArgumentOutOfRangeException(nameof(width));
        }
        Height = height;
        Width = width;
    }

    public int area()
    {
       return Height * Width;
    }
}

これは、ValueObjectとEntityに変換できるように見えます。
code5

ただ、コピーいずれかの説明もなく、ウィキペディアから貼り付ける
WST

誰がもっと早く書いたの?@wst
Alireza Rahmani Khalili

ウィキペディアの歴史によれば、@ AlirezaRahmaniKhaliliは最初の...
WST

-1

DDDへの従来のアプローチでは、AnemicモデルとRichモデルを絶対に回避するように述べられていません。ただし、MDAはすべてのDDD概念(境界付きコンテキスト、コンテキストマップ、値オブジェクトなど)を適用できますが、すべてのケースでAnemicモデルとRichモデルを使用します。ドメインサービスを使用して一連のドメインアグリゲート全体で複雑なドメインユースケースを調整する多くのケースがあり、アプリケーションレイヤーから単にアグリゲートを呼び出すよりもはるかに優れたアプローチです。従来のDDDアプローチとの唯一の違いは、すべての検証とビジネスルールがどこに存在するかです。モデルバリデーターとして知られる新しい構成要素があります。バリデーターは、ユースケースまたはドメインワークフローが実行される前に、完全な入力モデルの整合性を保証します。集約ルートおよび子エンティティは貧血ですが、必要に応じてそれぞれに独自のモデルバリデーターを呼び出すことができます。それはルートバリデーターです。バリデーターはまだSRPに準拠しており、保守が簡単で、ユニットテストが可能です。

このシフトの理由は、マイクロサービスへのUXファーストのアプローチよりも、APIファーストの方に向かっているからです。RESTはこの点で非常に重要な役割を果たしています。従来のAPIアプローチ(SOAPのため)は、最初はコマンドベースのAPIとHTTP動詞(POST、PUT、PATCH、GET、DELETE)に固定されていました。コマンドベースのAPIは、Rich Modelオブジェクト指向のアプローチによく適合し、依然として非常に有効です。ただし、シンプルなCRUDベースのAPIは、リッチモデルに収まるものの、シンプルな貧血モデル、バリデーター、およびドメインサービスに適しているため、残りを調整できます。

私はDDDが提供するすべての点で気に入っていますが、絶えず変化し、アーキテクチャへのより良いアプローチに適合するように少し拡張する必要があるときが来ます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.