DDD-エンティティがリポジトリに直接アクセスできないルール


185

ドメイン駆動設計では、エンティティがリポジトリに直接アクセスしてはならないという多く合意があるようです。

これは、Eric Evans Domain Driven Designによるものですか。本から来たのですか、それとも他の場所から来ましたか

その背後にある推論についての良い説明はどこにありますか?

編集:明確にするため:データアクセスをビジネスロジックとは別のレイヤーに分離する従来のOOプラクティスについて話しているのではなく、DDDではエンティティがデータに話しかけることを想定していない特定の配置について話しているアクセス層(つまり、Repositoryオブジェクトへの参照を保持することは想定されていません)

更新:彼の答えが最も近いようだったので、私はBacceSRに賞金を与えましたが、私はまだこれについてかなり暗いです。そのような重要な原則ならば、オンラインのどこかにそれについての良い記事があるはずです、きっと?

更新:2013年3月、質問への賛成票はこれに多くの関心があることを意味し、多くの回答があったとしても、人々がこれについてアイデアを持っているなら、まだまだ余裕があると思います。


私の質問stackoverflow.com/q/8269784/235715を見てください。エンティティにリポジトリへのアクセス権がなくてロジックをキャプチャするのが難しい状況を示しています。エンティティにリポジトリへのアクセス権を与えるべきではないと思いますが、リポジトリ参照なしでコードを書き換えることができるという状況への解決策はありますが、現時点では考えられません。
Alex Burtsev

それがどこから来たのかわからない。私の考え:この誤解は、DDDが何であるかを理解していない人々に起因すると思います。このアプローチはソフトウェアを実装するためではなく、それを設計するためのものです(ドメイン..設計)。昔は、アーキテクトとインプリメンターがいましたが、今はソフトウェア開発者だけです。DDDは建築家向けです。また、アーキテクトがソフトウェアを設計するとき、準備された設計を実装する開発者のために、メモリまたはデータベースを表すためのツールまたはパターンが必要です。しかし、デザイン自体(ビジネスの観点から)には、リポジトリは必要ありません。
berhalak

回答:


47

ここで少し混乱があります。リポジトリは集約ルートにアクセスします。集計ルートはエンティティです。この理由は、懸念の分離と適切な階層化です。これは小さなプロジェクトでは意味がありませんが、大規模なチームに所属している場合は、「製品リポジトリから製品にアクセスします。製品は、ProductCatalogオブジェクトを含むエンティティのコレクションの集約ルートです。 ProductCatalogを更新する場合は、ProductRepositoryを実行する必要があります。」

このようにして、ビジネスロジックと更新の場所が非常に明確に分離されます。あなたは一人で離れて、製品カタログにこれらの複雑なすべてを行うこのプログラム全体を書いている子供はいません。それを上流のプロジェクトに統合することになると、そこに座ってそれを見て理解しますすべてを捨てる必要があります。また、チームに加わって新しい機能を追加したときに、どこに行くべきか、そしてプログラムをどのように構成するかがわかります。

ちょっと待って!リポジトリは、リポジトリパターンと同様に、永続性レイヤーも指します。より良い世界では、Eric Evansのリポジトリとリポジトリパターンは、かなり重複する傾向があるため、別々の名前になります。リポジトリー・パターンを取得するために、サービス・バスまたはイベント・モデル・システムを使用して、データにアクセスする他の方法とは対照的です。通常、このレベルに到達すると、Eric Evansのリポジトリ定義が脇を行き、境界のあるコンテキストについて話し始めます。境界のある各コンテキストは、基本的に独自のアプリケーションです。あなたは物事を製品カタログに入れるための洗練された承認システムを持っているかもしれません。元のデザインでは製品は中心的要素でしたが、この限られた状況では製品カタログはそうです。サービスバス経由で製品情報にアクセスして製品を更新することもできます。

元の質問に戻ります。エンティティー内からリポジトリーにアクセスしている場合、それはエンティティーが実際にはビジネス・エンティティーではなく、おそらくサービス層に存在するはずのものであることを意味します。これは、エンティティがビジネスオブジェクトであり、DSL(ドメイン固有言語)にできるだけ似ていることに関心を持つ必要があるためです。このレイヤーにはビジネス情報のみが含まれます。パフォーマンスの問題のトラブルシューティングを行う場合、ここにはビジネス情報のみを含める必要があるため、他の場所を探すことになります。突然、ここでアプリケーションの問題が発生した場合、アプリケーションの拡張と保守が非常に難しくなります。これは、DDDの中心である保守可能なソフトウェアの作成です。

コメント1への応答:正しい質問です。したがって、すべての検証がドメイン層で行われるわけではありません。Sharpには、必要なことを行う「DomainSignature」属性があります。これは永続性を認識しますが、属性であることにより、ドメインレイヤーをクリーンに保ちます。これにより、例では同じ名前のエンティティが重複しないようになります。

しかし、もっと複雑な検証ルールについて話しましょう。あなたがAmazon.comだとしましょう。期限切れのクレジットカードで何かを注文したことがありますか?カードを更新しておらず、何かを購入しました。それは注文を受け入れ、UIはすべてが桃色であることを私に通知します。約15分後、注文に問題があり、クレジットカードが無効であるというメールが届きます。ここで何が起こっているかというと、理想的には、ドメインレイヤーで正規表現の検証が行われるということです。これは正しいクレジットカード番号ですか?はいの場合、注文を継続します。ただし、アプリケーションタスクレイヤーには追加の検証があり、クレジットカードで支払いができるかどうかを確認するために外部サービスが照会されます。そうでない場合は、実際には何も出荷せず、注文を一時停止して、顧客を待ちます。

リポジトリにアクセスできるサービス層で検証オブジェクトを作成することを恐れないでください。ドメインレイヤーには入れないでください。


15
ありがとう。しかし、エンティティー(およびそれらに関連付けられたファクトリーや仕様など)に可能な限り多くのビジネス・ロジックを組み込むように努力する必要がありますよね?しかし、それらのいずれもがリポジトリを介してデータをフェッチすることを許可されていない場合、どのようにして(合理的に複雑な)ビジネスロジックを記述することになっていますか?例:チャットルームのユーザーは、自分の名前を他のユーザーがすでに使用している名前に変更することはできません。そのルールをChatUserエンティティに組み込むことを希望しますが、そこからリポジトリにアクセスできない場合は、それほど簡単ではありません。だから私は何をすべきですか?
codeulike 2011

私の応答はコメントボックスが許容するよりも大きかった、編集を参照してください。
ケルトーシス

6
エンティティは、自身を害から保護する方法を知っている必要があります。これには、無効な状態にならないようにすることも含まれます。チャットルームのユーザーについて説明しているのは、エンティティが自身を有効に保つために必要なロジックに追加して存在するビジネスロジックです。必要なものなどのビジネスロジックは、ChatUserエンティティではなく、Chatroomサービスに属しています。
Alec

9
ありがとうアレック。それはそれを表現する明確な方法です。しかし、私にとっては、「すべてのビジネスロジックはドメインレイヤーに移動する」というEvansのドメイン中心のゴールデンルールは、「エンティティがリポジトリにアクセスしてはならない」というルールと矛盾しているようです。理由がわかればそれで十分ですが、エンティティがリポジトリにアクセスしてはならない理由をオンラインで説明することはできません。エヴァンスはそれを明確に述べていないようです。それはどこから来たのか?いくつかの優れた文献を指す回答を投稿できれば、50ptの賞金を手に入れることができるかもしれません:)
codeulike

4
「彼は小さなことでは意味をなさない」これはチームが犯す大きな間違いです...それは小さなプロジェクトなので、私はこれを行うことができます...そのように考えるのをやめます。私たちが取り組んでいる小規模なプロジェクトの多くは、ビジネス要件のために、結局は大きくなってしまいます。小さくても大きくても何かをしている場合は、正しく行います。
MeTitus 2013

35

最初は、一部のエンティティにリポジトリへのアクセスを許可するように説得しました(つまり、ORMなしの遅延読み込み)。後で私はすべきではないという結論に達し、別の方法を見つけることができました。

  1. リクエストの意図とドメインに何を求めているかを知っている必要があるため、Aggregateの動作を作成または呼び出す前にリポジトリを呼び出すことができます。これは、メモリ内の状態の不整合の問題や遅延読み込みの必要性を回避するのにも役立ちます(この記事を参照)。匂いは、データアクセスを心配せずにエンティティのメモリ内インスタンスを作成できないことです。
  2. CQS(コマンドクエリの分離)を使用すると、エンティティ内のオブジェクトのリポジトリを呼び出す必要性を減らすことができます。
  3. 仕様を使用してドメインロジックのニーズをカプセル化して伝達し、代わりにそれをリポジトリに渡すことができます(サービスはこれらのことを調整できます)。仕様は、その不変条件の維持を担当するエンティティから取得できます。リポジトリは、仕様の一部を独自のクエリ実装に解釈し、仕様からのルールをクエリ結果に適用します。これは、ドメインロジックをドメイン層に保持することを目的としています。また、ユビキタス言語とコミュニケーションをよりよく提供します。「期限切れの注文の指定」と言うか、「placedatがsysdateの30分前であるtbl_orderからの注文をフィルター処理する」と言うことを想像してください(この回答を参照)。
  4. 単一責任原則に違反しているため、エンティティの動作についての推論がより困難になります。ストレージ/永続性の問題を解決する必要がある場合は、どこに行けばどこに行けないかがわかります。
  5. これにより、エンティティーに(リポジトリーおよびドメイン・サービスを介して)グローバル状態への双方向アクセスを与える危険が回避されます。また、トランザクションの境界を越えたくない場合もあります。

赤い本「ドメイン駆動設計の実装」のVernon Vaughnは、私が知っている2つの場所でこの問題について言及しています(注:この本は、序文で読むことができるように、Evansによって完全に承認されています)。サービスに関する第7章では、ドメインサービスと仕様を使用して、集約がリポジトリと別の集約を使用してユーザーが認証されているかどうかを判断する必要性を回避しています。彼は言っていると引用されています:

経験則として、できる限り、集約内からのリポジトリ(12)の使用は避けるようにしてください。

ヴァーノン、ヴォーン(2013-02-06)。ドメイン駆動設計の実装(Kindleロケーション6089)。ピアソン教育。キンドル版。

また、アグリゲートに関する第10章の「モデルナビゲーション」というタイトルセクションで、彼は次のように述べています(他のアグリゲートルートを参照するためにグローバル一意IDの使用を推奨した直後):

IDによる参照は、モデル全体のナビゲーションを完全には妨げません。一部のユーザーは、集約用のリポジトリ(12)をルックアップに使用します。この手法はDisconnected Domain Modelと呼ばれ、実際には遅延読み込みの形式です。ただし、推奨される別のアプローチがあります。Aggregateの動作を呼び出す前に、リポジトリまたはドメインサービス(7)を使用して依存オブジェクトを検索します。クライアントアプリケーションサービスがこれを制御し、Aggregateにディスパッチします。

彼はこの例をコードで示しています:

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

彼はまた、ドメインサービスをdouble-dispatchとともにAggregateコマンドメソッドで使用する方法のさらに別のソリューションについても言及しています。(私は彼の本を読むのがどれほど有益であるかを十分にお勧めすることはできません。インターネットで延々と物事を練るのに疲れたら、相応の価値のあるお金を手放して本を読んでください。)

次に、いつも優雅なMarco Pivetta @Ocramius話し合い、ドメインから仕様を引き出してそれを使用するためのコードを少し見せてくれました。

1)これは推奨されません。

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2)ドメインサービスでは、これは適切です。

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}

1
質問:無効または一貫性のない状態でオブジェクトを作成しないように常に教えられています。リポジトリからユーザーをロードし、その後getFriends()何をする前に呼び出すと、空または遅延ロードされます。空の場合、このオブジェクトは横になっていて無効な状態です。これについて何か考えはありますか?
神保

リポジトリは、ドメインを呼び出してインスタンスを新規作成します。ドメインを経由せずにユーザーのインスタンスを取得することはできません。この回答が対処する問題は逆です。ドメインがリポジトリを参照している場合、これは回避する必要があります。
プログラハンマー、

28

その非常に良い質問です。これについての議論が楽しみです。しかし、私はそれが言及されていると思いますいくつかのDDDの本やJimmy nilssonsやEric Evansで。例のように、レポジトリーのパターンを使用する方法からもわかると思います。

しかし、話し合いましょう。エンティティが別のエンティティを永続化する方法を知っている必要があるのは、非常に有効な考えだと思います DDDで重要なのは、各エンティティが独自の「知識範囲」を管理する責任を持ち、他のエンティティの読み取りまたは書き込み方法について何も知らないようにする必要があることです。確かに、エンティティBを読み取るためのリポジトリインターフェイスをエンティティAに追加するだけで済みます。しかし、リスクは、Bを永続化する方法に関する知識を公開することです。エンティティAは、Bをdbに永続化する前にBの検証も行いますか?

ご覧のように、エンティティAはエンティティBのライフサイクルにより深く関与することができ、それによってモデルがさらに複雑になる可能性があります。

ユニットテストはより複雑になると思います(例はありません)。

しかし、エンティティーを介してリポジトリーを使用したくなるシナリオが常にあると私は確信しています。有効な判断を行うには、各シナリオを確認する必要があります。長所と短所。しかし、私の意見では、リポジトリエンティティソリューションは多くの短所から始まります。それは短所をバランスさせる長所を備えた非常に特別なシナリオでなければなりません...


1
いい視点ね。おそらく古い学校のドメインモデルでは、エンティティBがそれ自体を永続化する前にそれを検証する責任を負っていると思います。エバンスがリポジトリを使用していないエンティティに言及していることを確信していますか?私は本の途中で、まだそれについて言及していません...
codeulike '18 / 4/18

ええと、私は数年前にこの本を読んで(3つ...)、私の記憶は失敗に終わりました。彼がそれを正確に言ったかどうかは思い出せませんが、彼は例を使ってこれを説明したと思います。また、dddsamplenet.codeplex.comで、彼のCargoの例のコミュニティによる解釈(彼の本から)を見つけることもできます。コードプロジェクトをダウンロードします(バニラプロジェクト-本の例をご覧ください)。リポジトリは、ドメインエンティティにアクセスするためにアプリケーション層でのみ使用されることがわかります。
Magnus Backeus、2011

1
書籍p2p.wrox.com/からDDD SmartCAの例をダウンロードすると 、リポジトリはサービスで使用されますが(ここでは奇妙なことではありません)、別のアプローチがありますが、サービスはエンティティ内で使用されます。これは私がやらないことですが、私はウェッブアプリの人です。オフラインで作業する必要があるSmartCAアプリのシナリオを考えると、おそらくdddの設計は異なって見えるでしょう。
Magnus Backeus、2011

SmartCAの例は興味深く聞こえますが、どの章にありますか?(コードのダウンロードは章ごとに配置されています)
codeulike

1
@codeulike現在、dddの概念を使用してフレームワークを設計および実装しています。検証を行うと、データベースにアクセスしてクエリを実行する必要がある場合があります(例:複数列の一意のインデックスチェックのクエリ)。これと、クエリがリポジトリレイヤーに書き込まれる必要があるという事実に関して、ドメインエンティティは、検証をドメインモデルレイヤーに完全に配置するために、ドメインモデルレイヤーのリポジトリインターフェイス。それでは、ドメインエンティティがリポジトリにアクセスできるようになっても大丈夫ですか?
Karamafrooz 2017年

13

データアクセスを分離する理由

この本から、「モデル駆動型設計」の章の最初の2ページは、ドメインモデルの実装から技術的な実装の詳細を抽象化したい理由を正当化するものだと思います。

  • ドメインモデルとコードの間の密接な接続を維持したい
  • 技術的な懸念を分離することで、モデルが実装に実用的であることを証明できます
  • ユビキタス言語をシステムの設計に浸透させたい

これは、システムの実際の実装から切り離される別の「分析モデル」を回避するためのすべてのようです。

この本を理解したところ、この「分析モデル」はソフトウェアの実装を考慮せずに設計されてしまう可能性があると書かれています。開発者は、ビジネス側が理解したモデルを実装しようとすると、必要に応じて独自の抽象化を形成し、コミュニケーションと理解の壁を引き起こします。

反対に、ドメインモデルにあまりにも多くの技術的な懸念を持ち込む開発者は、この分裂を引き起こす可能性もあります。

したがって、持続性などの懸念の分離を実践することは、これらの設計に対する分析モデルの発散を防ぐのに役立つと考えることができます。持続性などをモデルに導入する必要があると感じた場合、それは危険信号です。多分このモデルは実装には実用的ではありません。

引用:

「デザインは、慎重に検討されたモデルの直接の結果となったため、単一モデルはエラーの可能性を低減します。デザイン、さらにはコード自体も、モデルの伝達​​性を持っています。」

私がこれを解釈している方法では、データベースアクセスのようなものを処理するコードの行が多くなった場合、その伝達性が失われます。

データベースにアクセスする必要性が一意性のチェックなどの目的である場合は、以下を参照してください。

Udi Dahan:チームがDDDを適用するときに犯す最大の間違い

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

「すべてのルールが等しく作成されていない」の下

そして

ドメインモデルパターンの採用

http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119

「ドメインモデルを使用しないシナリオ」では、同じ主題に触れています。

データアクセスを分離する方法

インターフェースを介したデータの読み込み

「データアクセスレイヤー」は、必要なデータを取得するために呼び出すインターフェイスを通じて抽象化されています。

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

長所:このインターフェースでは、「データアクセス」配管コードが分離されているため、テストを記述できます。データアクセスはケースバイケースで処理できるため、一般的な戦略よりも優れたパフォーマンスが得られます。

短所:呼び出しコードは、何がロードされ、何がロードされていないかを想定する必要があります。

たとえば、GetOrderLinesは、パフォーマンス上の理由から、ProductInfoプロパティがnullのOrderLineオブジェクトを返します。開発者は、インターフェイスの背後にあるコードについて詳しく知っている必要があります。

私は実際のシステムでこの方法を試しました。パフォーマンスの問題を修正するために、常に読み込まれる対象の範囲を変更することになります。ロードされているものとロードされていないものを確認するためにデータアクセスコードを確認するために、インターフェイスの後ろをのぞき込むことになります。

現在、懸念事項を分離することで、開発者はコードの1つの側面にできるだけ集中することができます。インターフェース手法は、このデータがロードされた方法を削除しますが、ロードされたHOW MUCHデータはロードされません。

結論:かなり低い分離!

遅延読み込み

データはオンデマンドで読み込まれます。データをロードするための呼び出しは、オブジェクトグラフ自体の中に隠されています。プロパティにアクセスすると、結果を返す前にSQLクエリが実行される可能性があります。

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

長所:データアクセスの「いつ、どこで、どのように」は、ドメインロジックに焦点を当てた開発者から隠されています。データのロードを処理するコードは、集計に含まれていません。ロードされるデータの量は、コードが必要とする正確な量になる場合があります。

短所:パフォーマンスの問題に遭遇した場合、一般的な「1つのサイズですべてに対応する」ソリューションがあると、修正が困難です。遅延読み込みは全体的なパフォーマンスを低下させる可能性があり、遅延読み込みの実装は注意が必要です。

役割インターフェイス/熱心なフェッチ

各ユースケースは、集約クラスによって実装されたロールインターフェースを介して明示的になり、ユースケースごとにデータロード戦略を処理できるようになります。

フェッチ戦略は次のようになります。

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

次に、集計は次のようになります。

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

BillOrderFetchingStrategyを使用して集約を作成し、集約がその作業を行います。

長所:ユースケースごとにカスタムコードを使用できるため、最適なパフォーマンスを実現できます。インターフェース分離の原則に沿っています。複雑なコード要件はありません。集約ユニットテストは、読み込み戦略を模倣する必要はありません。一般的なローディング戦略はほとんどの場合に使用でき(「すべてロード」戦略など)、必要に応じて特別なローディング戦略を実装できます。

短所:ドメインコードを変更した後も、開発者はフェッチ戦略を調整/確認する必要があります。

フェッチ戦略アプローチを使用しても、ビジネスルールの変更に合わせてカスタムフェッチコードを変更していることに気付く場合があります。これは懸念事項を完全に分離するものではありませんが、最終的には保守しやすくなり、最初のオプションよりも優れています。フェッチ戦略は、HOW、WHEN、およびWHEREデータがロードされることをカプセル化します。1つのサイズがすべての遅延読み込みアプローチに適合するような柔軟性を失うことなく、問題をより適切に分離します。


ありがとう、リンクをチェックします。しかし、あなたの答えでは、「懸念の分離」と「まったくアクセスできない」とを混同していますか?確かに、ほとんどの人は、永続層をエンティティが存在する層から分離する必要があることに同意します。インターフェース'。
codeulike、2011

インターフェースを介してデータをロードするかどうかに関係なく、ビジネスルールを実装する際のデータのロードに引き続き関心があります。多くの人々がこの懸念の分離をまだ呼んでいることには同意しますが、おそらく単一責任の原則を使用する方が適切でしょう。
TTG 2011

1
最後のコメントを解析する方法がよくわかりませんが、ビジネスルールの処理中にデータをロードしないように提案していると思いますか?ルールが「より純粋」になると思います。しかし、多くのタイプのビジネスルールは他のデータを参照する必要があります-別のオブジェクトによって事前にロードする必要があることを示唆していますか?
codeulike、

@codeulike:私は私の答えを更新しました。どうしても必要な場合は、ビジネスルールの実行中にデータを読み込むこともできますが、データアクセスコードの行をドメインモデルに追加する必要はありません(遅延読み込みなど)。私が設計したドメインモデルでは、通常、データはあなたが言ったように事前に読み込まれるだけです。ビジネスルールを実行するのに、通常、過剰な量のデータは必要ないことがわかりました。
ttg 2011年


12

なんて素晴らしい質問でしょう。私は同じ発見の道を進んでおり、インターネット全体のほとんどの回答は、解決策をもたらすのと同じくらい多くの問題をもたらすようです。

それで(1年後に私が同意しないものを書くリスクがある)ここに私の発見があります。

まず、リッチドメインモデルが好きです。これにより、(集計で実行できることの)高い発見可能性と読みやすさ(表現力のあるメソッド呼び出し)が得られます。

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

次の理由により、エンティティのコンストラクタにサービスを注入せずにこれを実現したいと考えています。

  • (新しいサービスを使用する)新しい動作を導入すると、コンストラクターが変更される可能性があります。つまり、変更は、エンティティーをインスタンス化するすべての行に影響します
  • これらのサービスはモデルの一部ではありませんが、コンストラクターインジェクションはそうであると示唆します。
  • 多くの場合、サービス(そのインターフェースでさえ)は、ドメインの一部ではなく、実装の詳細です。ドメインモデルには、外向きの依存関係があります。
  • これは、可能な混乱エンティティは、これらの依存関係なしでは存在できない理由。(貸方票サービスだと思いますか?私は貸方票で何もしません...)
  • これはインスタンス化を困難にし、テスト困難にします
  • この問題を含む他のエンティティは同じ依存関係を取得するため、問題は簡単に広がります。それらのエンティティでは、非常に不自然な依存関係のように見える場合があります。

では、どうすればこれを実現できるでしょうか。これまでの私の結論は、メソッドの依存関係二重ディスパッチは適切なソリューションを提供するということです。

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()現在は、クレジットノートの作成を担当するサービスが必要です。エンティティからの検出可能性維持しながら、ダブルディスパッチを使用して、作業を責任のあるサービスに完全にオフロードします。Invoice

SetStatus()今では、ロガー単純な依存関係があります。これは明らかに作業の一部実行します。

後者の場合、クライアントコードの処理を簡単にするために、代わりにを介してログに記録する場合がありIInvoiceServiceます。結局のところ、請求書ロギングは請求書にかなり内在しているようです。このような単一のIInvoiceServiceサービスは、さまざまな操作のためのあらゆる種類のミニサービスの必要性を回避するのに役立ちます。欠点は、そのサービスが正確に何をするかが不明確になることです。ほとんどの作業はまだそれ自体で行われていますが、ダブルディスパッチのように見える場合もありSetStatus()ます。

意図を明らかにすることを期待して、パラメーターを「ロガー」と名付けることもできます。ただし、少し弱いようです。

代わりに、IInvoiceLogger(コードサンプルで既に行っているように)を要求し、IInvoiceServiceそのインターフェイスを実装することを選択します。クライアントシグネチャは、そのような非常に特定の請求書固有の「ミニサービス」を要求するIInvoiceServiceすべてのInvoiceメソッドに単一を使用できますが、メソッドシグネチャは、要求する内容を十分に明確にします。

私は明示的にリポジトリに対応していないことに気づきました。さて、ロガーはリポジトリであるか、リポジトリを使用していますが、より明確な例も示します。1つまたは2つの方法でリポジトリが必要な場合は、同じアプローチを使用できます。

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

実際、これは、面倒なレイジーロードの代替手段となります

更新:以下のテキストは歴史的な目的で残しましたが、遅延負荷を100%回避することをお勧めします。

真の、プロパティベースのレイジーロードの場合、私現在コンストラクターインジェクションを使用ていますが、永続性を無視した方法で使用しています。

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

一方、Invoiceデータベースからをロードするリポジトリは、対応するクレジットノートをロードする関数に自由にアクセスでき、その関数をに挿入できますInvoice

一方、実際のnew を作成するコードInvoiceは、空のリストを返す関数を渡すだけです。

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(慣習ILazy<out T>により、への醜いキャストを取り除くことができますがIEnumerable、それは議論を複雑にします。)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

皆様のご意見、ご要望、改善点をお待ちしております!


3

私にとって、これはDDDに固有のものではなく、OODに関連する一般的な良い習慣のようです。

私が考えることができる理由は次のとおりです。

  • 懸念の分離(エンティティは、永続化の方法から分離する必要があります。使用シナリオによっては、同じエンティティを永続化する戦略が複数存在する可能性があるため)
  • 論理的には、エンティティは、リポジトリが動作するレベルよりも下のレベルで確認できます。下位レベルのコンポーネントは、上位レベルのコンポーネントに関する知識を持っていてはなりません。したがって、エントリにはリポジトリに関する知識があってはなりません。

2

単にVernon Vaughnがソリューションを提供します。

リポジトリまたはドメインサービスを使用して、集約動作を呼び出す前に依存オブジェクトを検索します。クライアントアプリケーションサービスがこれを制御できます。


しかし、エンティティからではありません。
ssmith 2017年

Vernon Vaughn IDDDソースから:public class Calendar extends EventSourcedRootEntity {... public CalendarEntry scheduleCalendarEntry(CalendarIdentityService aCalendarIdentityService、
Teimuraz

彼の論文@Teimurazをチェックしてください
Alireza Rahmani Khalili

1

この個別のレイヤーの話題がすべて現れる前に、オブジェクト指向プログラミングをコーディングする方法を学び、最初のオブジェクト/クラスDIDをデータベースに直接マップしました。

最終的に、別のデータベースサーバーに移行する必要があったため、中間層を追加しました。同じシナリオについて何度か見たり聞いたりしたことがあります。

データアクセス(別名「リポジトリ」)をビジネスロジックから分離することは、ドメインドリブンデザインブックに代わって何度か再発明され、多くの「ノイズ」を生み出すことの1つだと思います。

私は現在、多くの開発者と同じように、3つのレイヤー(GUI、ロジック、データアクセス)を使用しています。

データをRepositoryレイヤー(別名Data Accessレイヤー)に分離することは、ルールに従うだけでなく、従うのに適したプログラミング手法のように見えます。

多くの方法論のように、実装されていないことから始めて、最終的にプログラムを理解したら、プログラムを更新することができます。

引用:IliadはHomerによって完全に発明されたのではなく、Carmina BuranaはCarl Orffによって完全に発明されたわけではありません。


1
ありがとうございます。ただし、データアクセスをビジネスロジックから分離することについて質問しているわけではありません。これについては、非常に幅広い合意があることは明らかです。S#arpなどのDDDアーキテクチャーでは、エンティティがデータアクセスレイヤーと「通信」することさえできない理由を尋ねています。それについて私が多くの議論を見つけることができなかったその興味深い配置。
codeulike、2011

0

これは、Eric Evans Domain Driven Designの本から来たのですか、それとも他の場所から来たのですか?

古いものです。エリックの本はもう少し話題になった。

その背後にある推論についての良い説明はどこにありますか?

理由は単純です-あいまいに関連する複数のコンテキストに直面すると、人間の心は弱くなります。それらはあいまいさをもたらし(南アメリカ/北アメリカのアメリカは南北アメリカを意味します)、あいまいさは心が「それに触れる」たびに常に情報のマッピングをもたらし、それは悪い生産性とエラーとしてまとめられます。

ビジネスロジックはできるだけ明確に反映する必要があります。外部キー、正規化、オブジェクトリレーショナルマッピングは完全に異なるドメインからのものです-それらは技術的なもので、コンピューターに関連しています。

類推:手書きの方法を学習している場合は、ペンがどこで作られたか、インクが紙に保持されている理由、紙が発明された時期、および他の有名な中国の発明とは何かを理解する必要はありません。

編集:明確にするため:データアクセスをビジネスロジックとは別のレイヤーに分離する従来のOOプラクティスについて話しているのではなく、DDDではエンティティがデータに話しかけることを想定していない特定の配置について話しているアクセス層(つまり、Repositoryオブジェクトへの参照を保持することは想定されていません)

理由はまだ私が上で述べたのと同じです。ここでは、さらに一歩進んでいます。エンティティが完全に(少なくとも近く)存在できるのに、なぜエンティティは部分的に永続性を無視する必要があるのですか?私たちのモデルが抱えるドメインに関係のない懸念が少なくなります-それを再解釈しなければならないときに、私たちの心がより多くの呼吸空間を得ることができます。


正しい。では、完全に永続性を知らないエンティティが永続性レイヤーと通信することさえ許可されていない場合、ビジネスロジックをどのように実装するのでしょうか。他の任意のエンティティの値を確認する必要がある場合、何をしますか?
codeulike

エンティティが他の任意のエンティティの値を調べる必要がある場合、おそらくいくつかの設計上の問題があります。おそらく、よりまとまりのあるクラスにするために、クラスを分割することを検討してください。
cdaq 2013

0

Carolina Lilientahlを引用すると、「パターンは循環を防ぐ必要がありますhttps://www.youtube.com/watch?v=eJjadzMRQAkで、クラス間の循環依存関係を参照しています。アグリゲート内のリポジトリの場合、唯一の理由として、オブジェクトナビゲーションの利便性から周期的な依存関係を作成するという誘惑があります。上記のprograhammerによるパターンは、Vernon Vaughnによって推奨されたもので、他の集約がルートインスタンスではなくIDによって参照されます(このパターンに名前はありますか?)。

クラス間の循環依存の例(告白):

(Time0):2つのクラス、SampleとWellは互いに参照します(循環依存)。ウェルとはサンプルのことで、サンプルとはウェルのことです(サンプルをループすることもあれば、プレート内のすべてのウェルをループすることもあります)。Sampleが配置されたWellをSampleが参照しないケースは想像できませんでした。

(Time1):1年後、多くのユースケースが実装されます。サンプルが配置されたウェルをサンプルが参照してはならないケースがあります。作業ステップ内に一時的なプレートがあります。ここでウェルとはサンプルを指し、サンプルは別のプレート上のウェルを指します。このため、誰かが新しい機能を実装しようとすると、奇妙な動作が発生することがあります。浸透するのに時間がかかります。

また、遅延読み込みの否定的な側面に関する上記の記事も参考になりました。


-1

理想的な世界では、DDDはエンティティがデータレイヤーへの参照を持つべきではないことを提案します。しかし、私たちは理想的な世界に住んでいません。ドメインは、依存関係がない可能性のあるビジネスロジックについて、他のドメインオブジェクトを参照する必要がある場合があります。エンティティが値をフェッチするために、読み取り専用の目的でリポジトリレイヤーを参照することは論理的です。


いいえ、これはエンティティへの不必要な結合をもたらし、SRPと懸念の分離に違反し、永続性からエンティティを逆シリアル化することを困難にします(逆シリアル化プロセスでは、エンティティが必要とするサービス/リポジトリも挿入する必要があるため)。
ssmith 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.