DDD Injecting Services on Call di metodi entità


11

Breve formato della domanda

È nelle migliori pratiche di DDD e OOP iniettare servizi nelle chiamate al metodo dell'entità?

Esempio di formato lungo

Supponiamo di avere il classico caso Order-LineItems in DDD, in cui abbiamo un'entità dominio denominata un ordine, che funge anche da radice aggregata e che l'entità è composta non solo dai suoi oggetti valore, ma anche da una raccolta di elementi pubblicitari Entità.

Supponiamo di voler sintassi fluida nella nostra applicazione, in modo da poter fare qualcosa del genere (notando la sintassi nella riga 2, dove chiamiamo il getLineItemsmetodo):

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

Non vogliamo iniettare alcun tipo di LineItemRepository in OrderEntity, poiché si tratta di una violazione di diversi principi a cui riesco a pensare. Ma la fluidità della sintassi è qualcosa che vogliamo davvero, perché è facile da leggere e mantenere, oltre che da testare.

Considera il codice seguente, notando il metodo getLineItemsin 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;
    }
}

È questo il modo accettato di implementare la sintassi fluida nelle Entità senza violare i principi fondamentali di DDD e OOP? A me sembra bene, poiché stiamo solo esponendo il livello di servizio, non il livello di infrastruttura (che è nidificato all'interno del servizio)

Risposte:


9

Va del tutto bene passare un servizio di dominio in una chiamata di entità. Diciamo, dobbiamo calcolare una somma della fattura con qualche algoritmo complicato che può dipendere, per esempio, da un tipo di cliente. Ecco come potrebbe apparire:

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;
    }
}

Un altro approccio è però quello di separare una logica aziendale che si trova nel servizio di dominio tramite eventi di dominio . Tenere presente che questo approccio implica solo servizi applicativi diversi, ma lo stesso ambito di transazione del database.

Il terzo approccio è quello a cui sono favorevole: se mi trovo a utilizzare un servizio di dominio, ciò probabilmente significa che mi sono perso alcuni concetti di dominio, dal momento che modello i miei concetti principalmente con sostantivi , non con i verbi. Quindi, idealmente, non ho affatto bisogno di un servizio di dominio e buona parte di tutta la mia logica aziendale risiede nei decoratori .


6

Sono scioccato nel leggere alcune delle risposte qui.

È perfettamente valido passare i servizi di dominio ai metodi di entità in DDD per delegare alcuni calcoli aziendali. Ad esempio, immagina che la tua radice aggregata (un'entità) debba accedere a una risorsa esterna tramite http al fine di eseguire alcune logiche aziendali e generare un evento. Se non si inietta il servizio attraverso il metodo commerciale dell'entità, in quale altro modo lo si farebbe? Avresti un'istanza di un client http all'interno della tua entità? Sembra un'idea terribile.

Ciò che non è corretto è iniettare servizi negli aggregati attraverso il suo costruttore. Ma attraverso un metodo aziendale è ok e perfettamente normale.


1
Perché il caso che hai indicato non sarebbe la responsabilità di un servizio di dominio?
e_i_pi,

1
è un servizio di dominio, ma viene iniettato nel metodo aziendale. Il livello dell'applicazione è solo un orchestratore,
diegosasw il

Non ho esperienza con DDD ma il servizio dominio non dovrebbe essere chiamato dal servizio applicazione e dopo la convalida del servizio dominio continuare a chiamare i metodi Entity tramite quel servizio applicazione? Sto affrontando lo stesso problema nel mio progetto, perché il servizio di dominio esegue la chiamata al database tramite repository ... Non so se va bene.
Muflix,

Il servizio di dominio dovrebbe orchestrare, se lo chiami dall'applicazione in seguito significa che in qualche modo elabori la risposta e quindi fai qualcosa con esso. Forse suona come una logica aziendale. In tal caso, appartiene al livello Dominio e l'applicazione successivamente risolve semplicemente la dipendenza e la inietta nell'aggregato. Il servizio di dominio avrebbe potuto iniettare un repository il cui implementazione colpire il database dovrebbe appartenere al livello dell'infrastruttura (solo l'implementazione, non l'interfaccia / contratto). Se descrive il tuo linguaggio onnipresente, appartiene al dominio.
Diegosasw,

5

È nelle migliori pratiche di DDD e OOP iniettare servizi nelle chiamate al metodo dell'entità?

No, non dovresti iniettare nulla all'interno del tuo livello di dominio (questo include entità, oggetti valore, fabbriche e servizi di dominio). Questo livello deve essere indipendente da qualsiasi framework, librerie o tecnologie di terze parti e non deve effettuare chiamate IO.

$order->getLineItems($orderService)

Ciò è errato poiché l'aggregato non dovrebbe avere bisogno di nient'altro che di se stesso per restituire gli articoli dell'ordine. L' intero aggregato deve essere già caricato prima della sua chiamata al metodo. Se ritieni che questo dovrebbe essere pigro, allora ci sono due possibilità:

  1. I confini dei tuoi aggregati sono sbagliati, sono troppo grandi.

  2. In questo caso d'uso si utilizza Aggregate solo per la lettura. La soluzione migliore è quella di dividere il modello di scrittura dal modello di lettura (ovvero usare CQRS ). In questa architettura più pulita non è consentito eseguire query su Aggregate ma su un modello di lettura.


Se ho bisogno di una chiamata al database per la convalida, devo chiamarlo nel servizio dell'applicazione e passare un risultato al servizio di dominio o direttamente nella radice aggregata anziché iniettare il repository nel servizio di dominio?
Muflix,

1
@Muflix sì, è vero
Constantin Galbenu,

3

L'idea chiave nei modelli tattici DDD: l'applicazione accede a tutti i dati dell'applicazione agendo su una radice aggregata. Ciò implica che le uniche entità accessibili al di fuori del modello di dominio sono le radici aggregate.

La radice aggregata Ordine non produrrebbe mai un riferimento alla sua raccolta lineitem che ti consentirebbe di modificare la raccolta, né genererebbe una raccolta di riferimenti a qualsiasi elemento pubblicitario che ti consentirebbe di modificarlo. Se si desidera modificare l'aggregato dell'Ordine, si applica il principio di hollywood: "Dillo, non chiedere".

Restituire valori dall'interno dell'aggregato va bene, perché i valori sono intrinsecamente immutabili; non puoi cambiare i miei dati cambiando la tua copia.

L'uso di un servizio di dominio come argomento, per aiutare l'aggregato a fornire i valori corretti, è una cosa perfettamente ragionevole da fare.

Normalmente non useresti un servizio di dominio per fornire l'accesso ai dati che si trovano all'interno dell'aggregato, perché l'aggregato dovrebbe già avervi accesso.

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

Quindi l'ortografia è strana, se stiamo provando ad accedere alla raccolta di valori dell'elemento pubblicitario di questo ordine. L'ortografia più naturale sarebbe

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

Naturalmente, ciò presuppone che gli elementi pubblicitari siano già stati caricati.

Il modello normale è che il carico dell'aggregato includerà tutto lo stato richiesto per il caso d'uso specifico. In altre parole, si può avere diversi diversi modi di caricamento del medesimo aggregato; i metodi del tuo repository sono adatti allo scopo .

Questo approccio non è qualcosa che troverai nell'Evans originale, dove ha assunto che un aggregato avrebbe un singolo modello di dati associato ad esso. Cade più naturalmente dal CQRS.


Grazie per questo. Ora ho letto circa la metà del "libro rosso" e ho avuto il mio primo assaggio dell'applicazione corretta del Principio di Hollywood a livello di infrastruttura. Rileggendo tutte queste risposte, fanno tutti dei buoni punti, ma penso che il tuo abbia alcuni punti molto importanti riguardo la portata lineItems()e il precarico al primo recupero della radice aggregata.
e_i_pi,

3

In generale, gli oggetti valore appartenenti all'aggregato non hanno un repository da soli. È responsabilità della radice aggregata popolarli. Nel tuo caso, è responsabilità del tuo OrderDepository popolare sia l'entità Order sia gli oggetti dei valori OrderLine.

Per quanto riguarda, l'implementazione dell'infrastruttura del OrderRepository, nel caso in cui ORM, sia una relazione uno-a-molti, e puoi scegliere di caricare OrderLine con impazienza o in modo pigro.

Non sono sicuro di cosa significhino esattamente i tuoi servizi. È abbastanza vicino a "Servizio applicazioni". In questo caso, in genere non è una buona idea iniettare i servizi nell'oggetto Aggregate root / Entity / Value. Il servizio applicativo dovrebbe essere il client di radice aggregata / Entità / oggetto valore e servizio dominio. Un'altra cosa che riguarda i tuoi servizi è che non è nemmeno una buona idea esporre oggetti valore in Application Service. Dovrebbero essere accessibili da radice aggregata.


2

La risposta è: sicuramente NO, evitare di passare i servizi nei metodi di entità.

La soluzione è semplice: lascia che il repository Order restituisca l'ordine con tutti i suoi LineItems. Nel tuo caso l'aggregato è Order + LineItems, quindi se il repository non restituisce un aggregato completo, non sta facendo il suo lavoro.

Il principio più ampio è: mantenere i bit funzionali (ad es. Logica di dominio) separati dai bit non funzionali (ad es. Persistenza).

Ancora una cosa: se puoi, cerca di evitare di farlo:

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

Fallo invece

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

Nella progettazione orientata agli oggetti, cerchiamo di evitare di pescare nei dati di un oggetto. Preferiamo chiedere all'oggetto di fare ciò che vogliamo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.