web-dev-qa-db-ja.com

エンティティメソッドコールでのDDDインジェクションサービス

質問の短い形式

エンティティメソッドの呼び出しにサービスを注入することは、DDDおよびOOP=のベストプラクティスの範囲内ですか?

長形式の例

DDDに従来のOrder-LineItemsのケースがあるとします。ここでは、集約ルートとしても機能するOrderというドメインエンティティがあり、そのエンティティは値オブジェクトだけでなく、ラインアイテムのコレクションからも構成されています。エンティティ。

アプリケーションで流暢な構文を使用して、次のようなことができると仮定します(getLineItemsメソッドを呼び出す2行目の構文に注目してください):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

OrderEntityにLineItemRepositoryを挿入したくないのは、それが考えられるいくつかの原則に違反しているためです。しかし、構文の流暢さは私たちが本当に望んでいることです。なぜなら、テストと同様に、読みやすく、保守しやすいからです。

getLineItemsのメソッドOrderEntityに注目して、次のコードを検討してください。

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

それは、DDDとOOPのコア原則に違反することなく、エンティティに流暢な構文を実装する方法として受け入れられていますか?インフラストラクチャレイヤー(サービス内にネストされている)ではなく、サービスレイヤーのみを公開しているので、私にはそれは問題ないようです。

11
e_i_pi

エンティティ呼び出しでドメインサービスを渡すのは まったく問題ありません です。たとえば、顧客のタイプなどに依存する可能性があるいくつかの複雑なアルゴリズムを使用して、請求書の合計を計算する必要があるとします。これは次のようになります。

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

ただし、もう1つのアプローチは、ドメインサービスにあるビジネスロジックを domain events で分離することです。このアプローチは、異なるアプリケーションサービスを意味しますが、データベーストランザクションのスコープは同じであることを覚えておいてください。

3番目のアプローチは、私が支持するものです。ドメインサービスを使用していることに気付いた場合、動詞ではなく概念をモデル化しているため、おそらくドメインの概念を見逃していることになります 主に名詞を使用 。したがって、理想的には、私は ドメインサービスは必要ありません で、すべてのビジネスロジックの大部分は decorators にあります。

9
Zapadlo

ここでいくつかの回答を読んでショックを受けました。

一部のビジネス計算を委任するためにDDDのエンティティメソッドにドメインサービスを渡すことは完全に有効です。例として、ビジネスロジックを実行してイベントを発生させるために、集約ルート(エンティティ)がhttpを介して外部リソースにアクセスする必要があるとします。エンティティのビジネスメソッドを通じてサービスを注入しない場合、他にどのように実行しますか?エンティティ内でhttpクライアントをインスタンス化しますか?それはひどい考えのように聞こえます。

間違っているのは、コンストラクタを通じてサービスを集約して注入することです。しかし、ビジネス手法を通じて、それは大丈夫で完全に正常です。

6
diegosasw

エンティティメソッドの呼び出しにサービスを挿入することは、DDDおよびOOP=のベストプラクティスの範囲内ですか?

いいえ、ドメインレイヤー内には何も挿入しないでください(エンティティ、値オブジェクト、ファクトリー、ドメインサービスを含みます)。このレイヤーは、フレームワーク、サードパーティのライブラリ、テクノロジーにとらわれず、IO呼び出しを行わないでください。

$order->getLineItems($orderService)

Aggregateは注文アイテムを返すためにそれ自体を必要としないため、これは誤りです。 entireAggregateは、メソッド呼び出しの前にすでにロードされている必要があります。これを遅延ロードする必要があると思われる場合は、2つの可能性があります。

  1. 集計の境界が間違っています。大きすぎます。

  2. このユースケースでは、Aggregateを読み取り専用に使用します。最善の解決策は、書き込みモデルを読み取りモデルから分割することです(つまり、 [〜#〜] cqrs [〜#〜] を使用します)。このcleanerアーキテクチャでは、Aggregateではなく読み取りモデルをクエリすることはできません。

5

DDDの戦術パターンにおける重要なアイデア:アプリケーションは、集計ルートに作用することにより、アプリケーション内のすべてのデータにアクセスします。これは、ドメインモデルの外部からアクセスできる唯一のエンティティが集約ルートであることを意味します。

Order集計ルートは、コレクションを変更できるラインアイテムコレクションへの参照を生成することはなく、コレクションを変更できるラインアイテムへの参照のコレクションも生成しません。 Orderの集計を変更する場合は、ハリウッドの原則「Tell、do n't ask」が適用されます。

値は本質的に不変なので、集約内からvaluesを返すことは問題ありません。データのコピーを変更して私のデータを変更することはできません。

集計として正しい値を提供するのを助けるためにドメインサービスを引数として使用することは、完全に合理的なことです。

アグリゲートは既にアクセスしているはずなので、通常、ドメインサービスを使用してアグリゲート内のデータへのアクセスを提供することはありません。

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

したがって、このオーダーのラインアイテム値のコレクションにアクセスしようとすると、そのスペルは奇妙です。より自然なスペルは

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

もちろん、これは広告申込情報が既に読み込まれていることを前提としています。

通常のパターンでは、アグリゲートのロードには、特定のユースケースに必要なすべての状態が含まれます。言い換えると、同じアグリゲートをロードする方法がいくつか異なる場合があります。リポジトリメソッドは 目的に適合 です。

このアプローチは、元のエヴァンスで見られるようなものではありません。彼は、集約には単一のデータモデルが関連付けられると想定していました。それはより自然にCQRSから外れます。

3
VoiceOfUnreason

一般的に、集約に属する値オブジェクトには、それ自体にはリポジトリがありません。それらを設定するのは集約ルートの責任です。あなたの場合、OrderエンティティとOrderLine値オブジェクトの両方を設定するのは、OrderRepositoryの責任です。

OrderRepositoryのインフラストラクチャの実装については、ORMの場合、それは1対多の関係であり、OrderLineを積極的にロードするか遅延ロードするかを選択できます。

サービスの正確な意味がわかりません。 「アプリケーションサービス」にかなり近いです。これが事実である場合、通常、サービスを集約ルート/エンティティ/値オブジェクトに注入することはお勧めできません。アプリケーションサービスは、集約ルート/エンティティ/値オブジェクトおよびドメインサービスのクライアントである必要があります。サービスに関するもう1つのことは、Application Serviceで値オブジェクトを公開することも良い考えではありません。それらは集約ルートによってアクセスされるべきです。

3
ivenxu

正解は次のとおりです。エンティティメソッドでサービスを渡すことは避けてください。

解決策は簡単です。OrderリポジトリにすべてのLineItemを含むOrderを返すだけです。あなたの場合、集計はOrder + LineItemsなので、リポジトリが完全な集計を返さない場合、その仕事はしていません。

より広い原則は、機能ビット(ドメインロジックなど)を非機能ビット(永続性など)から分離することです。

もう1つ:可能であれば、これを回避するようにしてください。

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

代わりにこれを行う

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

オブジェクト指向の設計では、オブジェクトデータ内でのフィッシングを回避しようとします。私たちはオブジェクトに私たちがやりたいことをするように依頼することを好みます。

2
xpmatteo