Dove dovresti convalidare lo stato di "altri" aggregati?

8

Scenario:

Un cliente effettua un ordine, quindi, dopo aver ricevuto il prodotto, fornisce feedback sul processo dell'ordine.

Assumi le seguenti radici aggregate:

  • Clienti
  • Ordine
  • Feedback

Ecco le regole aziendali:

  1. Un cliente può fornire feedback solo sul proprio ordine, non su quello di qualcun altro.
  2. Un cliente può fornire un feedback solo se l'ordine è stato pagato.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Ora, supponiamo che l'azienda desideri una nuova regola:

  1. Un cliente può fornire feedback solo se Supplier dell'ordine le merci sono ancora in funzione.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    Supplier $supplier,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            // NEW RULE HERE
            if (!$supplier->isOperating()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Ho inserito l'implementazione delle prime due regole all'interno di Feedback aggregare se stesso. Mi sento a mio agio nel fare questo, soprattutto considerando che il Feedback aggregato fa riferimento a tutti gli altri aggregati per identità. Per esempio., le proprietà del componente Feedback indicano che conosce il esistenza degli altri aggregati, quindi mi sento a mio agio nel sapere anche lo stato di sola lettura di questi aggregati.

Tuttavia, in base alle sue proprietà, l'aggregato Feedback non ha alcuna conoscenza dell'esistenza del Supplier aggregato, quindi dovrebbe avere conoscenza dello stato di sola lettura di questo aggregato?

La soluzione alternativa all'implementazione della regola 3 è spostare questa logica in appropriato CommandHandler . Tuttavia, sembra che si stia spostando la logica del dominio lontano dal "centro" della mia architettura basata sulla cipolla.

    
posta magnus 20.05.2016 - 07:33
fonte

2 risposte

1

Se la correttezza della transazione richiede che un aggregato conosca lo stato corrente di un altro aggregato, allora il tuo modello è sbagliato.

Nella maggior parte dei casi, non è richiesta la correttezza della transazione . Le aziende tendono ad avere tolleranza sulla latenza e sui dati obsoleti. Ciò è particolarmente vero per le incoerenze facili da individuare e facili da porre rimedio.

Quindi il comando verrà eseguito dall'aggregato che cambia stato. Per eseguire il controllo non necessariamente corretto, è necessario non necessariamente l'ultima copia dello stato dell'altro aggregato.

Per i comandi su un aggregato esistente, lo schema abituale è passare un Repository all'aggregato e l'aggregato passerà il suo stato al repository, che fornisce una query che restituisce uno stato / proiezione immutabile dell'altra aggregazione

class Feedback {
    void downvote(Repository<Supplier.State> query) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Ma i modelli di costruzione sono strani - quando si crea l'oggetto, il chiamante conosce già lo stato interno, perché lo sta fornendo. Lo stesso modello funziona, sembra semplicemente inutile

class Feedback {
    __construct(SupplierId supplierId, SupplierOperatingQuery query ...) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Seguiamo le regole mantenendo tutta la logica di dominio negli oggetti del dominio, ma in realtà non stiamo proteggendo l'invarianza di business in alcun modo utile (perché tutte le stesse informazioni sono disponibili per il componente dell'applicazione ). Per il modello di creazione, sarebbe altrettanto buono scrivere

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}
    
risposta data 20.05.2016 - 08:47
fonte
-1

So che questa è una vecchia domanda, ma vorrei sottolineare che il problema deriva direttamente da una premessa errata. Cioè, le radici aggregate che intendiamo assumere esistono semplicemente errate.

Esiste solo una radice aggregata nel sistema che hai descritto: Cliente. Sia un Ordine che il Feedback, mentre possono essere aggregati a sé stanti, dipendono dal Cliente per l'esistenza, quindi non sono essi stessi radici aggregate. La logica che fornisci nel tuo costruttore di feedback sembra indicare che un ordine DEVE avere un customerId e che anche il feedback DEVE essere correlato a un cliente. Questo ha senso. In che modo un ordine o un feedback non possono essere correlati a un cliente? Inoltre, il Fornitore sembra essere correlato logicamente all'Ordine (così sarebbe all'interno di questo aggregato).

Tenendo presente quanto sopra, tutte le informazioni che desideri sono già disponibili nella radice aggregata del cliente e diventa chiaro che stai applicando le tue regole nel posto sbagliato. I costruttori sono luoghi terribili per far rispettare le regole aziendali e dovrebbero essere evitati a tutti i costi. Ecco come dovrebbe apparire (Nota: non includerò costruttori per Cliente e ordine perché probabilmente dovrebbero essere utilizzate le fabbriche. Inoltre non mostra tutti i metodi di interfaccia).

/*******************************\
   Interfaces, explained below 
\*******************************/

interface ICustomer
{
    public function getId() : int;
}

interface IUser extends ICustomer
{
    public function getUsername() : string;

    public function getPassword() : string;

    public function changeUsername( string $new ) : void;

    public function resetPassword( string $new ) : void;

}

interface IReviewer extends ICustomer
{
    public function provideFeedback( IOrder $order, string $content ) : void;
}

interface IBuyer extends ICustomer
{
    public function placeOrder( IOrder $order ) : void;
}

interface IOrder
{
    public function getCustomerId() : int;

    public function addFeedback( string $content ) : void;
}


interface IFeedback
{
    public function addContent( string $content ) : void;

    public function isValidContent( string $content ) : void;
}



/*******************************\
   Implentation
\*******************************/



class Customer implements IReviewer, IBuyer
{
    protected $id;

    protected $orders = [];

    public function provideFeedback( IOrder $order, string $content ) : void
    {
        if( $order->getCustomerId() !== $this->getId() )
            throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');

        $order->addFeedback( $content );
    }
}


class Order implements IOrder
{
    protected $supplier;

    protected $feedbacks = [];

    public function addFeedback( string $content ) : void
    {
        if( false === $this->supplier->isOperating() )
            throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');

        // could be any IFeedback
        $feedback = new Feedback( $this );

        $feedback->addContent( $content );

        $this->feedbacks[] = $feedback;
    }
}


class Feedback implements IFeedback
{
    protected $order;

    protected $content;

    public function __construct( IOrder $order )
    {    
         // we don't carry our business rules in constructors
         $this->order = $order;
    }

    public function addContent( string $content ) : void
    {
        if( false === $this->isValidContent($content) )
            throw new \Exception("Content contains offensive language.");

        $this->content = $content;
    }
}

Va bene. Let's rompere questo un po '. La prima cosa che noterete è quanto più dichiarativo è questo modello. Tutto è un'azione, diventa chiaro DOVE dovrebbero essere applicate le regole aziendali. Il design sopra non "fa" la cosa giusta, "dice" la cosa giusta.

Cosa indurrebbe nessuno a presumere che le regole vengano eseguite nella riga seguente?

// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);

In secondo luogo, è possibile vedere che tutta la logica relativa alla convalida delle regole aziendali viene eseguita il più vicino possibile ai modelli a cui appartengono. Nel tuo esempio, il costruttore (un singolo metodo) sta eseguendo più convalide su diversi modelli. Questo rompe il design SOLIDO. Dove dovremmo aggiungere un assegno per assicurarci che il contenuto del Feedback non contenga parole parolacce? Un altro controllo nel costruttore? Che cosa succede se diversi tipi di feedback richiedono controlli di contenuto diversi? Brutto.

In terzo luogo, guardando le interfacce, puoi vedere che ci sono luoghi naturali per estendere / modificare le regole attraverso la composizione. Ad esempio, diversi tipi di ordini possono avere regole diverse riguardo a quando il feedback può essere fornito. L'ordine può anche fornire diversi tipi di feedback, che a loro volta possono avere regole diverse per la convalida.

Puoi anche vedere una serie di interfacce di ICustomer *. Questi sono usati per comporre l'aggregato del Cliente di cui abbiamo bisogno qui (probabilmente non solo chiamato Cliente). La ragione di questo è semplice. È MOLTO probabile che un Cliente sia una radice aggregata ENORME che si diffonde in tutto il dominio / DB. Utilizzando le interfacce, possiamo decomporre un aggregato (che è probabilmente troppo grande per essere caricato) in più radici aggregate che forniscono solo determinate azioni (come ordinare o fornire feedback). È possibile visualizzare l'aggregato nella mia implementazione in grado di ENTRARE ordini e fornire feedback, ma non può essere utilizzato per reimpostare una password o modificare un nome utente.

Quindi la risposta alla tua domanda è che gli aggregati dovrebbero convalidare se stessi. Se non riesci, probabilmente hai un modello difettoso.

    
risposta data 09.02.2018 - 22:13
fonte