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.