Anemic ドメイン モデルを使用する必要がある場合、ビジネス ロジックと計算フィールドをどこに配置しますか?
-
20-09-2019 - |
質問
現在の O/RM ツールではリッチなドメイン モデルを実際には使用できないため、あらゆる場所で貧血 (DTO) エンティティを利用することを余儀なくされています。これはうまく機能しましたが、基本的なオブジェクトベースのビジネス ロジックと計算フィールドをどこに配置するかについては引き続き苦労しています。
現在のレイヤー:
- プレゼンテーション
- サービス
- リポジトリ
- データ/エンティティ
リポジトリ層には基本的なフェッチ/検証/保存ロジックのほとんどがありますが、サービス層はより複雑な検証と保存の多くを実行します (保存操作ではログ記録や権限のチェックなども行われるため)。問題は、次のようなコードをどこに配置するかです。
Decimal CalculateTotal(LineItemEntity li)
{
return li.Quantity * li.Price;
}
または
Decimal CalculateOrderTotal(OrderEntity order)
{
Decimal orderTotal = 0;
foreach (LineItemEntity li in order.LineItems)
{
orderTotal += CalculateTotal(li);
}
return orderTotal;
}
何かご意見は?
解決
基本に戻りましょう:
サービス
サービスには 3 つの種類があります。 ドメインサービス, アプリケーションサービス, 、 そして インフラストラクチャサービス
- ドメインサービス :ドメインオブジェクト内に自然に適合しないビジネスロジックをカプセル化します。あなたの場合、 全て ビジネスロジックの。
- アプリケーションサービス :外部の消費者がシステムと通信するために使用
- インフラストラクチャサービス :技術的な懸念事項を抽象化するために使用されます (例:MSMQ、電子メールプロバイダーなど)
リポジトリ
ここはデータにアクセスし、 整合性チェック 行く。純粋な DDD では、 集約ルート (オブジェクトを永続化する前に) 一貫性をチェックする責任があります。あなたの場合、あなたの小切手を使用します ドメインサービス 層。
提案された解決策: 既存のサービスを分割する
新しいものを使用してください ドメインサービス レイヤーを使用して、DTO のすべてのロジックと一貫性チェックもカプセル化します ( 仕様, 、 多分?)。
使用 アプリケーションサービス 必要なフェッチメソッドを公開する(FetchOpenOrdersWithLines
)、リクエストを次のユーザーに転送します。 リポジトリ (ジェレミーが提案したように、ジェネリックを使用します)。の使用を検討することもできます クエリ仕様 クエリをラップします。
あなたから リポジトリ, 、 使用 仕様 あなたの中で ドメインサービス レイヤーを使用して、オブジェクトを永続化する前にオブジェクトの一貫性などをチェックします。
エヴァンスの本でサポート情報を見つけることができます。
- 「サービスと分離ドメイン層」 (106ページ)
- 「仕様」 (224ページ)
- 「クエリ仕様」 (229ページ)
他のヒント
答えたくなった ムー, 、詳しく説明したいと思います。要約すれば: ORM の選択によってドメイン モデルの定義方法が決定されることのないようにしてください。
ドメイン モデルの目的は、ドメインをモデル化する豊富なオブジェクト指向 API であることです。真実に従うために ドメイン駆動設計, 、ドメインモデルを定義する必要があります テクノロジーに制約されない.
言い換えれば、 ドメインモデルが最初にある, 、そしてすべてのテクノロジー固有の実装はその後、によって対処されます。 マッパー ドメインモデルと問題のテクノロジーの間のマッピング。これには多くの場合、次の両方の方法が含まれます。データ アクセス層では ORM の選択によって制約が生じる可能性があり、UI 層では UI テクノロジによって追加の要件が課されます。
実装がドメイン モデルから著しくかけ離れている場合は、 腐敗防止層.
あなたの場合、貧血ドメインモデルと呼ばれるものは、実際にはデータアクセス層です。最善の手段は、次のように定義することです。 リポジトリ これは、テクノロジーに依存しない方法でエンティティへのアクセスをモデル化します。
例として、注文エンティティを見てみましょう。テクノロジーの制約を受けずに注文をモデル化すると、次のような結果になる可能性があります。
public class Order
{
// constructors and properties
public decimal CalculateTotal()
{
return (from li in this.LineItems
select li.CalculateTotal()).Sum();
}
}
これは単純な古い CLR オブジェクトであることに注意してください ( ポコ )そのため、テクノロジーに制約されません。ここで問題となるのは、これをデータ ストアにどのように出し入れするかということです。
これは、抽象 IOrderRepository を介して実行する必要があります。
public interface IOrderRepository
{
Order SelectSingle(int id);
void Insert(Order order);
void Update(Order order);
void Delete(int id);
// more, specialized methods can go here if need be
}
選択した ORM を使用して IOrderRepository を実装できるようになりました。ただし、一部の ORM (Microsoft の Entity Framework など) では、特定の基本クラスからデータ クラスを派生する必要があるため、これは POCO としてのドメイン オブジェクトにはまったく適合しません。したがって、マッピングが必要です。
認識すべき重要な点は、強く型付けされたデータ クラスが存在する可能性があるということです。 意味的に ドメイン エンティティに似ています。ただし、これは純粋な実装の詳細なので、混乱しないでください。たとえば、から派生した Order クラス。 エンティティオブジェクト はドメインクラスではありません - これは実装の詳細であるため、IOrderRepository を実装するときは、Order をマップする必要があります。 データクラス 命令に ドーマクラス.
これは面倒な作業かもしれませんが、以下を使用できます。 オートマッパー あなたのためにそれをするために。
SelectSingle メソッドの実装は次のようになります。
public Order SelectSinge(int id)
{
var oe = (from o in this.objectContext.Orders
where o.Id == id
select o).First();
return this.mapper.Map<OrderEntity, Order>(oe);
}
これは、サービス層が何のためにあるのかを正確です。
これはあなたの時間のテストの大半を過ごすことになるでしょうルーチンであり、彼らは自分の層にいるならば、リポジトリ層をからかっするのは簡単でなければなりません。
それは特定のクラスに個別のビジネスロジックのための適切な場所ではありませんので、リポジトリ層は、可能な限りgenericizedする必要があります。
あなたの発言からすると、サービス層とリポジトリ層について厳格に考えすぎているのかもしれません。プレゼンテーション層がリポジトリ層に直接依存することを望んでいないように思えます。これを実現するには、リポジトリからのメソッド (パススルー メソッド) をサービス層に複製しています。
私はそれを疑問に思います。それを緩和して、プレゼンテーション レイヤー内で両方を使用できるようにして、まずは作業を簡素化することができます。そのようにしてリポジトリを非表示にすることで何を達成できるのか、自分自身に問いかけてみてください。すでに永続性を抽象化し、それらを使用して IMPLEMENTATION をクエリしています。これは素晴らしいことであり、そのために設計されています。エンティティが永続化されているという事実を隠すサービス層を作成しようとしているように見えます。理由を聞いてみますか?
注文合計の計算などについてサービス層が本来のホームとなるでしょう。LineTotal(LineItem lineItem) メソッドと OrderTotal(Order order) メソッドを備えた SalesOrderCalculator クラスは問題ありません。適切なファクトリーの作成を検討することもできます。OrderServices.CreateOrderCalculator() を使用して、必要に応じて実装を切り替えます (たとえば、注文割引に対する税金には国固有のルールがあります)。これにより、Order サービスへの単一のエントリ ポイントが形成され、IntelliSense を通じて簡単に検索できるようになります。
これらすべてが実行不可能に思える場合は、抽象化が何を達成しているのか、それらが互いにどのように関連しているのか、そして 単一責任の原則. 。リポジトリはインフラストラクチャの抽象化です (エンティティの保存および取得方法を非表示にします)。サービスはビジネス アクションやルールの実装を抽象化し、バージョン管理や差異のためのより適切な構造を可能にします。通常、それらはあなたが説明したように階層化されていません。サービスに複雑なセキュリティ ルールがある場合は、リポジトリの方が適切なホームとなる可能性があります。典型的な DDD スタイルのモデルでは、リポジトリ、エンティティ、値オブジェクト、およびサービスはすべて、同じレイヤー内で同じモデルの一部として並べて使用されます。したがって、上の層 (通常はプレゼンテーション) は、これらの抽象化によって分離されます。モデル内では、あるサービスの実装が別のサービスの抽象化を使用する場合があります。さらなる改良により、誰がどのエンティティまたは値オブジェクトへの参照を保持するかに関するルールが追加され、より正式なライフサイクル コンテキストが強制されます。これについて詳しく知りたい場合は、次の資料を勉強することをお勧めします。 エリック・エヴァンスの本 または ドメイン駆動設計を迅速に実行.
あなたのORM技術はDTOを扱う場合は、豊かなエンティティオブジェクトを捨てる必要はあり意味するものではありません、よくオブジェクト。あなたはまだ、エンティティオブジェクトを使用してDTOオブジェクトをラップすることができます:
public class MonkeyData
{
public string Name { get; set; }
public List<Food> FavoriteFood { get; set; }
}
public interface IMonkeyRepository
{
Monkey GetMonkey(string name) // fetches DTO, uses entity constructor
void SaveMonkey(Monkey monkey) // uses entity GetData(), stores DTO
}
public class Monkey
{
private readonly MonkeyData monkeyData;
public Monkey(MonkeyData monkeyData)
{
this.monkeyData = monkeyData;
}
public Name { get { return this.monkeyData.Name; } }
public bool IsYummy(Food food)
{
return this.monkeyData.FavoriteFood.Contains(food);
}
public MonkeyData GetData()
{
// CLONE the DTO here to avoid giving write access to the
// entity innards without business rule enforcement
return CloneData(this.monkeyData);
}
}
私はディーノエスポジトの新しい本を見つけたのMicrosoft®.NETのための:設計するアプリケーションEnterpriseは、質問および問題について、これらのタイプのための知識の偉大なリポジトリであることをします。
サービス層ます。
、拡張メソッドを試してみます。私はしかし、あなたの例のように、単純なシナリオのためにこれを行うだろう。より複雑またはそのものはいくつかのエンティティおよび/またはサービスを、層、または何でもすでに示唆したように、ドメインサービスであるべきとの間で調整します。
(あなたの例から)たとえばます:
public static class LineItemEntityExtensions
{
public static decimal CalculateTotal(this LineItemEntity li)
{
return li.Quantity * li.Price;
}
}
public static class OrderEntityExtensions
{
public static decimal CalculateOrderTotal(this OrderEntity order)
{
decimal orderTotal = 0;
foreach (LineItemEntity li in order.LineItems)
orderTotal += li.CalculateTotal();
return orderTotal;
}
}
public class SomewhereElse
{
public void DoSomething(OrderEntity order)
{
decimal total = order.CalculateOrderTotal();
...
}
}
あなたが欲しいこれらの追加の非常に少数がある場合は、、あなただけの「DomainExtensions」クラスでそれらのすべてを置くことができますが、私はそうでない場合は、通常の尊敬でそれらを処理することを提案し、中に一つのクラスでは、エンティティのすべての拡張機能を維持したいです独自のファイルます。
FYI:私はL2Sソリューションを持っていたし、パーシャルと混乱したくなかったとき、私はこれをやっただけです。ソリューションが小さかったので、私はまた、多くの拡張機能を持っていませんでした。私は本格的なドメインサービス層よりよく一緒に行くのアイデアを気に入っています。