DDD Iniezione di servizi sulle chiamate di metodi di entità

6

Breve formato della domanda

Nelle migliori pratiche di DDD e OOP è possibile iniettare servizi sulle chiamate al metodo di entità?

Esempio di formato lungo

Diciamo che abbiamo il classico caso Order-LineItems in DDD, dove abbiamo un'entità di dominio chiamata Ordine, che agisce anche come radice di aggregazione, e che Entità è composta non solo da oggetti valore, ma anche da una collezione delle entità degli elementi pubblicitari.

Supponiamo di volere una sintassi fluente nella nostra applicazione, in modo che possiamo fare qualcosa di simile (osservando la sintassi nella riga 2, dove chiamiamo il metodo getLineItems ):

$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 posso pensare. Ma la fluidità della sintassi è qualcosa che vogliamo davvero, perché è facile da leggere e gestire, oltre che da test.

Considera il seguente codice, prendendo nota del metodo getLineItems in 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 fluente nelle Entità senza violare i principi fondamentali di DDD e OOP? Per me sembra soddisfacente, poiché stiamo solo esponendo il livello di servizio, non il livello dell'infrastruttura (che è annidato all'interno del servizio)

    
posta e_i_pi 25.09.2017 - 02:22
fonte

6 risposte

5

È assolutamente soddisfacente per passare un servizio di dominio in una chiamata di entità. Diciamo, dobbiamo calcolare una somma di fattura con qualche algoritmo complicato che può dipendere, diciamo, 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 è quello di separare una logica di business che si trova nel servizio di dominio tramite eventi di dominio . Tieni presente che questo approccio implica solo diversi servizi applicativi, 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ò significa probabilmente che mi sono perso qualche concetto di dominio, dal momento che modellino i miei concetti principalmente con nomi , non i verbi. Quindi, idealmente, ho non ho bisogno di un dominio servizio del tutto e una buona parte di tutta la mia logica di business risiede in decoratori .

    
risposta data 02.11.2017 - 09:25
fonte
5

Is it within best practices of DDD and OOP to inject services on entity method calls?

No, non dovresti iniettare nulla all'interno del tuo livello di dominio (questo include entità, oggetti valore, fabbriche e servizi di dominio). Questo strato dovrebbe essere agnostico di qualsiasi framework, librerie di terze parti o tecnologia e non dovrebbe effettuare alcuna chiamata IO.

$order->getLineItems($orderService)

Questo è sbagliato in quanto Aggregato non dovrebbe aver bisogno di altro se non di restituire gli articoli dell'ordine. L'aggregato intero dovrebbe essere già caricato prima della chiamata al metodo. Se ritieni che questo dovrebbe essere pigro, allora ci sono due possibilità:

  1. I tuoi limiti di aggregati sono errati, sono troppo grandi.

  2. In questo caso, si utilizza Aggregate solo per la lettura. La soluzione migliore è dividere il modello di scrittura dal modello di lettura (ad esempio, utilizzare CQRS ). In questa architettura più pulita non ti è permesso interrogare l'aggregato ma un modello di lettura.

risposta data 25.09.2017 - 07:10
fonte
4

Sono scioccato di leggere alcune delle risposte qui.

È perfettamente valido passare i servizi di dominio in metodi di entità in DDD per delegare alcuni calcoli aziendali. Ad esempio, immagina che la tua radice aggregata (un'entità) abbia bisogno di accedere a una risorsa esterna tramite http per fare una certa logica aziendale e generare un evento. Se non si inserisce il servizio tramite il metodo di business dell'entità, in quale altro modo si farebbe? Vuoi istanziare un client http all'interno della tua entità? Sembra un'idea terribile.

Che cos'è errato è iniettare servizi in aggregati attraverso il suo costruttore. Ma attraverso un metodo di business è ok e perfettamente normale.

    
risposta data 23.08.2018 - 12:14
fonte
3

In generale gli oggetti valore appartenenti all'aggregato non hanno repository autonomamente. È responsabilità della radice aggregata compilarli. Nel tuo caso, è responsabilità del tuo OrderRepository compilare sia l'entità Order che gli oggetti OrderLine.

Per quanto riguarda, l'implementazione dell'infrastruttura del OrderSepository, nel caso dell'ORM, è una relazione uno-a-molti, ed è possibile scegliere entrambi  o pigro caricare OrderLine.

Non sono sicuro di cosa significano esattamente i tuoi servizi. È abbastanza vicino a "Application Service". Se questo è il caso, non è generalmente una buona idea iniettare i servizi su Oggetto radice / Entità / Valore aggregato. Il servizio dell'applicazione deve essere il client di radice radice / entità / oggetto valore e servizio dominio. Un altro aspetto dei tuoi servizi è che non è una buona idea neanche l'esposizione di oggetti valore nel servizio applicativo. Dovrebbero essere accessibili da radice aggregata.

    
risposta data 25.09.2017 - 05:32
fonte
2

La risposta è: decisamente NO, evita di passare servizi nei metodi di entità.

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

Il principio più ampio è: mantieni i bit funzionali (ad es. logica di dominio) separati dai bit non funzionali (ad esempio, persistenza).

Un'altra cosa: se puoi, cerca di evitare di fare questo:

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

Fai questo 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.

    
risposta data 25.09.2017 - 07:37
fonte
1

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

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

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

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

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

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

Quindi l'ortografia è strana, se stiamo tentando di accedere alla raccolta di questo ordine di valori di elementi pubblicitari. L'ortografia più naturale sarebbe

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

Ovviamente, questo presuppone che gli elementi pubblicitari siano già stati caricati.

Lo schema abituale è che il carico dell'aggregato includerà tutti gli stati richiesti per il caso d'uso specifico. In altre parole, potresti avere diversi diversi modi per caricare lo stesso aggregato; i tuoi metodi di deposito sono adatti allo scopo .

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

    
risposta data 25.09.2017 - 05:28
fonte