Come esattamente un comando CQRS dovrebbe essere convalidato e trasformato in un oggetto di dominio?


22

Sto adattando il CQRS 1 dei poveri da un po 'di tempo perché adoro la sua flessibilità di avere dati granulari in un archivio dati, offrendo grandi possibilità di analisi e quindi aumentando il valore aziendale e, quando necessario, un altro per letture contenenti dati denormalizzati per prestazioni migliori .

Ma sfortunatamente fin dall'inizio ho lottato con il problema di dove esattamente dovrei collocare la logica di business in questo tipo di architettura.

Da quello che ho capito, un comando è un mezzo per comunicare l'intento e non ha legami con un dominio da solo. Sono fondamentalmente oggetti di trasferimento dati (stupidi - se lo si desidera). Questo per rendere i comandi facilmente trasferibili tra diverse tecnologie. Lo stesso vale per gli eventi come le risposte agli eventi completati con successo.

In un'applicazione DDD tipica la logica di business risiede all'interno di entità, oggetti valore, radici aggregate, sono ricchi sia di dati che di comportamento. Ma un comando non è un oggetto di dominio, quindi non dovrebbe essere limitato alle rappresentazioni di dati dei domini, perché ciò li mette troppo a dura prova.

Quindi la vera domanda è: dov'è esattamente la logica?

Ho scoperto che tendo ad affrontare questa lotta il più delle volte quando provo a costruire un aggregato abbastanza complicato che stabilisce alcune regole sulle combinazioni dei suoi valori. Inoltre, durante la modellazione di oggetti di dominio mi piace seguire il paradigma fail-fast , sapendo quando un oggetto raggiunge un metodo che si trova in uno stato valido.

Diciamo che un aggregato Carutilizza due componenti:

  • Transmission,
  • Engine.

Entrambi gli oggetti value Transmissione Enginesono rappresentati come super-tipi e hanno in base i sottotipi Automatice le Manualtrasmissioni, o Petrole Electricrispettivamente i motori.

In questo dominio, vivere da solo un creato con successo Transmission, sia esso Automatico Manual, o entrambi i tipi di un Engineva completamente bene. Ma l' Caraggregato introduce alcune nuove regole, applicabili solo quando Transmissione gli Engineoggetti vengono utilizzati nello stesso contesto. Vale a dire:

  • Quando un'auto utilizza il Electricmotore, l'unico tipo di trasmissione consentito è Automatic.
  • Quando un'auto usa il Petrolmotore, può avere uno dei due tipi di Transmission.

Potrei rilevare questa violazione della combinazione di componenti a livello di creazione di un comando, ma, come ho già detto, da ciò che capisco non dovrebbe essere fatto perché il comando conterrebbe quindi una logica aziendale che dovrebbe essere limitata al livello di dominio.

Una delle opzioni è quella di spostare questa convalida della logica di business per comandare lo stesso validatore, ma neanche questo sembra essere giusto. Mi sento come se stessi decostruendo il comando, controllando le sue proprietà recuperate usando getter e confrontandole all'interno del validatore e controllando i risultati. Questo mi grida come una violazione della legge di Demetra .

Scartando l'opzione di convalida menzionata perché non sembra fattibile, sembra che si dovrebbe usare il comando e costruire l'aggregato da esso. Ma dove dovrebbe esistere questa logica? Dovrebbe essere all'interno del gestore dei comandi responsabile della gestione di un comando concreto? O dovrebbe forse essere all'interno del validatore dei comandi (non mi piace neanche questo approccio)?

Attualmente sto usando un comando e ne creo un aggregato all'interno del gestore dei comandi responsabile. Ma quando lo faccio, dovrei avere un validatore di comandi che non conterrebbe nulla, perché se il CreateCarcomando esistesse conterrebbe componenti che so essere validi in casi separati ma l'aggregato potrebbe dire diverso.


Immaginiamo uno scenario diverso che mescola diversi processi di validazione - creando un nuovo utente usando un CreateUsercomando.

Il comando contiene uno Iddegli utenti che saranno stati creati e il loro Email.

Il sistema stabilisce le seguenti regole per l'indirizzo e-mail dell'utente:

  • deve essere unico,
  • non deve essere vuoto,
  • deve contenere al massimo 100 caratteri (lunghezza massima di una colonna db).

In questo caso, anche se avere un'e-mail unica è una regola aziendale, controllarla in un aggregato ha molto poco senso, perché avrei bisogno di caricare l'intero set di e-mail correnti nel sistema in una memoria e controllare l'e-mail nel comando contro l'aggregato ( Eeeek! Qualcosa, qualcosa, performance.). Per questo motivo, sposterei questo controllo sul validatore di comandi, che prenderebbe UserRepositorycome una dipendenza e userei il repository per verificare se esiste già un utente con l'e-mail presente nel comando.

Quando si tratta di questo, improvvisamente ha senso inserire anche le altre due regole e-mail nel validatore dei comandi. Ma ho la sensazione che le regole dovrebbero essere davvero presenti all'interno di un Useraggregato e che il validatore di comandi dovrebbe solo verificare l'unicità e se la validazione ha successo dovrei procedere a creare l' Useraggregato nel CreateUserCommandHandlere passarlo a un repository per essere salvato.

Mi sento così perché è probabile che il metodo di salvataggio del repository accetti un aggregato che assicuri che una volta passato l'aggregato tutti gli invarianti siano soddisfatti. Quando la logica (ad esempio la non vuoto) è presente solo all'interno della convalida del comando stesso, un altro programmatore potrebbe saltare completamente questa convalida e chiamare direttamente il metodo save UserRepositorycon un Useroggetto che potrebbe portare a un errore irreversibile del database, perché l'e-mail potrebbe avere è passato troppo tempo.

Come gestite personalmente queste complesse convalide e trasformazioni? Sono per lo più contento della mia soluzione, ma mi sento come se avessi bisogno di affermare che le mie idee e i miei approcci non sono completamente stupidi per essere abbastanza contenti delle scelte. Sono completamente aperto ad approcci completamente diversi. Se hai qualcosa che hai personalmente provato e lavorato molto bene per te, mi piacerebbe vedere la tua soluzione.


1 di lavoro come sviluppatore PHP responsabile della creazione di sistemi RESTful mia interpretazione di CQRS devia un po 'dallo standard asincrono-comando-elaborazione approccio, come ad esempio a volte di restituire i risultati da comandi a causa della necessità di elaborare i comandi in modo sincrono.


ho bisogno di un codice di esempio, penso. come sono i tuoi oggetti di comando e dove li crei?
Ewan,

@Ewan Aggiungerò esempi di codice più tardi oggi o domani. Partenza per un viaggio in pochi minuti.
Andy,

Essendo un programmatore di PHP, suggerisco di dare un'occhiata alla mia implementazione CQRS + ES: github.com/xprt64/cqrs-es
Constantin Galbenu,

@ConstantinGALBENU Dovremmo considerare corretta l'interpretazione di Greg Young di CQRS (cosa che probabilmente dovremmo) allora la tua comprensione di CQRS è sbagliata - o almeno lo è la tua implementazione di PHP. I comandi non devono essere gestiti direttamente dagli aggregati. I comandi devono essere gestiti da gestori di comandi che possono produrre modifiche negli aggregati che quindi producono eventi da utilizzare per le repliche di stato.
Andy,

Non penso che le nostre interpretazioni siano diverse. Devi solo scavare di più in DDD (a livello tattico di aggregati) o aprire gli occhi più largamente. Esistono almeno due stili di implementazione di CQRS. Ne uso uno di loro. La mia implementazione assomiglia di più al modello Actor e rende il livello Application molto sottile, il che è sempre una buona cosa. Ho osservato che esiste un sacco di duplicazione del codice all'interno di quei servizi app e ho deciso di sostituirli con un CommandDispatcher.
Constantin Galbenu,

Risposte:


22

La seguente risposta è nel contesto dello stile CQRS promosso da cqrs.nu in cui i comandi arrivano direttamente sugli aggregati. In questo stile architettonico, i servizi dell'applicazione vengono sostituiti da un componente dell'infrastruttura ( CommandDispatcher ) che identifica l'aggregato, lo carica, lo invia il comando e quindi persiste l'aggregato (come una serie di eventi se viene utilizzato il sourcing degli eventi).

Quindi la vera domanda è: dov'è esattamente la logica?

Esistono diversi tipi di logica (di convalida). L'idea generale è quella di eseguire la logica il più presto possibile - fallire velocemente se lo si desidera. Quindi, le situazioni sono le seguenti:

  • la struttura dell'oggetto comando stesso; il costruttore del comando ha alcuni campi obbligatori che devono essere presenti per la creazione del comando; questa è la prima e più rapida validazione; questo è ovviamente contenuto nel comando.
  • convalida dei campi di basso livello, come la non vacuità di alcuni campi (come il nome utente) o il formato (un indirizzo e-mail valido). Questo tipo di validazione dovrebbe essere contenuto all'interno del comando stesso, nel costruttore. Esiste un altro stile nell'avere un isValidmetodo, ma questo mi sembra inutile poiché qualcuno dovrebbe ricordarsi di chiamare questo metodo quando in effetti dovrebbe essere sufficiente un'istanza di comando riuscita.
  • separate command validators, classi che hanno la responsabilità di convalidare un comando. Uso questo tipo di convalida quando devo controllare informazioni da più aggregati o fonti esterne. Puoi usarlo per verificare l'unicità di un nome utente. Command validatorspotrebbe essere iniettata qualsiasi dipendenza, come i repository. Tieni presente che questa convalida alla fine è coerente con l'aggregato (ovvero quando l'utente viene creato, nel frattempo potrebbe essere creato un altro utente con lo stesso nome utente)! Inoltre, non provare a mettere qui la logica che dovrebbe risiedere all'interno dell'aggregato! I validatori di comandi sono diversi dai gestori di saghe / processi che generano comandi in base agli eventi.
  • i metodi aggregati che ricevono ed elaborano i comandi. Questa è l'ultima (tipo di) convalida che si verifica. L'aggregato estrae i dati dal comando e utilizza alcune logiche aziendali fondamentali che accetta (esegue modifiche al suo stato) o lo rifiuta. Questa logica è verificata in modo fortemente coerente. Questa è l'ultima linea di difesa. Nel tuo esempio, la regola When a car uses Electric engine the only allowed transmission type is Automaticdovrebbe essere selezionata qui.

Mi sento così perché è probabile che il metodo di salvataggio del repository accetti un aggregato che assicuri che una volta passato l'aggregato tutti gli invarianti siano soddisfatti. Quando la logica (ad es. La non-vacuità) è presente solo all'interno della convalida del comando stesso, un altro programmatore potrebbe saltare completamente questa convalida e chiamare direttamente il metodo save in UserRepository con un oggetto User che potrebbe causare un errore irreversibile del database, perché l'e-mail potrebbe essere stato troppo lungo.

Utilizzando le tecniche di cui sopra nessuno può creare comandi non validi o bypassare la logica all'interno degli aggregati. I validatori di comandi vengono caricati automaticamente + chiamati dal CommandDispatcherquindi nessuno può inviare un comando direttamente all'aggregato. Si potrebbe chiamare un metodo sull'aggregato che passa un comando ma non è possibile persistere le modifiche, quindi sarebbe inutile / innocuo farlo.

Lavorando come sviluppatore di PHP responsabile della creazione di sistemi RESTful, la mia interpretazione di CQRS si discosta leggermente dall'approccio standard di elaborazione dei comandi asincroni, come talvolta restituire risultati dai comandi a causa della necessità di elaborare i comandi in modo sincrono.

Sono anche un programmatore di PHP e non restituisco nulla dai miei gestori di comandi (metodi aggregati nel modulo handleSomeCommand). Tuttavia, molto spesso restituisco informazioni al client / browser in HTTP response, ad esempio l'ID della radice aggregata appena creata o qualcosa da un modello di lettura, ma non restituisco mai (davvero mai ) nulla dai miei metodi di comando aggregati. Il semplice fatto che il comando sia stato accettato (ed elaborato - stiamo parlando dell'elaborazione PHP sincrona, giusto ?!) è sufficiente.

Restituiamo qualcosa al browser (e sto ancora facendo CQRS dal libro) perché CQRS non è un'architettura di alto livello .

Un esempio di come funzionano i validatori di comandi:

Il percorso del comando attraverso i validatori di comando nel suo cammino verso l'aggregato


Per quanto riguarda la tua strategia di validazione, il punto numero due mi salta fuori come un luogo probabile in cui la logica verrà duplicata spesso. Certamente si vorrebbe che l'aggregato dell'utente convalida anche un'e-mail non vuota e ben formata, no? Ciò diventa evidente quando introduciamo un comando ChangeEmail.
King-side-slide

@ king-side-slide non se hai un EmailAddressoggetto valore che si convalida da solo.
Constantin Galbenu,

È del tutto corretto. Si potrebbe incapsulare un EmailAddressper ridurre la duplicazione. Ancora più importante, tuttavia, nel far ciò si sposterebbe anche la logica fuori dal proprio comando e nel proprio dominio. Vale la pena notare che questo può essere portato troppo lontano. Spesso simili conoscenze (oggetti valore) possono avere requisiti di convalida diversi a seconda di chi li utilizza. EmailAddressè un esempio conveniente perché l'intera concezione di questo valore ha requisiti di convalida globali.
King-side-slide

Allo stesso modo, l'idea di un "validatore di comandi" sembra superflua. L'obiettivo non è impedire la creazione e l'invio di comandi non validi. L'obiettivo è impedire loro di eseguire. Ad esempio, posso trasmettere tutti i dati desiderati con un URL. Se non è valido, il sistema rifiuta la mia richiesta. Il comando è ancora stato creato e inviato. Se un comando richiede più aggregati per la convalida (ovvero una raccolta di Utenti per verificare l'unicità della posta elettronica), un servizio di dominio è più adatto. Oggetti come "x validator" sono spesso un segno di un modello anemico in cui i dati vengono separati dal comportamento.
King-side-slide

1
@ king-side-slide Un esempio concreto è UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Puoi vedere che questo è un dominio separato quello degli Ordini, quindi non può essere convalidato da OrderAggregate stesso.
Constantin Galbenu,

6

Una premessa fondamentale di DDD è che i modelli di dominio si convalidano da soli. Questo è un concetto critico perché eleva il tuo dominio come parte responsabile per garantire l'applicazione delle regole aziendali. Mantiene anche il tuo modello di dominio come focus per lo sviluppo.

Un sistema CQRS (come correttamente sottolineato) è un dettaglio di implementazione che rappresenta un sottodominio generico che implementa il proprio meccanismo coesivo. Il tuo modello non dovrebbe in alcun modo dipendere da qualsiasi pezzo di infrastruttura CQRS per comportarsi secondo le tue regole aziendali. L'obiettivo di DDD è di modellare il comportamento di un sistema in modo tale che il risultato sia un'utile astrazione dei requisiti funzionali del tuo dominio aziendale principale. Spostare qualsiasi singolo pezzo di questo comportamento fuori dal tuo modello, per quanto allettante, sta riducendo l'integrità e la coesione del tuo modello (e rendendolo meno utile).

Semplicemente estendendo il tuo esempio per includere un ChangeEmailcomando, possiamo illustrare perfettamente perché non vuoi alcuna delle tue logiche aziendali nella tua infrastruttura di comando in quanto dovresti duplicare le tue regole:

  • l'email non può essere vuota
  • l'e-mail non può superare i 100 caratteri
  • l'email deve essere unica

Quindi ora che possiamo essere sicuri che la nostra logica debba essere nel nostro dominio, affrontiamo il problema del "dove". Le prime due regole possono essere facilmente applicate al nostro Useraggregato, ma quest'ultima regola è un po 'più sfumata; uno che richiede ulteriori approfondimenti sulla conoscenza per ottenere una visione più approfondita. In superficie, può sembrare che questa regola si applichi a a User, ma in realtà non lo è. La "unicità" di un messaggio di posta elettronica si applica a una raccolta di Users(secondo alcuni scopi).

Ah ah! Con questo in mente, diventa evidente che la tua UserRepository(la tua raccolta in memoria di Users) potrebbe essere un candidato migliore per far rispettare questo invariante. Il metodo "salva" è probabilmente il posto più ragionevole per includere il controllo (dove è possibile generare UserEmailAlreadyExistsun'eccezione). In alternativa, un dominio UserServicepotrebbe essere ritenuto responsabile della creazione di nuovi Userse dell'aggiornamento dei loro attributi.

Fail fast è un buon approccio, ma può essere fatto solo dove e quando si adatta al resto del modello. Può essere estremamente allettante controllare i parametri su un metodo (o comando) del servizio dell'applicazione prima di elaborarlo ulteriormente nel tentativo di rilevare errori quando l'utente (lo sviluppatore) sa che la chiamata fallirà da qualche parte più in profondità nel processo. Ma nel fare ciò, avrai una conoscenza duplicata (e trapelata) in un modo che probabilmente richiederà più di un aggiornamento del codice quando cambiano le regole aziendali.


2
Sono d'accordo con questo. La mia lettura fino ad ora (senza CQRS) mi dice che la validazione dovrebbe sempre andare nel modello di dominio per proteggere gli invarianti. Ora sto leggendo CQRS mi sta dicendo di mettere la validazione negli oggetti Command. Questo sembra contro intuitivo. Conosci qualche esempio, ad esempio, su GitHub in cui la convalida viene inserita nel modello di dominio anziché nel comando? +1.
w0051977,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.