Persistenza di entità grandi / complesse con il modello di comando - Sto facendo bene?

5

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 !!

    
posta John Hall 17.08.2015 - 18:12
fonte

2 risposte

2

Mantieni il tuo DTO semplice

Hai ragione che avere un comando con 9 argomenti nel costruttore è brutto. Ma devi davvero mettere queste cose nel costruttore? Rendi pubblici i tuoi campi, crea il tuo comando con un costruttore vuoto e assegna semplicemente i campi. Il punto delle argomentazioni del costruttore è di avere delle entità valide garantite, dal momento che si sta semplicemente usando un DTO e la validazione verrà eseguita da un'altra entità, non c'è molto motivo per nascondere i dati o avere costruttori complicati. Campi pubblici per tutti i comandi.

Niente di male nel valore-DTO

Se hai un comando composto da un insieme di campi che appartengono insieme, sentiti libero di mapparli in un oggetto valore che appartiene al tuo DTO. Non esiste una regola complessa che dice che non dovresti farlo.

I comandi non restituiscono nulla

Un comando è una modifica agli oggetti nel tuo dominio. Non dovresti aver bisogno di restituire nulla dal tuo bus di comando. Perché dovresti restituire l'ID? Hai creato l'ordine, come richiesto dal comando, e il gioco è fatto. C'è un'eccezione che potresti considerare comunque e cioè ...

Validation

Semplice convalida, come non null / non vuoto, date valide, regex-per-email, tutto appartiene a un validatore. La chiamata a questo validatore viene concatenata prima che la chiamata al proprio gestore di comandi al gestore sia garantita per ricevere un comando valido.

Per convalida complessa, ci sono diverse opinioni che puoi esaminare. Alcuni dicono che anche la convalida complessa che richiede di toccare il database ("nome utente già acquisito") non deve appartenere al validatore, alcuni preferiscono avere tutta questa validazione sul validatore (che richiede repository) e non eseguire alcuna convalida sul comando di sorta. Alla fine, devi decidere una strategia per gestire i fallimenti per convalidare i comandi.

A seconda delle tue preferenze, questo potrebbe portare a ValidationResults che viene restituito dal tuo commandbus, intrappolando e visualizzando CommandValidationException quando chiami il commandbus ...

    
risposta data 19.08.2015 - 16:09
fonte
0

Nella mia app, il flusso logico va

  • Il controller crea DTO
  • Il controllore invia al bus di comando
  • Eventi dei bus di comando
  • Gestori di eventi inviati anche al bus di comando
  • Il gestore comandi restituisce una risposta DTO al controllore
  • Controller genera una risposta al client in base alla risposta

La tua applicazione sembra simile. Il disaccoppiamento è anche un problema per me. Tuttavia, il problema di concatenare (o nidificare) comandi o eventi non è stato un problema. Mantenere un elenco di UUID generati e oggetti associati in ogni catena di comando, se possibile.

    
risposta data 19.08.2015 - 07:05
fonte