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 Car
utilizza due componenti:
Transmission
,Engine
.
Entrambi gli oggetti value Transmission
e Engine
sono rappresentati come super-tipi e hanno in base i sottotipi Automatic
e le Manual
trasmissioni, o Petrol
e Electric
rispettivamente i motori.
In questo dominio, vivere da solo un creato con successo Transmission
, sia esso Automatic
o Manual
, o entrambi i tipi di un Engine
va completamente bene. Ma l' Car
aggregato introduce alcune nuove regole, applicabili solo quando Transmission
e gli Engine
oggetti vengono utilizzati nello stesso contesto. Vale a dire:
- Quando un'auto utilizza il
Electric
motore, l'unico tipo di trasmissione consentito èAutomatic
. - Quando un'auto usa il
Petrol
motore, può avere uno dei due tipi diTransmission
.
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 CreateCar
comando 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 CreateUser
comando.
Il comando contiene uno Id
degli 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 UserRepository
come 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 User
aggregato e che il validatore di comandi dovrebbe solo verificare l'unicità e se la validazione ha successo dovrei procedere a creare l' User
aggregato nel CreateUserCommandHandler
e 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 UserRepository
con un User
oggetto 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.
CommandDispatcher
.