Bertrand Meyer a fost cel care a introdus distincția între termenii Commandă și Interogare, când a afirmat că orice metodă trebuie să fie ori o Interogare (doar citește informații) fie o Comandă (execută o acțiune), dar nu ambele simultan.
CQRS este un acronim în limba engleză pentru Command-Query Responsibility Segregation. Este un șablon arhitectural care a fost discutat prima dată de Greg Young în 2010. Folosește ca bază principiul enunțat de Meyer și îl completează, sugerând ca responsabilitățile de citire și cele de scriere să fie împărțite în obiecte complet distincte. Această abordare se opune celei tradiționale CRUD, unde Modelul este creat, citit, modificat și șters în mod uniform în cadrul aceleiași clase, cu metode pentru fiecare dintre aceste acțiuni.
Cele mai multe componente se pretează foarte bine pe abordarea CRUD și nu e indicat să le complicăm inutil doar de dragul de a folosi CQRS. Să ilustrăm modelul CRUD cu o diagramă în care o componentă foarte simplă interacționează direct cu utilizatorul final, dar și salvează și extrage informațiile din baza de date. Puteți oricând substitui atât utilizatorul cât și stocarea persistentă cu alte componente de sistem. Diagrama noastră CRUD folosește pattern-ul Repository pentru a executa acțiunile, acesta comunicând la rândul său cu modelul. În acest caz, deoarece comporatamentul este în același loc, există pericolul aglomerării codului, acesta devenind greu de menținut pe termen lung, precum și expus introducerii unor erori în părți ale sistemului în care nu ne-am aștepta.
Să aplicăm împreună separarea între Comenzi și Interogări pe un exemplu care tratează angajații.
// foloseste model Angajat
public class EmployeeService {
public function obtineAngajat($id) { /* ... */}
public function obtineAngajatiiInConcediu() { /* ... */}
public function angajeaza(Persoana $persoana, $laData) { /* ... */}
public function promoveaza($id, $noulPost, $deLaData) { /* ... */}
public function maresteRemuneratia($id, $suma, $delaData) { /* ... */}
public function concediaza($id, $laData) { /* ... */}
public function modificaAdresa($id, Adresa $adresa) { /* ... */}
}
La folosirea CQRS vom avea două servicii specializate,
// foloseste acelasi model Angajat
public class EmployeeReadService {
public function obtineAngajat($id) { /* ... */}
public function obtineAngajatiiInConcediu() { /* ... */}
}
// foloseste acelasi model Angajat
public class EmployeeWriteService {
public function angajeaza(Persoana $persoana, $laData) { /* ... */}
public function promoveaza($id, $noulPost, $deLaData) { /* ... */}
public function maresteRemuneratia($id, $suma, $delaData) { /* ... */}
public function concediaza($id, $laData) { /* ... */}
public function modificaAdresa($id, Adresa $adresa) { /* ... */}
}
Aceasta este cea mai simplă separare pe care o putem face, dar deja ne ajută foarte mult. Am reprezentat în diagrama de mai jos modelul CQRS cu Model comun.
Frumusețea folosirii serviciilor distincte pentru interogări și comenzi rezidă în faptul că ne face codul suficient de flexibil încât să poată folosi și metode diferite. Imaginați-vă Modelul specializat! Cât de elegant este acum doar pentru că ați eliminat partea care se ocupa de scriere. De asemenea, pentru modelul specializat pe citire, nu a devenit deja mult simplificat când nu trebuie să se ocupe de aspectele specifice citirii?
Să avansăm încă un pas și să separăm complet baza de date, pentru a scrie într-una în care consistența datelor este îndeplinită și să citim din alta care are o eventuală consistență (poate fi în urma cu versiunea datelor), dar optimizată pentru citire. Vă rog să observați cum în partea dreaptă a diagramei am eliminat complet orice referință la Model și cum returnăm obiecte DTO direct din serviciile de Interogare.
Am adăugat de asemenea conceptul de Fațadă, care este foarte răspândit în arhitecturile software contemporane, acesta ajutându-ne să separăm limitele (en: boundaries) mult mai curat.
În abordarea CRUD, codul poate deveni strâns cuplat foarte repede. Una din cele mai frecvente probleme pe care le-am întâlnit ca programator a fost situația în care o modificare într-o parte a codului care se ocupă de o interogare să rezulte în efecte secundare nedorite ale comportamentului unei Comenzi. Aceste situații sunt foarte dificil de tratat, deoarece ceea ce părea o mică modificare de cod ajunge în realitate să fie o refactorizare nu atât de simplă. CQRS abordează această problemă foarte simplu, prin separarea fizică a codului care face scrierile de cel care citește.
Serviciile responsabile pentru comportamente diferite pot fi găzduite pe sisteme diferite, permițând așadar scalarea lor independentă. Cele mai multe dintre aplicații necesită citiri intensive, astfel că serviciile dedicate de interogare pot fi scalate orizontal folosing zeci de mașini. În același timp, serviciile de comenzi trebuie scalate la o scară mult mai mică, așadar vor fi necesare mai puține mașini.
A avea servicii diferite crește flexibilitatea în abordarea stocării datelor. Cel mai simplu model, cel al unei singure baze de date e foarte bun pentru început, dar libertatea de a scrie într-o bază de date normalizată unde sunt reguli stricte de consistență a datelor și mutarea datelor de citire într-una denormalizată optimizată pentru scopul său este o soluție foarte elegantă la o nevoie pe care am întâlnit-o destul de des în ultimul timp, fiind cea mai ieftin de pus în practică refactorizare a unor sisteme a căror performanță lăsa de dorit.
CQRS se potrivește foarte bine cu modelele bazate pe evenimente, deoarece permite event sourcing-ului să înlocuiască baza de date consistentă cu un storage pentru evenimente. Evenimentele sunt apoi aplicate și se obțin datele interogabile. Pe site-ul cqrsinfo există un articol foarte detaliat pe acest subiect.
Un avantaj mai puțin intuitiv vine din ușurința cu care se pot împărți task-uri atomice programatorilor din echipă. Astfel, echipele mai numeroase pot colabora mai bine pe cod. În timp ce componentele CRUD pot fi încredințate colegilor mai puțin experimentați, complexitatea CQRS poate fi manevrată de colegii seniori, care apoi distribuie informația întregii echipe.
Să presupunem că ai ales să folosiți modele diferite pentru cele două responsabilități, le numim aici generic CommandModel și QueryModel. Se adaugă în mod inevitabil un overhead din perspectiva mentenanței, câmpurile noi trebuie adăugate în ambele locuri.
Este necesară o abordare la nivel de componentă a utilității șablonului CQRS, nu una la nivelul întregii aplicații, deoarece doar o parte din componente vor avea nevoie de un tratament special, non-CRUD.
Cele mai bune locuri pentru a lua în calcul CQRS sunt limitele dintre componentele sistemului, unde segregarea permite cazurilor specifice să fie rafinate și mai în profunzime, spre exemplu prin folosirea abordării event sourcing dacă aceasta este necesară, fără afectarea altor componente ale aplicației.
Modelarea folosind CQRS este posibil a fi mai greu înțeleasă de o parte din colegii programatori, astfel încât folosită în locurile nepotrivite va cauza probleme. Dar așa cum am menționat mai sus, acest lucru se poate evita printr-o mai inteligentă distribuire a rolurilor în echipă și transferul de cunoștințe între colegi în timp.
Există în general mai multe clase care trebuie încărcate, mai mult spațiu pe disk este consumat atunci când se separă și datele, dar și mai mult spațiu este necesar la folosirea event sourcing. De aceea, sunt necesare capacități DevOps în cadrul echipei și în general o infrastructură mai bine organizată.
Note:
În diagramele de mai sus termenul Model a fost folosit cu înțelesul de Domain Object. A nu fi încurcat cu șablonul Active Record care furnizează în mod suplimentar și comportament.