Maciej Aniserowicz CQRS Pragmatycznie

July 29, 2017 | Author: kowgli | Category: Programmer, Sql, Source Code, Class (Computer Programming), Databases
Share Embed Donate


Short Description

CQRS Pragmatycznie...

Description

CQRS Pragmatycznie

Maciej Aniserowicz devstyle.pl

CQRS pragmatycznie Nowoczesne podejście do tworzenia systemów w praktyce CQRS – Command Query Responsibility Segregation – to bardzo popularny temat wśród programistów od ładnych kilku lat. Wokół niego urosło tyle dodatkowych pojęć i zależności, że aktualnie ciężko jest zacząć zgłębiać to zagadnienie. W artykule przedstawię czym naprawdę jest CQRS i jak to “ugryźć”. Przykłady kodu napisane są w C#, jednak powinny być zrozumiałe dla programistów każdego języka.

Trochę historii Ojcami CQRS są Udi Dahan i Greg Young – wyśmienici eksperci wywodzący się ze światka .NET. Pod koniec pierwszej dekady XXI wieku określili kilka prostych zasad, które potrafią znacznie uprzyjemnić pracę nad skomplikowanymi systemami informatycznymi. Można wspomnieć, że wówczas wyszukanie informacji na ten temat nie było proste:

Rysunek 1. Wyszukiwanie materiałów o CQRS na początku bieżącej dekady. Wbrew pozorom jednak, w dniu dzisiejszym z CQRS powiązane jest bardzo wiele innych pojęć. Mam na myśli chociażby Event Sourcing, Doman Driven Design, Multiple datastores, NoSQL databases czy Service-Oriented Architecture. Warto zdawać sobie jednak sprawę z faktu, iż zastosowanie CQRS wcale nie wymaga znajomości tych pojęć!

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie U podstaw CQRS leży zasada CQS (Command Query Separation) przedstawiona przez Bertranda Meyera – wybitną postać w IT, twórcę języka Eiffel – jeszcze w latach ’80 poprzedniego stulecia. Zasada ta ma jedno założenie: “zadanie pytania nie powinno zmieniać odpowiedzi” (“asking a question should not change the answer”). Przekładając to na język programistów: każda metoda powinna albo wykonywać operacje modyfikujące stan systemu albo zwracać dane, nigdy jedno i drugie jednocześnie. Przedstawieni wcześniej panowie rozszerzyli nieco tę zasadę, zaktualizowali ją do najnowszych trendów, języków i technologii oraz zarekomendowali jej stosowanie do komponentów większych niż metoda. Tak narodziło się CQRS.

CQRS.init() Command Query Responsibility Segregation często jest określane jako wzorzec projektowy bądź wzorzec architektoniczny. Mija się to z prawdą. Dana implementacja CQRS może wykorzystywać zdefiniowane wzorce, jednak na poziomie teoretycznym jest to po prostu podejście do tworzenia oprogramowania. Podejście, które wpływa na system już w fazie budowania modelu reprezentującego domenę. Kluczowa i najważniejsza zasada CQRS mówi, że jeden model w systemie to za mało do wydajnej pracy – zarówno systemu, jak i programisty. Bardzo często można spotkać aplikacje, w których klasy budujące model koncepcyjny są po prostu odwzorowaniem struktury bazy danych. Wówczas najprostsze możliwe przedstawienie schematu komunikacji wygląda tak:

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie

Rysunek 2. Diagram koncepcyjny standardowego systemu.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Jedyny model służy zarówno do dostarczania klientom (aplikacjom, usługom, przeglądarkom…) informacji o systemie, jak też wykonywania operacji wchodzących w skład logiki biznesowej. Ten model to nader często kod wygenerowany na podstawie bazy danych “zaimportowanej” do ulubionego IDE. O problemach wynikających z takiego podejścia można poczytać w kolejnych akapitach. Teraz skupimy się na tym, co proponuje nam CQRS:

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie

Rysunek 3. Diagram koncepcyjny systemu CQRS. CQRS to po prostu utworzenie więcej niż jednego modelu w systemie. Przez “model” można rozumieć “zestaw klas” – zatem otrzymujemy przynajmniej dwie niezależne od siebie grupy klas, dedykowane do odczytania lub modyfikacji stanu. Separacja operacji odczytu i zapisu jest sercem CQRS.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Jeden (lub więcej) model posłuży do odczytu, więc będzie zoptymalizowany pod konkretne scenariusze wyświetlania danych w aplikacji. Gro takich scenariuszy wymaga zdenormalizowanych danych gotowych do wyświetlenia użytkownikowi bądź dostarczenia zewnętrznemu systemowi. Ten model często nie zawiera żadnej logiki innej niż przygotowanie informacji w odpowiednim formacie: zastosowanie grupowania czy agregacji. Koncepcyjnie jego zadaniem jest obsługa dowolnych “zapytań”, czyli: Queries. Drugi model to kod logiki biznesowej. Po zastosowaniu takiego podejścia łatwo się zdziwić, jak wiele relacji pomiędzy encjami po stronie “write modelu” po prostu… znika. Model domeny niesamowicie się upraszcza, ponieważ w ani jednym miejscu nie musi przewidywać ani obsługiwać dziesiątków scenariuszy wymagających przedstawianie informacji. Tak naprawdę to jest serce CQRS. Wszystkie pozostałe kwestie są pochodną rekomendacji: jeden model w systemie to za mało.

“Read side” w praktyce Do pracy na “jedynym modelu” służy rodzina znanych wszystkim bibliotek: ORMy, czyli Object/Relational Mappers. CQRS skłania do refleksji: czy optymalnie wykorzystujemy ORMy? Czy zdajemy sobie sprawę z ich słabości i próbujemy je niwelować? Przełomowym punktem w podejściu do tworzenia “nowoczesnego” oprogramowania może być uświadomienie sobie, iż te biblioteki nie są najlepszym narzędziem do pobierania danych. Tak! Świetnie operują one na grafach obiektów, śledzą zmiany, niesamowicie upraszczają proces zapisywania zmian w pojedynczych agregatach i ich zależnościach, ale… zbudowanie za ich pomocą wydajnego, skomplikowanego zapytania jest niezmiernie trudne. I to nie tylko dla początkujących. Nie powinniśmy traktować ORM jako generatora SQL. Świetnym generatorem SQL, lepszym od ORMów, jest prawie każdy programista.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie “Standardowe” podejście do pisania aplikacji może skutkować bardzo niewydajnym “procesem” kodowania. Obrazuje to poniższy zrzut jednego deweloperskiego ekranu. Czy Czytelnik jest w stanie wydedukować co się tu dzieje?

Rysunek 4. Proces dodawania nowej funkcji do systemu przez programistę. Dostając do oprogramowania nową funkcję mającą za zadanie wyświetlenie danych na ekranie często musimy wykonać kilka powtarzalnych kroków, które przedstawia powyższy obrazek. Co po kolei robimy jako programiści? Po pierwsze (rys. 4A): piszemy zapytanie SQL, które finalnie aplikacja powinna wykonać na bazie danych. Piszemy je w edytorze tekstu, testowo ręcznie uruchamiamy na bazie i przechodzimy do kolejnego kroku. Drugi krok (rys. 4B) to podłączenie się profilerem do naszej bazy danych, aby monitorować treść zapytań wysyłanych przez aplikację. Dzięki temu zweryfikujemy, czy faktycznie wykonywany jest oczekiwany SQL.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Krok trzeci (rys. 4C) to tłumaczenie zapytania SQL na ORM, którego używany w danym projekcie. W przypadku .NET może to być Entity Framework czy NHibernate, a w Javie na przykład Hibernate. Programiści Ruby najprawdopodobniej wykorzystają Active Record. Założę się, że wielu Czytelników przechodziło przez ten proces i na długie godziny utknęło na etapie “dostosowywania ORMa” tak, aby efektem było dokładnie takie zapytanie jakie jest wymagane przy danym scenariuszu. Wreszcie (rys. 4D): uruchamiamy aplikację i używamy jej, powodując uruchomienie zaimplementowanej właśnie logiki. W profilerze weryfikujemy SQL i powtarzamy powrót do kodu tak długo, aż wreszcie efekt nas satysfakcjonuje. W tym momencie przedstawione kroki powinny zostać zakwestionowane. Mogą pojawić się przynajmniej dwie wątpliwości: “dlaczego najpierw piszemy SQL?” oraz “dlaczego nie testujemy tego automatycznie?”. Pierwszą wątpliwość rozwiać jest łatwo: od razu definiujemy dokładny taki tekst wysyłany do bazy danych, ponieważ może on wykorzystać istniejące indeksy. Programista o nich wie, a ORM – niekoniecznie. Dodatkowo zapytanie to nie pobiera więcej danych, niż potrzeba, unikamy tzw. błędu “select *”. Z drugiej strony: pobieramy wszystkie dane, które są wymagane w tym scenariuszu. Dążymy, o ile to możliwe, aby całość zamknąć w jednym odwołaniu do bazy.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Wątpliwość związana z testami automatycznymi wymaga dłuższego wyjaśnienia. To prawda, powinniśmy unikać testów manualnych, szczególnie na etapie tworzenia kodu. Jednak w tym przypadku: jak upewnić się, że zapytanie jest wystarczająco wydajne? W programistycznej bazie danych najprawdopodobniej posiadamy bardzo niewiele rekordów, więc nawet niepoprawne zapytanie wykona się szybko i nie zmierzymy tego w testach. Moglibyśmy co prawda do celów testowych wygenerować miliony wierszy, lecz taki proces bardzo wydłuży czas wykonywania testów… a i tak nie zasymuluje prawdziwego zachowania produkcyjnego środowiska. Jak upewnić się, że pobieramy naraz wszystkie potrzebne dane, nie uciekając się do mechanizmu “lazy loading”, czyli automatycznego “dociągania” danych przez ORM? Taka luka prowadzi do błędu zwanego “select N+1” i jest częstą przyczyną problemów wydajnościowych. Wreszcie: możemy ulec pokusie uruchamiania testów na innym silniku bazy danych – działającym w pamięci – niż tym faktycznie używanym na produkcji. O ile taka praktyka jest niejednokrotnie przydatna, to zastosowanie jej w niewłaściwym przypadku przysporzy więcej kłopotów niż pożytku. To zagadnienie wykracza jednak poza zakres niniejszego artykułu. Wszystkie te kwestie DA SIĘ zaadresować w testach automatycznych, lecz… jest prostsza droga.

Jak CQRS może nam pomóc? Przyjrzyjmy się temu na przykładzie. Załóżmy, że tworzony system służy do wyświetlania reklam klientom firm. Czyli piszemy jeden z 90% systemów w aktualnej rzeczywistości :). Klasa Advertisement reprezentuje reklamę pokazywaną użytkownikowi. Nieaktualne reklamy archiwizujemy zamiast usuwać je z bazy. Dodatkowo projektant złamał w tym przypadku dobrą praktykę i postanowił, że grafika reklamy znajdzie się w bazie danych zamiast na dysku:

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie 1

public class Advertisement

2

{

3

public int Id { get ; set ; }

4

public string Title { get ; set ; }

5

public string Content { get ; set ; }

6

public bool IsArchived { get ; set ; }

7

public byte [] Picture { get ; set ; } }

8

Listing 1. Klasa reprezentująca reklamę pokazywaną użytkownikowi. Będziemy zliczać liczbę wyświetleń reklam, aby obserwować trendy popularności reklam: 1

public class AdView

2

{

3

public Advertisement ParentAd { get ; set ; }

4

public DateTime ViewDate { get ; set ; }

5

}

Listing 2. Klasa reprezentująca pojedyncze wyświetlenie reklamy.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Użytkownik systemu to najprostsza możliwa klasa User: 1

public class User

2

{

3

public int Id { get ; set ; }

4

public string UserName { get ; set ; } }

5

Listing 3. Klasa reprezentująca użytkownika systemu. I wreszcie: firma będąca klientem aplikacji. Posiada zdefiniowaną listę swoich klientów (naszych użytkowników) oraz listę reklam, które chciałaby tym klientom wyświetlić: 1

public class Company

2

{

3

public int Id { get ; set ; }

4

public string Name { get ; set ; }

5

public List Customers { get ; set ; }

6

public List Ads { get ; set ; }

7

}

Listing 4. Klasa reprezentująca użytkownika firmę – klienta systemu.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Scenariusz, jaki mamy do napisania, wymaga pobrania listy reklam do wyświetlenia użytkownikowi aktualnie przeglądającemu stronę. Musimy zatem pobrać reklamy wszystkich firm, których ta osoba jest klientem. Pokazywanie reklam zarchiwizowanych nie ma sensu, zatem te pomijamy. Chcielibyśmy zobrazować również popularność każdej reklamy, zatem przyda się liczba jej dotychczasowych obejrzeń. I dodatkowe założenie: tworzony widok listy reklam nie zawiera grafik – te są używane tylko na ekranie “szczegółów” reklamy. Brzmi sensownie? Jestem przekonany, że każdy z Czytelników spotkał się z podobnym scenariuszem. Teraz wyobraźmy sobie, że mamy tylko jeden model przedstawiający naszą domenę: klasy przedstawione powyżej. Schody zaczną się już w momencie wykonywana podstawowej operacji: pobrania danych za pomocą ORM. Będziemy mieli tutaj JOINy pomiędzy tabelami. Pojawi się podzapytanie zliczające liczbę wyświetleń. Musimy odfiltrować reklamy zarchiwizowane. I – oczywiście – upewnić się, że potencjalnie “ciężka” kolumna Picture nie znajdzie się na liście wybieranych kolumn. Czyż nie prościej będzie zdefiniować osobną klasę – składową Read Modelu – obsługującą ten jeden konkretny scenariusz? Zawierającą wyłącznie dane wymagane przez implementowany właśnie ekran? Oczywiście, że tak! Zacznijmy od napisania odpowiedniego zapytania SQL:

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie 1 2 3

select a.Id, a.Title, a.Content , ( select count (*) from AdViews v where v.ParentAdId = a.Id) from Advertisements a

4

join Companies c on a.CompanyId = c.Id

5

join Users u on u.CompanyId = c.Id

6

where

7

a.IsArchived = 0

8

and u.Id =

Listing 5. Zapytanie dostarczające reklamy do wyświetlenia użytkownikowi. Wszystkie przedstawione warunki mamy spełnione, a spędziliśmy nad tym krokiem na pewno zdecydowanie mniej czasu niż gdybyśmy zabrali się do tego z pomocą dowolnego ORMa. Skoro mamy gotowe zapytanie to… jak użyć go w aplikacji? Rozwiązanie tego dylematu jest banalnie proste: stwórzmy widok!

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie 1 2 3 4

CREATE VIEW advertisements_for_ads_page AS select u.Id as UserId, a.Id, a.Title, a.Content , ( select count (*) from AdViews v where v.ParentAdId = a.Id) from Advertisements a

5

join Companies c on a.CompanyId = c.Id

6

join Users u on u.CompanyId = c.Id

7 8

where a.IsArchived = 0

Listing 6. Zapytanie zapisane w postaci widoku. Do klauzuli “select” musimy dodać UserId, aby być w stanie filtrować ów widok na potrzeby jednego użytkownika. Mając spłaszczony widok dedykowany dla konkretnego scenariusza jesteśmy w stanie o wiele prościej przetestować go za pomocą testów automatycznych, niż gdybyśmy to robili na grafie obiektów, pełnym zbędnych atrybutów i relacji. Dodatkowo niektóre silniki baz danych pozwolą także na “materializację” widoku i nałożenie nań indeksów w razie potrzeby. Przyzwyczajeni do ORMów możemy utworzyć klasę mapującą ten widok i wygenerować najprostsze zapytanie, które w każdej takiej bibliotece będzie wyglądać identycznie:

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie 1

select * from advertisements_for_ads_page where userid = X

Listing 7. Pobranie danych z użyciem widoku. Programiści .NET mają do dyspozycji bardzo przyjemną bibliotekę open source o nazwie Simple.Data, w której zapytanie będzie wyglądało tak: 1

IEnumerable ads = _simpleData.advertisements_for_ads_page.FindAllByUserId(userId);

Listing 8. Przykład wykorzystania biblioteki Simple.Data. Simple.Data odnajdzie widok w bazie i skonstruuje odpowiedni SQL na podstawie nazwy metody FindAllBy… Jej wykorzystanie wyeliminuje również konieczność implementacji klasy pośredniej reprezentującej widok, ponieważ zwracane przez nią obiekty typu ‘dynamic’ możemy bezpośrednio przekazać do silnika renderującego wynik. Jednak, chcąc zachować stosunkową uniwersalność niniejszego artykułu, nie będziemy zagłębiać się bardziej w tajniki i możliwości tej biblioteki. Podsumowując: dzięki takiej strategii proste i całkowicie standardowe wymagania są bardzo proste w implementacji. Wystarczyło uzmysłowić sobie, że istniejące już w systemie klasy nie nadają się do ich realizacji. Traktując odczyt i wyświetlanie danych jako odrębny “subsystem” uwalniamy się od wszelkich ograniczeń, na jakie możemy natknąć się w standardowym, jednomodelowym podejściu.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie “Write side” w praktyce Po odseparowaniu pobierania danych od “prawdziwej” logiki biznesowej zostajemy z kolejnym problemem: jak ją zorganizować? Istnieje wiele praktyk mówiących w jaki sposób można do tego podejść. Oczywistym przykładem może być DDD – Domain DrivenDesign. Praktyka ta ściśle definiuje z jakiego typu elementów należy budować system i gdzie umieszczać jakie konstrukcje. Jednak… chyba wszyscy spotkaliśmy się z systemami zawierającymi logikę biznesową w każdej warstwie. Poczynając od kontrolerów, przechodząc przez obiekty/serwisy domenowe, na procedurach składowanych kończąc. Nie zawsze wynika to z niedbalstwa bądź niewiedzy programistów. Taka jest niestety specyfika naszej branży. Kto pierwszy rzuci kamieniem? System z “rozproszoną” logicznie odpowiedzialnością jest bardzo trudny w utrzymaniu. Zrozumienie jego działania wymaga analizy praktycznie całego kodu, a odszukanie przyczyny buga bądź miejsca odpowiedniego do zaimplementowania nowej funkcji – wiele pracy. Takie rozwiązania charakteryzują się również bardzo dużą liczbą zależności pomiędzy komponentami, przez co zarówno ich debuggowanie jak i automatyczne testowanie nie jest zadaniem prostym ani przyjemnym. Zresztą… wszyscy znamy to z praktyki, prawda? Wróćmy do przykładowego systemu wyświetlającego reklamy i dodajmy kolejne wymaganie. Tym razem chcemy umożliwić użytkownikom oznaczenie dowolnej z nich jako “ulubioną”, aby w przyszłości być w stanie lepiej profilować ich upodobania bądź dać opcję przeglądania reklam oznaczonych w taki sposób. Jest to dość poważna zmiana, która wpłynie na kształt klasy Users:

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie 1

public class User

2

{

3

/// other properties...

4

public List InterestingAds { get ; set ; }

5

}

Listing 9. Dodanie „interesujących reklam” do klasy użytkownika. Implementacja takiego scenariusza wydaje się dość prosta: wystarczy do tego identyfikator użytkownika oraz oznaczonej reklamy. Kod realizujący to w wielu ORMach wyglądałby podobnie: 1

var user = session.Get(userId);

2

var ad = session.Get(adId);

3

user.InterestingAds.Add(ad);

4

session.Save(user);

Listing 10. Kod oznaczający reklamę jako “interesującą”.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Jednak: gdzie go umieścić? Przy goniących terminach można ulec pokusie wstawienia go po prostu do akcji kontrolera. Rozwiązanie o tyleż szybkie, co niepraktyczne: kontrolery nie powinny zawierać takiej logiki, prawda? Więc może do “AdsService”, zawierającego wszystkie operacje na reklamach? To może wydawać się naturalnym i prawidłowym podejściem, lecz wspomniana klasa “usługi operującej na reklamach” bardzo szybko rozrośnie się do wielu tysięcy linii kodu. Dzięki Command Query Responibility Segregation byliśmy w stanie uprościć stronę odczytującą dane: Queries. Spójrzmy, jak to podejście uprości kod odpowiedzialny za wykonywanie logiki biznesowej. Poznajmy Commands, czyli komendy. Założenie jest proste: każda operacja wykonująca modyfikacje danych w systemie powinna być zamodelowana za pomocą komendy. Pamiętacie, że do zrealizowania omawianego przypadku wystarczą tylko dwa identyfikatory? Więc: 1

public class MarkAdAsInterestingCommand

2

{

3

public int UserId { get ; set ; }

4

public int AdId { get ; set ; }

5

}

Listing 11. Komenda reprezentująca zamiar oznaczenia reklamy.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Jednak przyda się trochę infrastrukturalnej otoczki, aby usprawnić nawigację po kodzie oraz umożliwienie cywilizowanego i czytelnego sposobu. Niech każda komenda implementuje pusty interfejs, tzw. “marker interface”. Pozwoli to zamodelować pozostałe struktury używając zalet programowania obiektowego: 1

public interface ICommand

2

{

3

}

4

Listing 12. “Marker interface” dla wszystkich komend. Komenda jedynie reprezentuje intencję wykonania czynności, natomiast samo jej wykonanie niech znajduje się w dedykowanej do tego klasie implementującej interfejs: 1

public interface IHandleCommand

2

{

3

}

4

Listing 13. “Marker interface” dla wszystkich command handlerów.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Pusty interfejs w tym przypadku na niewiele się zda i ponownie: posłuży bardziej do spięcia całej infrastruktury w całość. Języki z typami generycznymi pozwolą na rozszerzenie go o: 1

public interface IHandleCommand : IHandleCommand

2 3

where TCommand : ICommand { void Handle(TCommand command);

4 5

}

Listing 14. Docelowy interfejs dla wszystkich command handlerów. Interfejs IHandleCommand definiuje faktyczną operację zajmującą się przetworzeniem komendy. Wykorzystanie typów generycznych uprości kod i zmaksymalizuje wykorzystanie potencjału drzemiącego w kompilatorze. Mając reprezentację komendy oraz klasy odpowiedzialnej za jej przetworzenie możemy skupić się na sposobie uruchamiania przetwarzania komendy. Na poziomie interfejsu konstrukcja taka jest bardzo prosta: 1

public interface ICommandBus

2

{ void SendCommand(T cmd) where T : ICommand;

3 4

}

Listing 15. Interfejs komponentu wysyłającego komendy do systemu.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Konkretna implementacja takiej szyny komend może mieć wiele postaci. Jedna z nich to: 1

public class CommandBus : ICommandBus

2

{

3

private readonly Func _handlersFactory;

4

public CommandBus(Func handlersFactory)

5

{ _handlersFactory = handlersFactory;

6 7

}

8

public void SendCommand(T cmd) where T : ICommand {

9

var handler = (IHandleCommand)_handlersFactory( typeof (T));

10

handler.Handle(cmd);

11 }

12 }

13 14 15

Listing 16. Przykładowa implementacja szyny komend.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Umiejętne użycie kontenera Dependency Injection pozwoli na bardzo klarowne połączenie komend z odpowiednimi dla nich handlerami w momencie uruchomienia aplikacji. Należy zwrócić uwagę na fakt, że powyższy kod wymaga posiadania jednej klasy przetwarzającej każdą komendę. Zasada: jedna komenda – jeden handler. Po przeanalizowaniu założeń stojących za interfejsem ICommandBus można dostrzec ciekawą charakterystykę tego komponentu: każda operacja w systemie przechodzi przez metodę SendCommand. Nikt nie może zrobić niczego z pominięciem tych kilku linii kodu. Można wykorzystać to zjawisko dodając w tym miejscu globalny audyt – logowanie “co, kto, kiedy”. Albo obsługę wyjątków. Interesującym pomysłem jest również wykrywanie wolnych operacji w systemie: każda komenda, której kompletne wykonanie trwa dłużej niż 1 sekunda (czy inny, dowolnie zdefiniowany, zakres czasu) powoduje powiadomienie administratora lub zespołu projektowego o potencjalnym problemie wydajnościowym. Pomysły można mnożyć. Warto zauważyć fakt, iż cały prezentowany kod jest niezależny od frameworka czy technologii wykorzystywanej do zbudowania systemu. Po rozbiciu funkcji pełnionych przez system na granularne komendy okazuje się, iż większość klas odpowiedzialnych za ich przetwarzanie jest… prosta. A co za tym idzie: łatwo jest je przeczytać, zrozumieć i utrzymywać. I, co bardzo ważne: przetestować!

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Brakującym elementem kompletnego rozwiązania jest handler nowej komendy. Co ciekawe, nawet w tak prostym przypadku mamy przynajmniej dwie możliwe drogi. Możemy pokusić się o rozszerzenie i wykorzystanie modelu, jak zostało to przedstawione na kodzie wprowadzającym w zagadnienie. Możemy jednak zastanowić się: jaka jest ostateczna operacja realizująca zadane wymaganie? Czy naprawdę musimy operować na grafie obiektów ze zdefiniowanymi relacjami? A może wystarczy:

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie 1

public class MarkAdAsInterestingHandler : IHandleCommand

2

{

3

private readonly dynamic _simpleData;

4

public MarkAdAsInterestingHandler(dynamic simpleData)

5

{ _simpleData = simpleData;

6 7

}

8

public void Handle(MarkAdAsInterestingCommand command) {

9

_simpleData.InterestingAds.Insert(

10

UserId: command.UserId, AdvertisementId: command.AdId

11 );

12 }

13 }

14 15 16

Listing 17. Klasa realizująca oznaczenie reklamy jako interesującej.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Napisanie tego kodu (z pokrywającymi go testami!) nie powinno zająć więcej niż kwadrans. Tkwi tutaj wielki potencjał. Nie mamy już problemu z umiejscowieniem logiki biznesowej. Każda operacja to komenda, a każda komenda ma swój handler. Logika znajduje się tylko tam: w handlerach. Dyscyplina w tym zakresie niesie za sobą kolejną korzyść: wystarczy odnaleźć wszystkie implementacje interfejsu ICommand (co każde IDE potrafi zrobić bez problemu), aby dowiedzieć się dokładnie: “co robi system?”. Przytaczając po raz kolejny aspekt testowalności systemu: przy takim podejściu otrzymujemy struktury idealnie wprost nadające się do testowania. Testy typowo jednostkowe przeprowadzić jest łatwo, gdyż każdy handler to niewielka klasa, realizująca tylko jedno konkretne zadanie, niewymagające wielu zależności. Natomiast pełne testy integracyjne wymagają jedynie skonfigurowania kontenera Dependency Injection i wysyłanie komend przez ICommandBus.

Podsumowanie CQRS można zaimplementować na wiele sposobów. Przedstawione tutaj podejście pozwala na spróbowanie swoich sił w praktycznie dowolnym systemie. Nie wymaga ono zmiany bazy danych czy wprowadzania szyny wiadomości do projektu. Wystarczy znajomość refactoringu “extract method to class”. Warstwa kontrolerów wygląda nareszcie tak jak powinna wyglądać: służy wyłącznie do dystrybucji poleceń w głąb aplikacji. Każdy kontroler (czy inny “punkt wejścia”, zależnie od zastosowanej architektury) potrzebuje tylko jednej z dwóch zależności: albo dostępu do danych (Simple.Data, inny ORM, repozytorium) albo implementacji ICommandBus.

CQRS pragmatycznie | devstyle.pl

CQRS pragmatycznie Tę koncepcję można rozszerzać o kolejne pojęcia – jak chociażby zdarzenia – jednak to wydaje się być tematem na osobny artykuł.

Chcesz więcej? Zapraszam na devstyle.pl!

W szczególności może zainteresować Cię kategoria CQRS. Tam znajdziesz teksty rozwijające temat Command Query Responsibility Segregation. Do przeczytania!

CQRS pragmatycznie | devstyle.pl

View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF