ドメイン駆動設計では、 lots of agreement があるようです。エンティティはリポジトリに直接アクセスしないでください。
これはエリック・エヴァンスによるものですか Domain Driven Design 本、または他の場所からのものですか?
その背後にある理由についての良い説明はどこにありますか?
編集:明確にするために:私は古典的なOOビジネスロジックから別のレイヤーにデータアクセスを分離する慣習について話していません-DDDでの特定の配置について話している、エンティティは、データアクセス層とまったく通信しないことになっています(つまり、リポジトリオブジェクトへの参照を保持することになっていません)。
更新:BacceSRの答えが最も近いように見えたので、BacceSRに賞金を贈りましたが、これについてはまだかなり暗いです。そのような重要な原則であれば、オンラインでそれに関する良い記事がどこかにあるはずです。
更新:2013年3月、この質問に対する賛成票は、これに多くの関心があることを意味します。多くの答えがあったとしても、人々がこれについてアイデアを持っている場合、さらに多くの余地があると思います。
ここには少し混乱があります。リポジトリは集約ルートにアクセスします。集約ルートはエンティティです。この理由は、関心の分離と適切な階層化です。これは小さなプロジェクトでは意味がありませんが、大規模なチームの場合は、「製品リポジトリから製品にアクセスします。製品はProductCatalogオブジェクトを含むエンティティのコレクションの集約ルートです。 ProductCatalogを更新する場合は、ProductRepositoryを確認する必要があります。」
このようにして、ビジネスロジックと更新箇所を非常に明確に分離できます。自分で離れて、これらすべての複雑なことを行うプログラム全体を製品カタログに書き込む子供がいないので、アップストリームプロジェクトに統合することになると、あなたはそこに座ってそれを理解しますすべてを捨てなければならない。また、人々がチームに参加し、新しい機能を追加するとき、彼らはどこに行き、プログラムを構築するかを知っています。
ちょっと待って!リポジトリは、リポジトリパターンのように、永続層も指します。より良い世界では、Eric Evansのリポジトリとリポジトリパターンは、かなり重なる傾向があるため、別々の名前になります。リポジトリパターンを取得するには、サービスバスやイベントモデルシステムなど、データにアクセスする他の方法とは対照的です。通常、このレベルに到達すると、Eric Evansのリポジトリ定義が脇に行き、境界のあるコンテキストについて話し始めます。各境界コンテキストは、本質的に独自のアプリケーションです。製品カタログに物を入れるための洗練された承認システムがあるかもしれません。元のデザインでは製品が中心でしたが、この境界のあるコンテキストでは製品カタログです。サービスバスを介して製品情報にアクセスし、製品を更新することもできますが、境界付けられたコンテキスト外の製品カタログは、まったく異なるものを意味する可能性があることを認識する必要があります。
元の質問に戻ります。エンティティ内からリポジトリにアクセスしている場合、そのエンティティは実際にはビジネスエンティティではなく、おそらくサービスレイヤーに存在するものであることを意味します。これは、エンティティがビジネスオブジェクトであり、DSL(ドメイン固有言語)に可能な限り似ていることに関心を持つ必要があるためです。このレイヤーにはビジネス情報のみがあります。パフォーマンスの問題をトラブルシューティングする場合は、ビジネス情報だけがここにあるはずなので、他の場所を確認する必要があります。突然、ここでアプリケーションの問題が発生した場合、アプリケーションの拡張と保守が非常に難しくなります。これは、DDDの中心である保守可能なソフトウェアの作成です。
コメント1への応答:いい質問です。そのため、ドメイン層ではall検証は行われません。シャープには、あなたが望むことをする属性「DomainSignature」があります。永続性を認識しますが、属性であるとドメイン層がきれいになります。この例では、同じ名前のエンティティが重複しないようにします。
しかし、より複雑な検証ルールについて話しましょう。あなたがAmazon.comだとしましょう。有効期限が切れたクレジットカードで何か注文したことがありますか?私はカードを更新しておらず、何かを購入していません。それは注文を受け入れ、UIはすべてが桃色であることを私に知らせます。約15分後、注文に問題があり、クレジットカードが無効であるというメールが届きます。ここで起こっているのは、理想的には、ドメイン層で正規表現の検証があることです。これは正しいクレジットカード番号ですか?はいの場合、注文を保持します。ただし、アプリケーションタスクレイヤーには追加の検証があり、外部サービスにクエリを実行して、クレジットカードで支払いが可能かどうかを確認します。そうでない場合は、実際に何も出荷せず、注文を一時停止して顧客を待ちます。これはすべてサービスレイヤーで実行する必要があります。
リポジトリにアクセスするcanサービス層で検証オブジェクトを作成することを恐れないでください。それをドメイン層から遠ざけてください。
最初は、一部のエンティティにリポジトリへのアクセスを許可するという説得力がありました(つまり、ORMなしの遅延読み込み)。後で私はすべきではないという結論に至り、別の方法を見つけることができました:
赤い本の実装ドメイン駆動設計のVernon Vaughnは、私が知っている2つの場所でこの問題に言及しています(注:この本はEvansによって完全に承認されています前書きで読むことができるように)。サービスに関する第7章では、ドメインサービスと仕様を使用して、リポジトリと別のアグリゲートを使用してユーザーが認証されているかどうかを判断するアグリゲートの必要性を回避しています。彼は言っているとして引用されています:
経験則として、可能な限り、Aggregates内からのリポジトリ(12)の使用を避けるようにしてください。
ヴァーノン、ヴォーン(2013-02-06)。ドメイン駆動設計の実装(Kindle Location 6089)。ピアソン教育。キンドル版。
そして、Aggregatesの第10章では、 「Model Navigation」というタイトルのセクション で(他の集約ルートを参照するためにグローバルな一意のIDの使用を推奨した直後)と彼は言います:
IDによる参照は、モデル内のナビゲーションを完全に妨げるものではありません。いくつかは、検索のためにAggregate内からRepository(12)を使用します。この手法は非接続ドメインモデルと呼ばれ、実際には遅延読み込みの形式です。ただし、別の推奨されるアプローチがあります。リポジトリまたはドメインサービス(7)を使用して、集約動作を呼び出す前に依存オブジェクトを検索します。クライアントアプリケーションサービスはこれを制御し、集約にディスパッチします。
彼はコードのこの例を示します:
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));
}
...
}
また、Aggregateコマンドメソッドでdouble-dispatchとともにドメインサービスを使用する方法の別のソリューションについても言及しています。 (私は彼の本を読むのがどれほど有益かをお勧めできません。あなたがインターネットで果てしなくうろついていることに疲れた後、ふさわしいお金に分岐して本を読んでください。)
その後、いくつかの discussion といつも優雅な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);
}
その非常に良い質問です。これについての議論を楽しみにしています。しかし、それは DDDの数冊の本 とJimmy nilssonsとEric Evansで言及されていると思います。 Reposistoryパターンの使用方法の例からもわかると思います。
しかし、議論することができます。非常に妥当な考えは、エンティティが別のエンティティを永続化する方法を知っている必要がある理由だと思いますか? DDDで重要なのは、各エンティティが独自の「知識領域」を管理する責任があり、他のエンティティの読み取りまたは書き込み方法について何も知らないことです。エンティティBを読み取るために、おそらくエンティティAにリポジトリインターフェイスを追加するだけです。しかし、リスクは、Bを永続化する方法に関する知識を公開することです。
ご覧のとおり、エンティティAはエンティティBのライフサイクルにより深く関与することができ、それによりモデルがより複雑になります。
(例はありませんが)単体テストはより複雑になると思います。
しかし、エンティティを介してリポジトリを使用したいというシナリオが常にあると確信しています。有効な判断を下すには、各シナリオを確認する必要があります。長所と短所。しかし、私の意見では、リポジトリエンティティソリューションは多くの短所から始まります。それは、短所をバランスさせるプロの非常に特別なシナリオでなければなりません。
この本から、モデル駆動設計の章の最初の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は、パフォーマンス上の理由から、null ProductInfoプロパティを持つOrderLineオブジェクトを返します。開発者は、インターフェイスの背後にあるコードに関する詳細な知識を持っている必要があります。
実際のシステムでこの方法を試しました。パフォーマンスの問題を修正するために、常にロードされるもののスコープを変更することになります。インターフェースの背後を覗き込んで、データアクセスコードを調べ、ロードされているものとロードされていないものを確認します。
現在、開発者は懸念を分離することで、可能な限りコードの1つの側面に集中することができます。インターフェース技術は、このデータがロードされたHOWを削除しますが、HOW MUCHデータがロードされたとき、ロードされたとき、およびロードされる場所はロードされません。
結論:かなり低い分離!
データはオンデマンドでロードされます。データをロードする呼び出しはオブジェクトグラフ自体の中に隠されており、プロパティにアクセスすると、結果を返す前にsqlクエリが実行されます。
foreach (var line in order.OrderLines)
{
total += line.Price;
}
長所:データアクセスの「WHEN、WHERE、およびHOW」は、ドメインロジックに焦点を当てた開発者から隠されています。集計には、データの読み込みを処理するコードはありません。ロードされるデータの量は、コードが必要とする正確な量にすることができます。
短所:パフォーマンスの問題が発生した場合、一般的な「すべてのサイズに対応する」ソリューションがある場合、修正するのは困難です。遅延読み込みは全体的なパフォーマンスの低下を引き起こす可能性があり、遅延読み込みの実装には注意が必要です。
各ユースケースは、集約クラスによって実装される Role Interface を介して明示的に作成され、ユースケースごとにデータロード戦略を処理できます。
フェッチ戦略は次のようになります。
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を使用して集約を構築し、集約がその作業を実行します。
長所:ユースケースごとにカスタムコードを使用できるため、最適なパフォーマンスが得られます。 Interface Segregation Principle とインラインです。複雑なコード要件はありません。集計ユニットテストは、ロード戦略を模倣する必要はありません。一般的なロード戦略は、ほとんどの場合に使用でき(「すべてロード」戦略など)、必要に応じて特別なロード戦略を実装できます。
短所:開発者は、ドメインコードを変更した後でもフェッチ戦略を調整/レビューする必要があります。
フェッチ戦略アプローチでは、ビジネスルールの変更に合わせてカスタムフェッチコードを変更していることに気付くかもしれません。懸念を完全に分離するものではありませんが、最終的には保守性が向上し、最初のオプションよりも優れています。フェッチ戦略は、HOW、WHEN、およびWHEREデータがロードされることをカプセル化します。ワンサイズがすべての遅延読み込みアプローチに適合するように、柔軟性を失うことなく、懸念をより適切に分離します。
このブログでは、エンティティ内にリポジトリをカプセル化することに対して非常に良い議論があることがわかりました。
http://thinkbeforecoding.com/post/2009/03/04/How-not-to-inject-services-in-entities
なんて素晴らしい質問でしょう。私は同じ発見の道を歩んでおり、インターネット全体のほとんどの答えは、解決策をもたらすのと同じくらい多くの問題をもたらすようです。
だから(今から1年後、私が反対する何かを書くという危険を冒して)ここに私のこれまでの発見があります。
まず、リッチドメインモデルが好きです。これは、高いdiscoverability(集計でできること)とreadability(表現力豊かなメソッド呼び出し)。
_// 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()
には、ロガーで単純な依存関係が追加されました。これは明らかにpartを実行します作品の。
後者の場合、クライアントコードで物事を簡単にするために、代わりにIInvoiceService
を介してログを記録します。結局、請求書の記録は請求書にかなり固有のもののようです。このような単一のIInvoiceService
は、さまざまな操作のためのあらゆる種類のミニサービスの必要性を回避するのに役立ちます。欠点は、そのサービスが正確に何をするかわかりにくくなることですdo。ダブルディスパッチのようにlookで始まることもありますが、ほとんどの作業はSetStatus()
自体で行われます。
意図を明らかにすることを期待して、パラメータに「ロガー」という名前を付けることができます。ただし、少し弱いようです。
代わりに、IInvoiceLogger
(コードサンプルで既に行っているように)を要求し、IInvoiceService
にそのインターフェイスを実装することを選択します。クライアントのコードは、そのような非常に特定の、請求書固有の「ミニサービス」を要求するすべてのIInvoiceService
メソッドに対して、単一のInvoice
を使用するだけで、メソッドのシグネチャにより、彼らは求めています。
私は明示的にリポジトリに取り組んでいないことに気付きます。ロガーはリポジトリであるか、リポジトリを使用しますが、より明確な例を提供します。 1つまたは2つのメソッドでリポジトリが必要な場合は、同じアプローチを使用できます。
_public class Invoice
{
public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
{ ... }
}
_
実際、これは非常に厄介な遅延負荷の代替手段となります。
更新:歴史的な目的のために以下のテキストを残しましたが、遅延負荷100%を避けてステアリングすることをお勧めします。
真の、プロパティベースの遅延読み込みの場合、Idoは現在、コンストラクター注入を使用しますが、永続性を無視します。
_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
に挿入する関数に自由にアクセスできます。
一方、実際のnewInvoice
を作成するコードは、空のリストを返す関数を渡すだけです。
_new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)
_
(カスタム_ILazy<out T>
_は、IEnumerable
への_いキャストを取り除くことができますが、それは議論を複雑にします。)
_// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())
_
私はあなたの意見、好み、改善を聞いてうれしいです!
私には、これはDDDに固有のものではなく、一般的に良いOOD関連のプラクティスのようです。
私が考えることができる理由は次のとおりです。
単純にVernon Vaughnは解決策を示します。
集約動作を呼び出す前に、リポジトリまたはドメインサービスを使用して依存オブジェクトをルックアップします。クライアントアプリケーションサービスがこれを制御する場合があります。
この個別のレイヤーバズがすべて表示される前にオブジェクト指向プログラミングをコーディングすることを学び、最初のオブジェクト/クラスDID=データベースに直接マップします。
最終的に、別のデータベースサーバーに移行する必要があるため、中間層を追加しました。同じシナリオを何度か見たり聞いたりしました。
データアクセス(別名「リポジトリ」)をビジネスロジックから分離することは、ドメインドリブンデザインブックとは異なり、何度も再発明されたものの1つであり、多くの「ノイズ」になると思います。
現在、多くの開発者が使用しているように、3つのレイヤー(GUI、ロジック、データアクセス)を使用しています。これは優れた手法だからです。
データをRepository
レイヤー(別名Data Access
層)、従うべき単なる規則ではなく、優れたプログラミング手法のように見えるかもしれません。
多くの方法論と同様に、実装しないで開始し、それらを理解したら、最終的にプログラムを更新することができます。
引用:IliadはHomerによって完全に発明されたわけではなく、Carmina BuranaはCarl Orffによって完全に発明されたものではありません。
Carolina Lilientahlを引用するために、「パターンはサイクルを防ぐべきです」 https://www.youtube.com/watch?v=eJjadzMRQAk で、彼女はクラス間の循環依存関係を指します。集約内のリポジトリの場合、唯一の理由としてオブジェクトナビゲーションの利便性から循環依存関係を作成する誘惑があります。上記のprograhammerによるパターンは、Vernon Vaughnが推奨したもので、ルートインスタンスの代わりにIDによって他の集約が参照されます(このパターンの名前はありますか)は、他のソリューションに導く代替案を示唆しています。
クラス間の循環依存関係の例(告白):
(Time0):2つのクラス、SampleおよびWellは、相互に参照します(循環依存関係)。ウェルはサンプルを指し、サンプルは便宜上、ウェルを指します(サンプルをループする場合があり、プレート内のすべてのウェルをループする場合があります)。 Sampleが配置されたWellを参照しないケースは想像できませんでした。
(Time1):1年後、多くのユースケースが実装されます....そして、サンプルが配置されたウェルを参照する必要がない場合があります。作業ステップ内に一時的なプレートがあります。ここで、ウェルはサンプルを指し、サンプルは別のプレート上のウェルを指します。このため、誰かが新しい機能を実装しようとすると、奇妙な動作が発生することがあります。侵入するのに時間がかかります。
私もこれに助けられました 記事 遅延読み込みのマイナス面について上記で述べました。
これはEric Evans Domain Driven Designの本からのものですか、それとも他の場所からのものですか?
古いものです。エリックの本はもう少し話題になりました。
その背後にある理由についての良い説明はどこにありますか?
理由は単純です-漠然と関連する複数のコンテキストに直面すると、人間の心は弱まります。それらは曖昧さ(南/北米のアメリカは南/北米を意味します)につながり、曖昧さは心が「触れる」たびに情報の絶え間ないマッピングにつながり、それは悪い生産性とエラーとして要約されます。
ビジネスロジックはできるだけ明確に反映する必要があります。外部キー、正規化、オブジェクトリレーショナルマッピングは完全に異なるドメインからのものです。これらは技術的なものであり、コンピューターに関連しています。
類推:手書きの書き方を学んでいるなら、ペンがどこで作られたのか、なぜインクが紙に保持されるのか、紙が発明されたのはいつか、他の有名な中国の発明は何なのかを理解するのに苦労すべきではありません。
編集:明確にするために:私は古典的なOOビジネスロジックから別のレイヤーにデータアクセスを分離する慣習について話していません-DDDでの特定の配置について話している、エンティティは、データアクセス層とまったく通信しないことになっています(つまり、リポジトリオブジェクトへの参照を保持することになっていません)。
理由は私が上で述べたのと同じです。ここではさらに一歩進んでいます。エンティティが完全に(少なくともほぼ)できる場合、なぜエンティティは部分的に永続性を無視する必要があるのですか?私たちのモデルが抱える領域に関係のない懸念は少なくなります。