CQRS – Command Query Responsibility Segregation. Sama nazwa już może przysporzyć zawrotu głowy. Gdy pierwszy raz usłyszałem o tym wzorcu projektowym to od razu w mojej głowie pojawiły się myśli typu „Wow! Dużo literek w skrócie więc pewnie trudne…” Nic bardziej mylnego!
Historia CQRS
Po raz pierwszy o CQRS świat usłyszał w 2010 roku dzięki Gregowi Youngowi w „CQRS Documents by Greg Young” (https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf). Greg opisuje tam często spotykane problemy jakie mogą występować podczas developmentu aplikacji o architekturze warstwowej. Architektura warstwowa MVC (Model View Controller) wydaje się być stosunkowo dobrym wyborem podczas implementacji aplikacji, ale sama w sobie niestety nie chroni nas przed implementowaniem klas typu „spagetti code”, czy też niedbalstwem o wydajność aplikacji. I tutaj z pomocą przychodzi nasz tajemniczy CQRS ze swoim logicznym podziałem kodu na dwie sekcje: „queries” i „commands”.
Czym jest CQRS?
W CQRS chodzi o to aby oddzielić od siebie te części kodu, które modyfikują stan obiektów (czyli commands) od tych, które tylko udostępniają dane (czyli queries). Taka organizacja kodu w projekcie znacznie upraszcza jego czytelność. Od razu widzimy za co odpowiadają poszczególne klasy. Stanowi także spore pole do optymalizacji zapytań do bazy danych. Ponad to, my, jako programiści, jesteśmy zmuszeni do zachowania pojedynczej odpowiedzialności klasy. Co za tym idzie możemy unikać tak zwanych klas „tasiemców”, których długość często przekracza nawet kilka tysięcy linii kodu! Poniżej znajduje się prosty przykład, przedstawiający przepływ danych w aplikacji z wykorzystaniem CQRS.
Przykład aplikacji
Z racji, że Kotlin ma zastąpić w najbliższej przyszłości JAVĘ na niektórych polach, to uznałem że będzie dobrym językiem, aby używać go do przykładów w moich postach na tym blogu. Więcej o Kotlinie możesz znaleźć na https://kotlinlang.org. Aby ułatwić nie co development aplikacji, w przykładzie zostanie użyty Spring Boot. Pozwoli nam to na uruchomienie aplikacji w relatywnie prosty sposób i sprawdzenie czy nasz kod działa!
Nasza przykładowa aplikacja będzie odpowiadała za zarządzanie listą osób. Wiem, to nie jest nic odkrywczego ale, nie o to tutaj chodzi. Skupmy się raczej na samej implementacji CQRS i na tym, co możemy dzięki osiągnąć.
Na stronie https://start.spring.io/ można wygenerować projekt, który będzie zawierał wszystkie niezbędne dla naszej aplikacji zależności. Ja wybrałem taką konfigurację:
Dodałem trzy zależności do projektu:
- String Data JPA – dzięki temu będziemy w mogli w bardzo łatwy sposób wykonywać zapytania do bazy danych. Za pomocą jednego interfejsu Spring Data dostarczy nam szereg najczęściej używanych metod takich jak save, delete, findOneById i wiele, wiele innych.
- Spring Web – podniesie kontekst webowy aplikacji, tak aby była możliwość wystawienia kilku endpoint’ów REST-owych.
- H2 Database – Relacyjna baza danych, która jest uruchamiana automatycznie w pamięci operacyjnej zaraz po starcie aplikacji.
W pliku pom.xml powinno to wyglądać mniej więcej tak jak poniżej:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ara</groupId>
<artifactId>cqrs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cqrs</name>
<description>CQRS Demo Application</description>
<properties>
<java.version>1.8</java.version>
<kotlin.version>1.3.61</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
<plugin>jpa</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
Implementacja
W pakiecie „com.ara” znajduję się klasa CqrsApplication. Odpowiada ona za start całej aplikacji Spring Boot.
package com.ara
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class CqrsApplication
fun main(args: Array<String>) {
runApplication<CqrsApplication>(*args)
}
Skoro nasza aplikacja ma za zadanie zarządzanie listą osób, to zaimplementujmy obiekt, który będzie przechowywał dane pojedynczej osoby. Poniższa klasa Person.kt to nic innego jak zwykłe POJO/Encja z trzeba polami:
- name
- age
- id
package com.ara.mvc.model
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
@Entity
data class Person(var name: String,
val age: Integer,
@Id @GeneratedValue var id: Long? = null)
No i tutaj zaczyna się całe mięso naszej aplikacji 😀 Jak wspomniałem wcześniej CQRS dzieli funkcjonalności aplikacji na dwie sekcje. Pierwsza z nich to Commands. Sekcja ta powinna zawierać tylko takie funkcjonalności, które faktycznie zmieniają stan obiektów i nie służą do pobierania danych. Utwórzmy zatem serwis w formie interfejsu, który będzie posiadał deklaracje dwóch metod: save() oraz delete().
package com.ara.cqrs.service
import com.ara.model.Person
import org.springframework.stereotype.Service
@Service
interface PersonCommandService {
fun save(person: Person)
fun delete(name: String)
}
Implementacja powyższego interfejsu PersonCommandService.kt będzie dostarczać te dwie metody. Jak widać żadna z nich nie zwraca wartości i nie jesteśmy w stanie użyć ich do pobierania informacji o osobach z bazy danych. Bardzo dobrze widać tutaj zachowaną tak zwaną „pojedynczą odpowiedzialność”.
package com.ara.cqrs.service.impl
import com.ara.cqrs.repository.PersonCommandRepository
import com.ara.cqrs.service.PersonCommandService
import com.ara.cqrs.service.PersonQueryService
import com.ara.model.Person
import org.springframework.stereotype.Service
@Service
class PersonCommandServiceImpl(private val personCommandRepository: PersonCommandRepository,
private val personQueryService: PersonQueryService) : PersonCommandService {
override fun save(person: Person) {
personCommandRepository.save(person)
}
override fun delete(name: String) {
val person : Person = personQueryService.findByName(name)
personCommandRepository.delete(person)
}
}
Stwórzmy jeszcze repozytorium z wykorzystaniem Spring Data na potrzeby naszego serwisu, obsługującego komendy.
package com.ara.cqrs.repository
import com.ara.model.Person
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
@Repository
interface PersonCommandRepository : CrudRepository<Person, Long> {
}
Analogicznie sprawa wygląda w przypadku kolejnej sekcji, którą jest „Queries”. Tutaj sprawa wygląda odwrotnie. W sekcji „Queries” mamy do czynienia tylko z funkcjonalnościami, które nie zmieniają stanów obiektów, a jedynie serwują dane o nich. Czyli poniższa klasa PersonQueryService.kt także udostępnia dwie definicje metod, ale każda z nich służy do pobierania danych, a nie ich edycji.
package com.ara.cqrs.service
import com.ara.model.Person
import org.springframework.stereotype.Service
@Service
interface PersonQueryService {
fun findAll(): MutableIterable<Person>
fun findByName(name: String) : Person
}
Implementacja powyższego interfejsu może wyglądać następująco:
package com.ara.cqrs.service.impl
import com.ara.cqrs.repository.PersonQueryRepository
import com.ara.cqrs.service.PersonQueryService
import com.ara.model.Person
import org.springframework.stereotype.Service
import java.util.*
@Service
class PersonQueryServiceImpl(private val personQueryRepository: PersonQueryRepository) : PersonQueryService {
override fun findAll(): MutableIterable<Person> {
return personQueryRepository.findAll()
}
override fun findByName(name: String): Person {
var result : Optional<Person> = personQueryRepository.findByName(name)
if (result.isPresent)
return result.get()
else
throw RuntimeException("Can not find person with name ${name}")
}
}
No i tworzymy analogicznie repozytorium do obsługi komunikacji z bazą danych dla serwisu, zajmującego się operacjami typu „Query”.
package com.ara.cqrs.repository
import com.ara.model.Person
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
import java.util.*
@Repository
interface PersonQueryRepository: CrudRepository<Person, Long> {
fun findByName(name : String) : Optional<Person>
}
W klasie PersonQueryServiceImpl.kt nie ma możliwości zmiany stanu obiektu Person. I o to chodzi! Programista, korzystający z takiego kodu, nie musi się zastanawiać, czy jeśli wywoła jakąś metodę, która teoretycznie powinna mu tylko zwrócić dane, to nie zmieni stanu jakiegoś obiektu. Poza tym można pójść krok dalej i napisać specjalnie zoptymalizowane zapytania SQL do bazy danych, aby przyspieszyć pracę aplikacji Ale to już temat na kolejny post 🙂
Ok, skoro mamy już zaimplementowaną warstwę serwisu i logiki biznesowej, to czas na stworzenie kontrolerów, które udostępnią nam dostęp do metod serwisowych. Jako pierwszy stwórzmy kontroler obsługujący zdarzenia typu „Command”
package com.ara.cqrs.controller
import com.ara.cqrs.service.PersonCommandService
import com.ara.model.Person
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/cqrs/command")
class PersonCommandController (private val personCommandService: PersonCommandService) {
@PostMapping("/person")
fun savePerson(@RequestBody person: Person) = personCommandService.save(person)
@DeleteMapping("/person")
fun deletePerson(@RequestParam(value = "name") name: String) = personCommandService.delete(name)
}
I analogicznie tworzymy kolejny kontroler z obsługą zdarzeń typu „Query”
package com.ara.cqrs.controller
import com.ara.cqrs.service.PersonQueryService
import com.ara.model.Person
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/cqrs/query")
class PersonQueryController(private val personQueryService: PersonQueryService) {
@GetMapping("/person/all")
fun findAll() : MutableIterable<Person> = personQueryService.findAll()
@GetMapping("/person")
fun getPersonById(@RequestParam(value = "name") name: String) : Person = personQueryService.findByName(name)
}
Podsumowanie
Jak widać na przedstawionym przykładzie, CQRS wyraźnie rozdziela aplikację na dwie odrębne sekcje. Dzięki temu znacznie łatwiej utrzymać „czystość” kodu. Zmniejsza się także próg wejścia do pracy nad projektem z wykorzystaniem CQRS. Nowi programiści mają znaczniej mniejsze szanse na przypadkowe (wynikające z braku wiedzy biznesowej) wywołanie kodu, który tyko z pozoru wykonuje jedną rzecz. Kolejnym plusem takiego podziału jest możliwość szubkiego skalowania aplikacji i jej modularyzacji. Bez większych problemów jesteśmy w stanie wydzielić część logiki biznesowej do nowego mikroserwisu lub modułu.
Cały kod aplikacji jest do pobrania z mojego Githuba (https://github.com/radek-osak/kotlin-cqrs-vs-mvc)
Inne przydatne materiały:
Mam nadzieję, że podobał Ci się ten post i jeszcze nie raz zawitasz na mojego bloga w poszukiwaniu wiedzy na temat programowania i architektury aplikacji.
Będę bardzo wdzięczny za pozostawienie komentarza oraz udostępnienie tego posta w social mediach 🙂