Sono in procinto di progettare e costruire un software di gestione dell'inventario su larga scala come un servizio che, si spera, vivrà una vita lunga e fruttuosa. Pertanto sto facendo un grande sforzo in anticipo per garantire che l'applicazione sia mantenibile e orizzontalmente scabale. Mi è piaciuto molto questo talk di Mathias Verraes sul disaccoppiamento che ha ispirato la prova del Command Pattern mentre presta stesso codice altamente disaccoppiato e molto esplicito.
In termini di tecnologia, sto usando Laravel 5 e Doctrine 2 supportati da un database MySQL.
Sebbene Mathias parli del tema "uscire dalla mentalità CRUD", penso che possiamo essere tutti d'accordo sul fatto che esistono molte situazioni in cui il linguaggio aziendale riflette in realtà CRUD. Ad esempio, il responsabile della logistica dice "Ho bisogno di creare un ordine d'acquisto che poi invierò al fornitore". La parte "SUBMIT" si illumina come un ottimo use case per un comando, ma non tanto per la parte CREATE.
Lascia che ti spieghi la mia posizione nella speranza che qualcuno possa essere d'accordo / in disaccordo e magari indirizzarmi verso una direzione migliore.
I sento come se il Command Pattern non fosse particolarmente adatto per l'atto di gestire una richiesta CREATE per un oggetto complesso, come un ordine d'acquisto. Nel mio dominio, l'ordine d'acquisto contiene informazioni come:
- Numero ordine / identificatore definito dall'utente (ad esempio, PO1234)
- Fornitore per il quale l'ordine verrà inviato a
- Indirizzo di spedizione
- Il messaggio da indirizzare
- Il metodo di spedizione richiesto e il corriere
- Termini NET
- Moneta monetaria
- Un ordine di vendita e un cliente se l'ordine deve essere consegnato a Drop
- Uno o più elementi pubblicitari
Se ho capito bene, Command Object dovrebbe essere essenzialmente un relativamente semplice DTO che fornisce un contratto al mondo esterno per interagire con l'applicazione. "Se si desidera CREARE un nuovo ordine d'acquisto, popolare questo oggetto con i dati e invialo nel bus" . Come sviluppatore, è meravigliosamente esplicito emettere un CreatePurchaseOrderCommand
all'applicazione - Mi piace come suona, ma quando si tratta di esecuzione si sente semplicemente maldestro.
Di goffo, voglio dire che sembra imbarazzante estrapolare tutti i dati sopra dall'oggetto Request e new-up a CreatePurchaseOrderCommand
con non meno di 9 argomenti , alcuni dei quali sarebbero array , forse un ValueObject o due (o introdurrebbe l'accoppiamento ??).
Ho preso in giro un codice simile a questo:
EDIT Ho aggiornato OrderController per riflettere le mie esigenze di emissione di un oggetto codificato JSON dopo un CREATE riuscito per soddisfare le esigenze del mio framework lato client. Questo è considerato un buon approccio? Non ho alcun problema con l'iniezione del repository nel controller in quanto sarà quasi certamente necessario per altri metodi di lettura.
OrderController.php
public function __construct(ValidatingCommandBus $commandBus, OrderRepositoryInterface $repository) { .. }
public function create(CreateOrderRequest $request){
$uuid = UUID::generate();
$command = new CreatePurchaseOrderCommand($uuid, $request->all());
try
{
$this->commandBus->execute($command);
$order = $this->repository->findByUuid($uuid);
return $this->response()->json([
'success' => true,
'data' => $order->jsonSerialize()
]);
}catch(OrderValidationException $e)
{
return $this->response()->json([
'success' => false,
'message' => $e->getMessage()
]);
}
}
ValidatingCommandBus.php (decora BaseCommandBus)
public function __construct(BaseCommandBus $baseCommandBus, IoC $container, CommandTranslator $translator) { .. }
public function execute(Command $command){
// string manipulation to CreatePurchaseOrderValidator
$validator = $this->translator->toValidator($command);
// build validator class from IOC container
// validates the command's data, might throw exception
// does *not* validate set constraints e.g., uniqueness of order number
// this is answering "does this look like a valid command?"
$this->container->make($validator)->validate($command)
// pass off to the base command bus to execute
// invokes CreatePurchaseOrderCommandHandler->handle($command)
$this->baseCommandBus->execute($command)
}
CreatePurchaseOrderCommandHandler.php
public function __construct(PurchaseOrderBuilderService $builder, PurchaseOrderRepositoryInterface $repository){ .. }
public function handle(CreatePurchaseOrderCommand $command){
// this again? i'm pulling the same data as I pulled from the
// Request object back in the Controller, now I'm just getting
// the same data out of the Command object. Seems repetitive...
$order = $this->builder->build([
$command->order_number,
$command->supplier_id,
]);
// now maybe I should handle set constraints?
// ensure order number is unique, order is not stale... etc.
$orderNumberIsUnique = new OrderNumberIsUniqueSpecification($this->repository);
if ( ! $orderNumberIsUnique->isSatisfiedBy($order) ){
throw new \ValidationException("The Order Number is not unique");
}
// ok now I can persist the entity...
try
{
// start a transaction
$this->repository->persist($order);
}catch(SomeDbException $e)
{
// roll back transaction
// cant return anything so i'll throw another exception?
throw new ErrorException('Something went wrong', $e);
}
// no return here as that breaks the CommandBus pattern :|
}
Da una prospettiva del codice penso che sembra il modo più logico per fare le cose in termini di posizionamento della validazione e così via. Ma, alla fine, non sembra la soluzione giusta.
Inoltre non mi piace il fatto che non possa restituire un valore dal Command Bus che mi lascia con la scelta di passare a generare un UUID prima di persistere (attualmente facendo affidamento su AUTO INC
di MySQL) o di eseguire una ricerca su l'identificatore univoco altro (il numero dell'ordine) che potrebbe non funzionare per tutte le entità (cioè, non tutte le entità / aggregati avranno qualche altra forma di identificatore univoco oltre all'ID del database).
Sto usando e comprendendo il Command Pattern correttamente? I miei dubbi sono validi (vedi i commenti negli esempi di codice). Mi sto perdendo qualcosa sul modello di comando? Qualsiasi input sarebbe fantastico !!