PRACA DYPLOMOWA MAGISTERSKA

Podobne dokumenty
Projektowanie Zorientowane na Dziedzinę. ang. Domain Driven Design

Warstwa integracji. wg. D.Alur, J.Crupi, D. Malks, Core J2EE. Wzorce projektowe.

Baza danych sql. 1. Wprowadzenie

Projektowanie architektury systemu rozproszonego. Jarosław Kuchta Projektowanie Aplikacji Internetowych

Temat: Ułatwienia wynikające z zastosowania Frameworku CakePHP podczas budowania stron internetowych

Projekt dotyczy stworzenia zintegrowanego, modularnego systemu informatycznego wspomagającego zarządzanie pracownikami i projektami w firmie

Analiza i projektowanie oprogramowania. Analiza i projektowanie oprogramowania 1/32

Java Persistence API - zagadnienia zaawansowane

Diagram wdrożenia. Rys. 5.1 Diagram wdrożenia.

Zagadnienia (1/3) Data-flow diagramy przepływów danych ERD diagramy związków encji Diagramy obiektowe w UML (ang. Unified Modeling Language)

Program szkolenia: Wprowadzenie do Domain Driven Design dla biznesu (część 0)

PHP: bazy danych, SQL, AJAX i JSON

Laboratorium Technologii Informacyjnych. Projektowanie Baz Danych

Grzegorz Ruciński. Warszawska Wyższa Szkoła Informatyki Promotor dr inż. Paweł Figat

PROCEDURA ANTYPLAGIATOWA PRAC DYPLOMOWYCH W POLITECHNICE ŁÓDZKIEJ

DDD : dla architektów oprogramowania / Vaughn Vernon. Gliwice, cop Spis treści

Analiza i projektowanie obiektowe 2016/2017. Wykład 10: Tworzenie projektowego diagramu klas

Wprowadzenie do Doctrine ORM

REFERAT PRACY DYPLOMOWEJ Temat pracy: Projekt i realizacja serwisu ogłoszeń z inteligentną wyszukiwarką

Zarządzenie Rektora Politechniki Gdańskiej nr 1/2014 z 16 stycznia 2014 r.

Zadanie polega na stworzeniu bazy danych w pamięci zapewniającej efektywny dostęp do danych baza osób.

REFERAT O PRACY DYPLOMOWEJ

Międzyplatformowy interfejs systemu FOLANessus wykonany przy użyciu biblioteki Qt4

Komputerowe Systemy Przemysłowe: Modelowanie - UML. Arkadiusz Banasik arkadiusz.banasik@polsl.pl

Produktywne tworzenie aplikacji webowych z wykorzystaniem Groovy i

Inżynieria oprogramowania II

OŚWIADCZENIE. Ja, niżej podpisany/a (imię i nazwisko autora pracy dyplomowej) Numer albumu: Kierunek studiów:

Projektowanie systemów informatycznych. Roman Simiński siminskionline.pl. Modelowanie danych Diagramy ERD

Baza danych sql. 1. Wprowadzenie. 2. Repozytaria generyczne

Analiza i projektowanie aplikacji Java

Projektowanie obiektowe oprogramowania Wzorce architektury aplikacji (3) Wykład 11 Repository, Unit of Work Wiktor Zychla 2016

Inżynieria wymagań. Wykład 2 Proces pisania przypadków użycia. Część 6 Wskazówki i sugestie

Wprowadzenie do Behaviordriven

Programowanie Komponentowe WebAPI

Diagramu Związków Encji - CELE. Diagram Związków Encji - CHARAKTERYSTYKA. Diagram Związków Encji - Podstawowe bloki składowe i reguły konstrukcji

Bazy danych TERMINOLOGIA

Dokumentacja wstępna TIN. Rozproszone repozytorium oparte o WebDAV

Pojęcie bazy danych. Funkcje i możliwości.

Bazy danych 2. Wykład 1

Plan. Wprowadzenie. Co to jest APEX? Wprowadzenie. Administracja obszarem roboczym

Procedura sprawdzania za pomocą programu antyplagiatowego prac dyplomowych inżynierskich/magisterskich realizowanych na Wydziale Chemicznym PRz

Informacje wstępne Autor Zofia Kruczkiewicz Wzorce oprogramowania 4

Usługi analityczne budowa kostki analitycznej Część pierwsza.

Sprawdzenie i ocena pracy z wykorzystaniem Archiwum Prac Dyplomowych

Laboratorium z przedmiotu Programowanie obiektowe - zestaw 04

Podstawowe pojęcia dotyczące relacyjnych baz danych. mgr inż. Krzysztof Szałajko

A Zasady współpracy. Ocena rozwiązań punktów punktów punktów punktów punktów

Technologie dla aplikacji klasy enterprise. Wprowadzenie. Marek Wojciechowski

Informatyka I. Standard JDBC Programowanie aplikacji bazodanowych w języku Java

Dzisiejszy wykład. Wzorce projektowe. Visitor Client-Server Factory Singleton

Technologia informacyjna

Projektowani Systemów Inf.

Implementacja Domain Driven Design - wzorce architektoniczne (część


Galileo - encyklopedia internetowa Plan testów

Wprowadzenie do programowania aplikacji mobilnych

REFERAT PRACY DYPLOMOWEJ

WOJSKOWA AKADEMIA TECHNICZNA

Spis wzorców. Działania użytkownika Strona 147 Obsługa większości Działań użytkownika za pomocą kodu JavaScript przy użyciu metod obsługi zdarzeń.

Systemy GIS Systemy baz danych

Plan. Formularz i jego typy. Tworzenie formularza. Co to jest formularz? Typy formularzy Tworzenie prostego formularza Budowa prostego formularza

Instrukcja laboratoryjna

Programowanie obiektowe

LK1: Wprowadzenie do MS Access Zakładanie bazy danych i tworzenie interfejsu użytkownika

Problemy optymalizacji, rozbudowy i integracji systemu Edu wspomagającego e-nauczanie i e-uczenie się w PJWSTK

Zarządzenie Nr 1/03/2019 Rektora Staropolskiej Szkoły Wyższej w Kielcach z dnia 10 marca 2019 r.

Program szkolenia: REST i Microservices w PHP

Microsoft Access materiały pomocnicze do ćwiczeń cz. 1

Diagramy związków encji. Laboratorium. Akademia Morska w Gdyni

Szczególne problemy projektowania aplikacji internetowych. Jarosław Kuchta Projektowanie Aplikacji Internetowych

WOJSKOWA AKADEMIA TECHNICZNA im. Jarosława Dąbrowskiego

Konspekt do lekcji informatyki dla klasy II gimnazjum. TEMAT(1): Baza danych w programie Microsoft Access.

E-commerce. Genialnie proste tworzenie serwisów w PHP i MySQL.

Wykład I. Wprowadzenie do baz danych

Dokument Detaliczny Projektu Temat: Księgarnia On-line Bukstor

Diagramy ERD. Model struktury danych jest najczęściej tworzony z wykorzystaniem diagramów pojęciowych (konceptualnych). Najpopularniejszym

Cel przedmiotu. Wymagania wstępne w zakresie wiedzy, umiejętności i innych kompetencji 1 Język angielski 2 Inżynieria oprogramowania

Webowy generator wykresów wykorzystujący program gnuplot

Zaawansowane programowanie obiektowe - wykład 5

Instrukcja 3 Laboratoria 3, 4 Specyfikacja wymagań funkcjonalnych za pomocą diagramu przypadków użycia

Zacznijmy więc pracę z repozytorium. Pierwsza konieczna rzecz do rozpoczęcia pracy z repozytorium, to zalogowanie się w serwisie:

REQB POZIOM PODSTAWOWY PRZYKŁADOWY EGZAMIN

Wzorce logiki dziedziny

WPROWADZANIE ZLECEŃ POPRZEZ STRONĘ INSTRUKCJA UŻYTKOWNIKA

Baza danych. Baza danych to:

Tworzenie bazy danych na przykładzie Access

Wykład 1 Inżynieria Oprogramowania

Laboratorium 7 Blog: dodawanie i edycja wpisów

Technologie Internetowe Raport z wykonanego projektu Temat: Internetowy sklep elektroniczny

5.3. Tabele. Tworzenie tabeli. Tworzenie tabeli z widoku projektu. Rozdział III Tworzenie i modyfikacja tabel

Uzupełnij pola tabeli zgodnie z przykładem poniżej,

Modelowanie diagramów klas w języku UML. Łukasz Gorzel @stud.umk.pl 7 marca 2014

Projektowanie oprogramowania cd. Projektowanie oprogramowania cd. 1/34

WPROWADZENIE DO BAZ DANYCH

4 Web Forms i ASP.NET Web Forms Programowanie Web Forms Możliwości Web Forms Przetwarzanie Web Forms...152

Programowanie obiektowe

Procesowa specyfikacja systemów IT

Instrukcja Użytkownika (Studenta) Systemu Obsługującego Lokalne Archiwum Dokumentów

Transkrypt:

Wydział Elektroniki, Telekomunikacji i Informatyki Politechniki Gdańskiej Imię i nazwisko: Tomasz Węgiel Nr albumu: 163722 Studia drugiego stopnia Forma studiów: niestacjonarne Kierunek studiów: Informatyka Specjalność/profil: Systemy i technologie mobilne PRACA DYPLOMOWA MAGISTERSKA Tytuł pracy w języku polskim: Wzorce CQRS i Event Sourcing w tworzeniu aplikacji biznesowych opartych na zdarzeniach Tytuł pracy w języku angielskim: CQRS and Event Sourcing patters in event-based business applications Potwierdzenie przyjęcia pracy Opiekun pracy Kierownik Katedry/Zakładu podpis dr inż. Łukasz Kuszner podpis prof. dr hab. inż. Krzysztof Giaro Data oddania pracy do dziekanatu:

Wydział Elektroniki, Telekomunikacji i Informatyki Politechniki Gdańskiej OŚWIADCZENIE Imię i nazwisko: Tomasz Węgiel Data i miejsce urodzenia: 19.02.1992 r., Słupsk Nr albumu: 163722 Wydział: Elektroniki, Telekomunikacji i Informatyki Politechniki Gdańskiej Kierunek: Informatyka Poziom studiów: drugi Forma studiów: niestacjonarne Ja, niżej podpisany, wyrażam zgodę na korzystanie z mojej pracy dyplomowej zatytułowanej: Wzorce CQRS i Event Sourcing w tworzeniu aplikacji biznesowych opartych na zdarzeniach do celów naukowych i dydaktycznych. 1 Gdańsk, dnia 29.07.2017 r.. podpis studenta Świadomy(a) odpowiedzialności karnej z tytułu naruszenia przepisów ustawy z dnia 4 lutego 1994 r. o prawie autorskim i prawach pokrewnych (Dz. U. z 2006 r., nr 90, poz. 631) i konsekwencji dyscyplinarnych określonych w ustawie Prawo o szkolnictwie wyższym (Dz. U. z 2012 r., poz. 572 z późn. zm.), 2 a także odpowiedzialności cywilno-prawnej oświadczam, że przedkładana praca dyplomowa została opracowana przeze mnie samodzielnie. Niniejsza praca dyplomowa nie była wcześniej podstawą żadnej innej urzędowej procedury związanej z nadaniem tytułu zawodowego. Wszystkie informacje umieszczone w ww. pracy dyplomowej, uzyskane ze źródeł pisanych i elektronicznych, zostały udokumentowane w wykazie literatury odpowiednimi odnośnikami zgodnie z art. 34 ustawy o prawie autorskim i prawach pokrewnych. Potwierdzam zgodność niniejszej wersji pracy dyplomowej z załączoną wersją elektroniczną. Gdańsk, dnia 29.07.2017 r.. podpis studenta Upoważniam Politechnikę Gdańską do umieszczenia ww. pracy dyplomowej w wersji elektronicznej w otwartym, cyfrowym repozytorium instytucjonalnym Politechniki Gdańskiej oraz poddawania jej procesom weryfikacji i ochrony przed przywłaszczaniem jej autorstwa. Gdańsk, dnia 29.07.2017 r.. podpis studenta 1 Zarządzenie Rektora Politechniki Gdańskiej nr 34/2009 z 9 listopada 2009 r., załącznik nr 8 do instrukcji archiwalnej PG. 2 Ustawa z dnia 27 lipca 2005 r. Prawo o szkolnictwie wyższym: Art. 214 ustęp 4. W razie podejrzenia popełnienia przez studenta czynu polegającego na przypisaniu sobie autorstwa istotnego fragmentu lub innych elementów cudzego utworu rektor niezwłocznie poleca przeprowadzenie postępowania wyjaśniającego. Art. 214 ustęp 6. Jeżeli w wyniku postępowania wyjaśniającego zebrany materiał potwierdza popełnienie czynu, o którym mowa w ust.4, rektor wstrzymuje postępowanie o nadanie tytułu zawodowego do czasu wydania orzeczenia przez komisję dyscyplinarną oraz składa zawiadomienie o popełnieniu przestępstwa.

STRESZCZENIE inż. Tomasz Węgiel, Praca magisterska Politechnika Gdańska Temat pracy: Wzorce CQRS i Event Sourcing w tworzeniu aplikacji biznesowych opartych na zdarzeniach Celem pracy jest przedstawienie wzorców CQRS i Event Sourcing oraz ich praktyczne wykorzystanie w wybranej aplikacji. Część pierwsza pracy zawiera opis teoretyczny. Przedstawiono zagadnienia dotyczące wzorców taktycznych i strategicznych podejścia Domain-Driven Design. Ukazano wzorzec Command-Query Responsibility Segregation (CQRS), dzielący pojedynczy model na stronę zapisującą oraz odczytującą. Zwrócono uwagę na spójność danych i zagadnienia współpracy wielu użytkowników. Opisano wzorzec Event Sourcing skupiający się na zapisywaniu zdarzeń w bazie danych, pozwalając na oddzielenie modelu domenowego od modelu danych. W części drugiej skupiono się na projekcie i implementacji dwóch przykładowych systemów, gdzie jeden wykorzystuje wzorce CQRS oraz Event Sourcing. Udokumentowano oraz zaprezentowano architektury obu aplikacji. Stanowiło to podstawę do potwierdzenia tezy, że dwa wzorce ułatwiają tworzenie poddomeny. W części trzeciej wykorzystano stworzone systemy do zbadania ich wydajności. Utworzono testy wydajnościowe, których wyniki były podstawą do stwierdzenia przydatności obu rozwiązań w zależności od liczby użytkowników korzystających z systemu. Słowa kluczowe: Informatyka, Systemy informatyczne, CQRS, Event Sourcing, DDD 3

ABSTRACT Engr. Tomasz Węgiel, Master s thesis Gdańsk University of Technology Subject: CQRS and Event Sourcing patters in event-based business applications This work aims to show CQRS and Event Sourcing patterns and their practical usage for a selected application. The first part of this work is theoretical, presenting various tactical and strategic patterns of Domain-Driven Design approach. It also discusses a Command-Query Responsibility Segregation (CQRS) pattern which splits a single model into a writing and a reading sides. Data cohesion and collaboration issue were noted. This part also presents an Event Sourcing pattern focused on recording events in a database, thus allowing for a separation of the domain model from the data model. The second part concentrates on the design and implementation of two example systems, one of which uses CQRS and Event Sourcing patterns. It documents and presents both applications in terms of their architecture, lending weight to the thesis that two patterns make it easier to create a subdomain. The third part examines the performance of the systems. Performance tests were created. The results of these tests have been used as a basis for deciding on the usefulness of either solution depending on the number of system users at a given time. Key words: Information technology, information systems, CQRS, Event Sourcing, DDD 4

SPIS TREŚCI Wykaz ważniejszych oznaczeń i skrótów... 7 1. Wstęp i cel pracy... 8 2. Domain-Driven Design... 10 2.1. Projektowanie ukierunkowane na domenę... 10 2.2. Model domenowy... 10 2.3. Wzorce strategiczne... 11 2.4. Wzorce taktyczne... 11 3. Command and Query Responsibility Segregation... 14 3.1. Architektura trójwarstwowa... 14 3.2. Interfejs oparty na zadaniach... 16 3.3. Command-Query Separation (CQS)... 18 3.4. Command-Query Responsibility Segregation... 19 3.5. Strona odczytująca... 20 3.6. Strona zapisująca... 22 3.7. Walidacja komend... 23 3.8. Spójność strony odczytującej - Eventual Consistency... 23 3.9. Współpraca wielu użytkowników... 24 4. Event Sourcing... 25 4.1. Znaczenie zdarzeń w systemie... 25 4.2. Zdarzenia w systemie... 25 4.3. Sposób działania... 26 4.4. Przechowywanie zdarzeń... 27 4.5. Odwracanie zdarzeń... 27 4.6. Systemy zewnętrzne... 28 4.7. Implementacja Event Storage... 28 4.8. Snapshots zrzuty aktualnego stanu agregatu... 30 4.9. Wersjonowanie zdarzeń... 31 4.10. Połączenie CQRS z Event Sourcingiem... 33 5. Projekt testowanego systemu... 34 5.1. Opis domeny... 34 5.2. Dekompozycja domeny... 34 5.3. Wszechobecny język... 35 5.4. Przypadki użycia... 36 5.5. Dwie architektury... 47 5.6. Zdarzenia w systemie... 49 6. Implementacja... 51 6.1. Wykorzystywane technologie i narzędzia... 51 6.2. Zależności w utworzonym rozwiązaniu... 52 6.3. Model obiektowy... 53 6.4. Model fizyczny bazy danych... 59 7. Testy wydajnościowe... 61 5

7.1. Przedmiot i cel badań... 61 7.2. Platforma testowa... 61 7.3. Metoda badania... 61 7.4. Wyniki badań - porównanie wydajności dwóch architektur... 62 Podsumowanie... 74 Wykaz literatury... 76 Wykaz rysunków... 78 Wykaz tabel... 79 6

WYKAZ WAŻNIEJSZYCH OZNACZEŃ I SKRÓTÓW DDD Domain-Driven Design DTO Data Transfer Object XML Extensible Markup Language JSON JavaScript Object Notation HTTP Hypertext Transfer Protocol REST Representational State Transfer SOAP Simple Object Access Protocol CRUD Create Read Update Delete CQS Command-Query Seperation CQRS Command Query Responsibility Segregation ERD Entity-Relationship Diagram 7

1. WSTĘP I CEL PRACY Tworzenie złożonych systemów wymaga uporządkowanego podejścia podczas budowy modelów dziedzinowych. Domain-Driven Design przychodzi z pomocą w postaci zestawu wzorców strategicznych i taktycznych. Jednakże ich zastosowanie wiąże się z problemami wydajnościowymi. Zauważono również, że tylko jeden model obiektowy lub danych nie wystarcza, aby sprostać złożonym wymaganiom dotyczącym ekspresji modelu obiektowego. Dynamiczny rozwój technologii i ogólny dostęp do Internetu powoduje, że coraz większą uwagę skupia się na zagadnieniach skalowalności i wydajności tworzonych systemów informatycznych. Duże przedsiębiorstwa handlowe szukają rozwiązań tego problemu w stosowaniu mikroserwisów oraz asynchronicznej komunikacji pomiędzy komponentami systemu. Coraz częściej koncentrują się na wykorzystywaniu sztucznej inteligencji w celu zwiększenia sprzedaży, poprzez analizę danych o działaniach użytkownika. W 2011 roku wyklarowała się idea rozdzielenia pojedynczego modelu na dwa osobne, służące do zapisu i odczytu, mające oddzielne źródła danych. Stworzony wzorzec projektowy nazwano CQRS Command Query Responsibility Segregation. Oba te modele mogą komunikować się asynchronicznie, zwiększając wydajność systemu. Następstwem tego podejścia jest niespójność danych pomiędzy tymi modelami. W ścisłej symbiozie z CQRS żyje wzorzec Event Sourcing. Polega on na zapisywaniu do bazy danych jedynie kolejnych zmian w modelu, które stają się dziennikiem wszystkich operacji wykonywanych przez użytkownika. Event Sourcing umożliwia całkowitą niezależność modelu domenowego od bazy danych. Oba te wzorce są wykorzystywane w systemach informatycznych klasy enterprise, w których dąży się do budowy modułowej, skalowalności, bezpieczeństwa oraz tworzenia kodu łatwiejszego do utrzymania i długoletniego wsparcia. Wybór tematu pracy podyktowany był jego aktualnością, własnymi zainteresowaniami związanymi z tematyką inżynierii oprogramowania, a także potrzebą poszukiwania rozwiązań w zakresie problemu niezgodności impedancji modelu bazy danych oraz modelu obiektowego. Podczas badań powyższej tematyki zastosowano metody teoretyczne, dokonując między innymi analizy dostępnej literatury, czasopism, stron internetowych będących źródłem informacji o wzorcach CQRS i Event Sourcing. Ponadto stosowano metodę empiryczną tworząc system informatyczny wykorzystujący te wzorce. Zastosowano porównanie wydajności architektury korzystającej z CQRS i Event Sorucing z architekturą nie korzystającą z tych wzorców. Stąd zasadniczym celem pracy dyplomowej jest przedstawienie wzorców CQRS i Event Sourcing oraz implementacja systemu wykorzystującego oba wzorce. Postawiono tezę, że używanie tych dwóch wzorców sprawi, iż implementacja subdomeny systemu będzie łatwiejsza, a system stanie się wydajniejszy. Struktura pracy obejmuje 7 rozdziałów, gdzie pierwszy jest wstępem. 8

Rozdział drugi dotyczy zagadnień Domain-Driven Design. Ukazuje znaczenie modelu domenowego w systemie informatycznym oraz opisuje wzorce strategiczne i taktyczne, które pozwalają na jego poprawne stworzenie. Rozdział trzeci skupia się na zagadnieniach dotyczących CQRS. Wyszczególniono w nim problemy, które zachodzą w architekturze trójwarstwowej, a także pokazano sposób tworzenia interfejsu użytkownika opartego na zadaniach. Przedstawiono wzorzec CQRS z podziałem na stronę odczytującą i zapisującą. Wprowadzono również pojęcie Eventual Consistency. Rozdział czwarty poświęcono wzorcu Event Sourcing oraz znaczeniu zdarzeń w systemie informatycznym. Przedstawiono rozwiązanie problemów zachodzących podczas tworzenia systemu korzystającego z tego wzorca. Zademonstrowano model bazy danych przetrzymującej zdarzenia i zalety płynące z połączenia wzorca CQRS z Event Sourcing. W rozdziale piątym przedstawiono projekt przykładowego systemu. Zawarto w nim opis domeny, jej dekompozycję, wszechobecny język, przypadki użycia i zdarzenia w systemie. Zaprezentowano dwie architektury stworzonego systemu. Rozdział szósty dotyczy kwestii implementacyjnych dwóch architektur utworzonego systemu. Umieszczono opis wykorzystywanych technologii, zależności między bibliotekami. Przedstawiono diagramy klas oraz modele fizyczne baz danych. Rozdział siódmy prezentuje sposób testowania obu architektur i przedstawia wyniki testów wydajnościowych. Praca zakończona została wnioskami podsumowującymi ocenę obu wzorców. 9

2. DOMAIN-DRIVEN DESIGN 2.1. Projektowanie ukierunkowane na domenę Według raportu grupy Standish, 19% projektów informatycznych w 2015 r. zakończyło się fiaskiem [1]. Jedną z głównych przyczyn tych niepowodzeń była złożoność tworzonych projektów, która często nie jest związana z problemami technicznymi, lecz z samą domeną. Domena w ujęciu słownikowym to obszar czyjejś głębszej wiedzy lub dziedzina działalności [2]. Tworzenie oprogramowania, które jest specyficzne dla danej domeny wiąże się ze zrozumieniem problemu oraz stawianych wymagań [3]. Głównym celem Domain-Driven Design (DDD) jest tworzenie oprogramowania klasy enterprise w sposób iteracyjny kładąc szczególny nacisk na definiowanie obiektów i komponentów systemu oraz ich zachowań, aby wiernie odzwierciedlały rzeczywistość. Samo DDD nie należy do żadnej metodologii lub technologii, ale jest to sposób myślenia, zestaw technik i koncepcji oraz zbiór priorytetów, które pomagają poradzić sobie ze złożoną domeną [4]. Jest to możliwe dzięki ścisłej współpracy programistów, analityków oraz ekspertów domenowych, a więc osób prezentujących najwyższy poziom kompetencji w specjalistycznej pracy, zadaniu lub umiejętności [5]. 2.2. Model domenowy Produktem ścisłej pracy zespołu jest model domenowy, który jest zrozumiały nie tylko przez programistów, lecz również przez analityków oraz ekspertów domenowych. Według M. Fowlera model domenowy jest to taki model obiektowy domeny, który łączy w sobie zarówno zachowania jak i dane [6]. Model ten staje się centralną bazą wiedzy. Wywiązywane dyskusje podczas wspólnej pracy sprawiają, że sami eksperci mogą dowiedzieć się czegoś nowego o procesach zachodzących w ich instytucjach, o których wcześniej nie mieli pojęcia. Techniki DDD pozwalają na wykształcenie wspólnego języka, który jest zrozumiały przez każdą ze stron. Model domenowy jest rozbudowywany i uszczegóławiany wraz ze zdobywaniem wiedzy. Jest bogaty w dane, reguły biznesowe i zachowania. Zachowana jest enkapsulacja. Sam model nie skupia się na szczegółach technologicznych, między innymi na używanych bibliotekach, frameworkach czy architekturze. Tak stworzony kod staje się testowalny oraz łatwiejszy w utrzymaniu i modyfikacji [7]. Domain-Driven Design rozróżnia dwa typy wzorców, które pozwalają na powstanie takiego oprogramowania wzorce strategiczne oraz taktyczne. Oba wzorce są komplementarne, tzn. uzupełniają się wzajemnie. Wzorce strategiczne odnoszą się przede wszystkim do rozdzielenia tworzonego systemu na mniejsze części, dlatego zostaną omówione jako pierwsze. Natomiast jako drugie zostaną omówione wzorce taktyczne, które odnoszą się do szczegółów implementacyjnych tworzonych podsystemów. 10

2.3. Wzorce strategiczne Wzorce strategiczne (ang. strategic patterns) odnoszą się do relacji z ekspertem domenowym. Pokazują w jaki sposób należy prowadzić sesje modelowania na których tworzy się model domenowy oraz identyfikuje się reguły biznesowe [8]. Dzięki rozmowom tworzy się tzw. wszechobecny język (ang. Ubiquitous Language) pozwalający na zdefiniowanie pojęć w sposób jednoznaczny [9]. Język ten nazywany jest wszechobecnym, ponieważ jest używany nie tylko w powstałym kodzie, lecz również podczas wszystkich rozmów, a także w tworzonej dokumentacji. Stworzenie takiego języka pozwala uniknąć sytuacji, w których jedna ze stron, np. programista, ma w głowie inną definicję pewnego obiektu lub operacji niż inna osoba w tym samym zespole [4]. Wzorce strategiczne określają też tzw. konteksty związane (ang. Bounded Context) [9]. Pozwalają one na określenie szczegółowych granic stworzonych definicji. Stworzone definicje w określonym kontekście związanym są od siebie niezależne oraz hermetyczne. W jednym kontekście związanym definicja klienta może być inna niż w drugim. Definicja w jednym kontekście nie może wpływać na drugą. Każdy kontekst związany powinien posiadać osobny model. Konteksty mogą być rozwijane autonomicznie przez różne zespoły [10]. Po określeniu poszczególnych kontekstów związanych tworzy się mapę kontekstów (ang. context map). Na mapie kontekstów określone są poszczególne konteksty związane oraz komunikacja między nimi. Mapa kontekstów pozwala na określenie domeny głównej (ang. core domain), poddomen wspierających (ang. supporting sub-domains), poddomen ogólnych (ang. generic sub-domains) oraz systemów zewnętrznych. Ponadto można zidentyfikować, która poddomena będzie korzystała ze wzorców taktycznych DDD [11]. Dziedzina główna jest to część tworzonego systemu ściśle powiązana ze strategią firmy. Implementacja dziedziny głównej jest kluczowa, gdyż dzięki niej firma będzie bardziej konkurencyjna na rynku. To ona będzie w głównej mierze generowała zyski [12]. Poddomena wspierająca to domena, która generuje jakieś zyski finansowe, bez której przedsiębiorstwo nie może istnieć. Natomiast domena ogólna nie generuje żadnego zysku finansowego dla przedsiębiorstwa. Przykładami takich podsystemów jest podsystem zarządzania ludźmi lub mieniem. Poddomena ta przeważnie nie jest specyficzna dla danego przedsiębiorstwa, dlatego można zakupić już gotowe systemy [12]. 2.4. Wzorce taktyczne Wzorce taktyczne (ang. tactical patterns) polegają na rozwiązywaniu typowych problemów technicznych podczas budowy modelu domenowego. Pierwszą składową tych wzorców są encje (ang. entities). Encje to klasy, które zawierają atrybuty oraz powiązane z nimi operacje. Ich cechą charakterystyczną jest to, że można je rozróżniać na podstawie identyfikatorów. To w nich znajduje się logika biznesowa, a więc implementacja pewnego procesu biznesowego i reguł nim rządzących. Modelowanie encji skupia się przede wszystkim na 11

zachowaniu enkapsulacji stanu. Stan encji nie powinien być zmieniany bezpośrednio, lecz za pomocą metod [4, 12]. Wzorce taktyczne definiują również obiekty wartościowe (ang. value objects). Są to klasy, które pozwalają na zamknięcie logiki odnoszącej się do kilku pól, które są ze sobą ściśle związane. W przeciwieństwie do encji nie zawierają identyfikatorów, dlatego rozróżnialne są na podstawie wartości ich pól [12]. Obiekty wartościowe są niezmienne, czyli po ich stworzeniu nie można zmienić ich stanu. Przykładowymi klasami mogą być klasy Adres lub Pieniądze [4]. W zachowaniu enkapsulacji pomagają agregaty (ang. agreggates). Agregaty enkapsulują obiekty domenowe należące do poszczególnych kontekstów związanych i zawierają w sobie powiązane encje oraz obiekty wartościowe. Dzięki nim możemy traktować drzewo takich obiektów jako jeden obiekt [4]. Encją nadrzędną w danym agregacie nazywa się korzeniem agregatu (ang. aggregate root). Korzeń agregatu będzie składał się z pól oraz metod jak i innych encji lub obiektów wartościowych, dlatego jest on odpowiedzialny za utrzymanie integralności danych w tym agregacie. Chcąc zmienić stan dowolnej encji lub obiektu wartościowego należy wywołać metodę korzenia agregatu. Dzięki takiemu zabiegowi możemy traktować cały agregat jak jeden pojedynczy obiekt. Identyfikacja korzenia agregatu jest zależna od problemu jaki chce się rozwiązać, ale często dobrą wskazówką, aby wskazać korzeń agregatu jest relacja jeden do wielu [12]. Przykładem agregatu jest klasa Zamówienie, która zawiera listę znajdujących się w niej produktów. W tym przypadku zamówienie jest encją nadrzędną (korzeniem agregatu), gdyż zamówione produkty są integralną częścią zamówienia i nie mogą bez niej istnieć. Mimo, iż obie te encje mogłyby istnieć niezależnie, powinno się je traktować jako jeden obiekt [13]. Każda zmiana stanu produktu w zamówieniu powinna być wywoływana przez korzeń agregatu, a referencje do encji znajdujących się w korzeniu agregatu nie powinny być dostępne spoza agregatu, aby nie dopuścić do sytuacji, w której manipulacja obiektem podrzędnym mogłaby wpłynąć na integralność całego agregatu [12]. Agregaty ładowane są z bazy danych za pomocą repozytorium (ang. repository). Jest ono odpowiedzialne za prawidłowe zbudowanie, zapis i usunięcie danego agregatu. W nomenklaturze DDD istnieje pojęcie serwisu domenowego (ang. domain service), który ma za zadanie koordynować pracę pomiędzy dwoma lub więcej agregatami. Zawiera on również logikę, która nie pasuje do żadnego agregatu [4, 11]. W Domain-Driven Design dużą uwagę skupia się na zdefiniowaniu zdarzeń biznesowych (ang. domain events). Są to obiekty w systemie nie zawierające żadnej logiki biznesowej, a jedynie informacje dotyczące zmian zachodzących w systemie. Taki obiekt będzie wysyłany do innych kontekstów związanych, aby zareagować na to co się wydarzyło w innej części systemu i jest ściśle związany ze zmianą stanu obiektu domenowego. Zdarzenia te są wywoływane po dokonaniu jakiejś operacji na agregacie lub po wykonaniu metody z serwisu domenowego. Dzięki zdarzeniom można wpłynąć na stan agregatu będącego w innym kontekście związanym sprawiając, że poszczególne konteksty nie będą ze sobą ściśle związane [14]. 12

Wszystkie wzorce taktyczne można stosować bez użycia wzorców strategicznych. Jest to tak zwany DDD-Light, a więc lekka wersja DDD [12]. Przykładowy agregat wykorzystujący wzorce taktyczne został przedstawiony na Rys. 2.1. Klasa Koszyk jest encją (jest rozróżnialna za pomocą identyfikatora), która jest jednocześnie korzeniem agregatu, więc przetrzymuje referencje do obiektów Produkt, jak również do obiektu wartościowego Cena. Koszyk dba o zachowanie wszystkich reguł agregatu np. w koszyku nie może być więcej niż 15 produktów, a ciężar wszystkich przedmiotów nie może przekraczać 5kg. Stan innych encji lub obiektów wartościowych mu podległych może być zmieniony jedynie wywołując metodę na obiekcie typu Koszyk". Dodając produkt do koszyka generowane jest zdarzenie DodanoProdukt. Z klasy Koszyk korzysta repozytorium, które dba o zapis i odczytywanie stanu koszyka z bazy danych, oraz serwis domenowy, który może zawierać logikę tworzenia agregatu zamówienia z obiektu koszyka. Rys. 2.1 Przykładowy agregat koszyk na zakupy 13

3. COMMAND AND QUERY RESPONSIBILITY SEGREGATION 3.1. Architektura trójwarstwowa Zanim zostanie omówiona architektura projektów opartych na zasadach Domain-Driven Design, która umożliwia zastosowanie wzorca CQRS, warto wspomnieć o architekturze trójwarstwowej (Rys. 3.1), ponieważ wiąże się z problemami wydajnościowym i kontrastuje z architekturą korzystającą z CQRS. Architektura trójwarstwowa rozdziela od siebie dane, logikę oraz warstwę prezentacji [15]. Przedstawiony przykład odnosi się również do architektur N-warstwowych, w których mogą zostać wprowadzone dodatkowe warstwy. Interakcja klienta (lub klientów) z serwerem odbywa się za pomocą wysyłanych obiektów Data Transfer Object (DTO). Są to obiekty w postaci zserializowanej (XML, JSON), które zawierają jedynie właściwości z danymi. Klient wysyła żądanie do serwera. Serwer ładuje stan potrzebnych obiektów z bazy danych, przetwarza je i zapisuje z powrotem do bazy danych. Następnie mapuje obiekt reprezentujący stan bazy danych znowu na obiekt DTO. Taki obiekt jest zwracany z powrotem do klienta, który wyświetla je na ekranie w celu dalszej interakcji z użytkownikiem [16, 17]. Po stronie serwerowej głównym elementem jest jednostka przechowywująca dane. Może być to dowolna baza danych np. relacyjna lub obiektowa. Poniżej bazy danych znajduje się serwer aplikacyjny. Składa się on z warstw: serwisy aplikacyjne, walidujące i przetwarzające przychodzące obiekty DTO na obiekty domenowe oraz wywołujące logikę biznesową z kolejnej warstwy, warstwa logiki biznesowej, warstwa dostępu do danych, przetwarzająca stan obiektów domenowych na ich reprezentację w bazie danych. Główną zaletą korzystania z takiej architektury jest jej prostota. Nowa osoba, która zaczyna pracę z istniejącym projektem korzystającym z takiej architektury bardzo szybko może się w nią wdrożyć, a tym samym zmniejszyć koszt całego przedsięwzięcia [18]. Ponadto na rynku znajduje się wiele narzędzi wspomagających tworzenie takich rozwiązań. Jednym z nich jest mapowanie relacyjno-obiektowe, czyli technika pozwalająca na konwertowanie danych z systemu obiektowego (aplikacji) na dane w systemie relacyjnym (bazie danych). Kolejnym przykładem mogą być narzędzia pozwalające na automatyczne mapowanie obiektów biznesowych w obiekty DTO. 14

Serwer aplikacyjny BAZA DANYCH Warstwa dostępu do danych Warstwa logiki biznesowej Serwisy aplikacyjne CZĘŚĆ SERWEROWA CZĘŚĆ KLIENCKA Wysłane DTO INTERNET Odebrane DTO Aplikacja kliencka Rys. 3.1 Typowa architektura systemu Słabą stroną takiej architektury jest brak możliwości zastosowania podejścia Domain- Driven Design bowiem wspiera ono bardziej anemiczny model domenowy (ang. anemic domain model) oraz podejście CRUD (Create, Read, Update, Delete) [16]. Anemiczny model domenowy jest to taki model, który nie zachowuje enkapsulacji, a więc wszystkie jego właściwości są publiczne oraz nie zawierają żadnych metod. Jest to spowodowane zastosowaniem mapowania obiektów domenowych na obiekty DTO (i na odwrót) [12]. Podejście CRUD odnosi się do podstawowych operacji wykonywanych na bazie danych tj. utworzenie, odczyt, zaktualizowanie oraz usunięcie rekordu. Powyższe problemy sprawiają, że logika biznesowa nie jest zawarta w spójnym modelu. Użytkownik nie widzi zachowania systemu. Edytuje jedynie rekordy w bazie danych. Cała logika biznesowa systemu znajduje się w jego głowie musi wiedzieć co utworzyć i edytować, aby wykonać znany scenariusz biznesowy [16]. 15

Występuje również problem ze skalowaniem takiego rozwiązania. Elementem ograniczającym pracę całego systemu jest baza danych, która używana jest przez wszystkich użytkowników systemu. Zwiększając wydajność sprzętu (tzw. skalowanie pionowe) nie zyskuje się zwiększonej wydajności systemu. To samo tyczy się skalowania systemu w poziomie dodatkowy komputer nadal będzie korzystał tylko z jednego źródła danych [18]. 3.2. Interfejs oparty na zadaniach Architektura opisana w powyższym rozdziale cechuje się prostym interfejsem użytkownika, który można zdefiniować jako odzwierciedlenie bazy danych. Zdefiniowane są operacje takie jak: utwórz, edytuj, odczyt, usuń. Wysyłane DTO nie przekazują żadnej intencji. Najważniejszą składową takiej architektury jest struktura bazy danych a nie logika biznesowa. Niekiedy zastosowanie takiego rozwiązania jest uzasadnione, gdyż klient nie potrzebuje żadnej logiki biznesowej lub budżet nie pozwala na zrobienie czegoś bardziej wysublimowanego [16]. Zmiana interfejsu użytkownika na pokazujący co można zrobić w systemie (np. przycisk dodaj do koszyka lub złóż zamówienie ) niesie za sobą sposób w jaki modeluje się logikę biznesową oraz sposób korzystania z serwera aplikacyjnego. Klient korzystający z takiego systemu musi wysłać wraz z danymi informację o tym co chce z nimi zrobić. Wysyłanie takich informacji jest tzw. komendą (ang. command). Jest to obiekt, który zawiera w sobie nazwę operacji oraz dane, które są niezbędne do jej wykonania. Ważne, aby nazwa komendy była w trybie rozkazującym w czasie teraźniejszym i powinna określać działanie. Taka forma nazewnictwa komendy mówi o tym, że może być odrzucona przez serwer. Odróżnia ją także od zdarzeń domenowych, które zapisywane są w czasie przeszłym i odzwierciedlają dokonane działania [16, 19]. public class AddItemToCartCommand { public Product Product { get; private set; } public Guid CartId { get; private set; } } public AddItemToCartCommand(Guid cartid, Product product) { this.product = product; this.cartid = cartid; } Listing 3.1 Przykładowa komenda dodająca przedmiot do koszyka Komenda przedstawiona na Listingu 3.1 zawiera w swojej nazwie operację dodawania przedmiotu do koszyka oraz dane, które są potrzebne do wykonania tej operacji unikalny identyfikator koszyka oraz dodawany produkt. Należy również zwrócić uwagę, iż jest to odmienne od wcześniej przedstawionego obiektu DTO, w którym wysyłany był stan całego obiektu koszyka. 16

Użycie komend niesie ze sobą zmianę sposobu projektowania interfejsu użytkownika. Należy zbudować go w taki sposób, aby użytkownik wysyłał komendy przedstawiające jego intencje. Przykładem takiego interfejsu użytkownika jest interfejs oparty na zadaniach (ang. Task Based User Interface). Polega on na odkryciu w jaki sposób odbiorcy systemu chcieliby z niego korzystać oraz na pokierowaniu użytkowników przez cały proces [17, 20]. Przeciwieństwem takiego podejścia jest dedukcyjny interfejs użytkownika bazujący na jego wiedzy, a zatem sposobu w jaki użytkownik nauczył się systemu. Stwarza on kilka problemów: zakłada, że użytkownik stworzy pamięciowy model systemu, zakłada, że użytkownik nauczy się systemu i z biegiem czasu nauczy się procesów biznesowych, użytkownik ma problem ze zrozumieniem każdej opcji na ekranie [16]. Rys. 3.2 Przykład interfejsu opartego na podejściu CRUD Przykładem źle stworzonego interfejsu jest formularz edycji produktu z możliwością dezaktywacji. Źle zaprojektowany interfejs będzie zawierał w sobie listę wybieralną ze statusem. Po mianie takiej kontrolki będzie musiał wpisać dodatkowo w pole tekstowe powód dezaktywacji. Przykład takiego interfejsu pokazany jest na Rys. 3.2. W takim przypadku interfejs oparty na zadaniach powinien pokazać przycisk dezaktywacji, po kliknięciu którego pokaże się pole tekstowe do wpisania powodu. Zmiana jaka nastąpiła jest ukazana na Rys. 3.3. Rys. 3.3 Przykład interfejsu opartego na zadaniach 17

W tym przypadku sprowadza się to do wykonania jednego, odseparowanego od reszty przypadku użycia. Sprawia to, że użytkownik doznaje lepszego doświadczenia z pracą z systemem. Staje się on dla niego intuicyjny i widzi procesy w nim zachodzące [16]. 3.3. Command-Query Separation (CQS) W roku 1986 Bertrand Mayer opisał zasadę konstrukcji metod, która polega na rozdzieleniu ich na komendę (ang. command) lub zapytanie (ang. query). Komenda powinna zmieniać stan obiektu oraz nic nie zwracać tzn. zwracać typ void. Przeciwieństwem komendy jest zapytanie. Zapytanie nie powinno mieć efektów ubocznych, a więc nie powinno zmieniać stanu obiektu, za to musi coś zwracać. Podejście to nazwane zostało Command-Query Seperation (CQS). Dla przykładu, projektując klasę koszyka na zakupy należy unikać takich metod jak pokazanych na Listingu 3.2 [21]. public Product GetOrAddProductToCart(Product product) { Product productincart = productsincart.firstordefault(p => p.id == product.id); } if (productincart == null) { productsincart.add(product); return product; } return productincart; Listing 3.2 Metoda zmieniająca stan obiektu jak i odczytująca wartości Prawidłowymi z punktu widzenie CQS metodami byłyby operacje przedstawione na Listingu 3.3. public void AddProductToCart(Product product) { bool productexists = productsincart.any(p => p.id == product.id); if (productexists) return; } productsincart.add(product); public Product GetProductFromCart(int productid) { Product productincart = productsincart.firstordefault(p => p.id == productid); return productincart; } Listing 3.3 Rozdzielenie metody na komendę i zapytanie 18

Zastosowanie się do tych reguł podczas projektowania klas sprawia, iż można używać metod-zapytań w dowolny sposób bez martwienia się czy obiekt znajdzie się w innym stanie [22]. Command-Query Separation stał się dobrą praktyką, która stanowiła podstawę do utworzenia innego wzorca - CQRS. 3.4. Command-Query Responsibility Segregation Wzorzec Command-Query Responsibility Segregation (CQRS) idzie o krok dalej niż CQS, i według twórcy tego wzorca Grega Younga jest to stworzenie dwóch obiektów do zapisu i odczytu tam, gdzie wcześniej był tylko jeden [17]. Dodatkowo to przejście z poziomu komponentu do poziomu architektury [12]. W myśl tej idei architektura aplikacji jest dzielona na model do aktualizacji informacji oraz do ich odczytywania. Komendy przetwarzane przez stronę zapisującą generują zdarzenia, które z kolei są przetwarzane przez stronę odczytującą. Tak zapisane dane są używane do wyświetlania ich użytkownikowi oraz raportowania [23]. Według B. Meyera wzorcem określamy sposób rozwiązania pewnego znanego problemu pojawiającego się podczas tworzenia systemu informatycznego [24]. Command-Query Responsibility Segregation powinien być stosowany w obszarze jednego kontekstu związanego, dlatego też nie należy go używać jako głównej architektury systemu. Powinna być nią architektura oparta na zdarzeniach (ang. Event-driven architecture) lub architektura oparta o serwisy (ang. Service Oriented Architecture) [25]. W przeciwieństwie do architektury typowej aplikacji, architektura oparta na CQRS będzie składała się z dwóch części: strony odczytującej (ang. read side) i zapisujący (ang. write side). Części te możemy traktować w sposób autonomiczny posiadający swój własny model oraz osobne wymagania. Nie jest możliwe stworzenie rozwiązania, które w sposób optymalny realizuje wyszukiwanie, raportowanie oraz przetwarzanie, które korzystałoby tylko z jednego modelu [16]. Wzorzec CQRS w ujęciu wysokopoziomowym został przedstawiony na Rys. 3.4. Projektowanie obiektów, które jednocześnie zwracają jak i zapisują zmiany powodują nadmierne rozrastanie się modelu, a więc również jego złożoność. Jeśli logika biznesowa jest mieszana z logiką odczytu, jest dużo ciężej poradzić sobie z problemami takimi jak: wielu użytkowników pracujących na tym samym obiekcie, transakcyjność, współdzielenie danych, wydajność, spójność i aktualność danych. Oddzielenie tych dwóch modeli sprawia, że można każdy z tych problemów przydzielić do strony odczytującej lub zapisującej, zmniejszając przy tym złożoność rozwiązania. Takie rozdzielenie logiki biznesowej oraz strony odczytu sprawia, iż testowanie staje się dużo łatwiejsze [17]. 19

Rys. 3.4 Architektura CQRS pokazana w ujęciu wysokopoziomowym Wymaganiem systemu odnośnie bazy danych dla strony zapisującej jest to, aby była zapisana w relacyjnej bazie danych w trzeciej postaci normalnej. Jednak dla strony odczytującej baza danych powinna być jak najbardziej wydajna, a więc zapytania do bazy danych powinny być opatrzone w jak najmniejszą liczbą operacji złączenia (operacja JOIN w języku SQL). Analizując spójność danych, strona zapisująca powinna mieć aktualne dane. Strona odczytująca może przechowywać dane tymczasowo nie zsynchronizowane, a więc niespójne. Podział ten ma również swoje odzwierciedlenie w skalowalności. Strona odczytująca ma przeważnie większy narzut na ilość operacji, które musi wykonać w przeciwieństwie do strony zapisującej [16]. Dobrym odzwierciedleniem tego problemu są np. internetowe serwisy aukcyjne więcej osób przegląda przedmioty wystawione na sprzedaż niż je kupuje. 3.5. Strona odczytująca Strona odczytująca będzie miała za zadanie jedynie zwracać dane z źródła danych, a więc obiekty DTO, które następnie będą pokazywane na ekranie klienta. Problemami, które spotyka się w architekturze N-warstwowej korzystającej z DDD jest przede wszystkim złożoność zapytań odczytujących stan agregatów. Koszt zbudowania jednego agregatu jest bardzo duży, gdyż często zapytania wykonywane są na bazie danych w trzeciej postaci normalnej [12]. Większość dzisiejszych rozwiązań korzysta z jakiegoś rozwiązania ORM, które może sprawiać problemy z optymalizacją zapytań odczytujących wartości z bazy danych z wielu tabel, z wieloma złączeniami. Kolejnym problemem, może być sama natura interfejsów użytkownika znajdujących się po stronie klienta. Często wymagane jest stronicowanie lub sortowanie [16]. 20

Baza danych Cienka warstwa odczytująca dane INTERNET żądanie DTO Odpowiedź DTO Klient Rys. 3.5 Strona odczytująca Wszystkie te problemy można rozwiązać całkowicie uniezależniając się od modelu domenowego, usuwając go z całej ścieżki odczytującej. Przedstawione to zostało na Rys. 3.5. Po stronie serwerowej umieszczona została jedynie cienka warstwa odczytująca dane, która będzie mapowała dane z bazy danych wprost do obiektów DTO. Po stronie odczytującej nie jest wykonywana żadna operacja domenowa. Zawiera jedynie metody odczytujące, a więc może być utrzymywana przez zespół początkujących programistów. Jak zostało już wspomniane, baza danych służąca do odczytu będzie prawdopodobnie w pierwszej postaci normalnej, a więc pisanie optymalnych zapytań będzie dużo prostsze. Nie potrzeba również znajomości poznania pełnego modelu domenowego, a jedynie modelu danych [16]. Jednym z podejść do budowy bazy danych po stronie odczytu jest dążenie, aby poszczególne tabele w bazie danych były odzwierciedleniem jednego typu widoku w interfejsie użytkownika. Dla przykładu, gdy otrzymane zostanie zdarzenie stworzenia nowego produktu, jego dane zostaną wpisane do każdej tabeli, która wykorzystuje chociaż jedną z tych danych. 21

Dodatkowo możliwe jest zbudowanie tabel, które będą używane tylko przez jeden typ użytkowników, np. tylko przez administratorów, którzy powinni mieć wgląd do dodatkowych informacji. Jeśli spełnione jest takie założenie nie potrzebna jest relacyjna baza danych. Można ją zastąpić rozwiązaniem NoSQL, które dla takich przypadków powinno dać jeszcze większą wydajność [12]. 3.6. Strona zapisująca Strona zapisująca jest bardzo podobna do przedstawionej wcześniej typowej architektury. Różni się jedynie tym, że zamiast obiektów DTO trzymających stan obiektu używa się komend, które przekazują również intencje użytkownika. Dzięki takiemu zabiegowi możliwe jest zastosowanie podejścia Domain-Driven Design. Strona zapisująca nie ma żadnych metod odczytujących, które będą bezpośrednio zwracane do klienta. Powoduje to, że domena jest wyspecjalizowana jedynie w przetwarzaniu komend. Obiekty domenowe nie muszą wystawiać światu swoich stanów wewnętrznych zachowana jest enkapsulacja. Repozytoria zawierają teraz jedynie metody odpowiedzialne za pobieranie danych ze źródła jedynie po identyfikatorze, które są niezbędne do zbudowania agregatu [16]. Po otrzymaniu komendy przez stronę zapisującą następuje przetworzenie jej przez procedurę obsługi, tzw. handler [15]. Może być ona zaimplementowana w różny sposób. Pierwszym z nich jest nastawienie się na kategorie przychodzących komend. Każdy serwis aplikacyjny ma interfejs implementujący zachowania po odebraniu danego rodzaju komendy. Tak zbudowany handler ma wiele metod, gdzie każda z nich odpowiada jednej komendzie z danymi parametrami. Drugim sposobem jest stworzenie oddzielnej klasy z tylko jedną metodą, która w jej parametrach zawiera kontrakt, który musi zostać spełniony. Każda z tych klas ma tylko jedną odpowiedzialność wykonanie specyficznej komendy. Trzecim typem jest zbudowanie handlerów drugiego typu z tym, że komendy są przesyłane w sposób asynchroniczny z użyciem jakiegoś systemu komunikatów. Typ ten powinien być wykorzystywany, gdy wymaganiem dotyczącym systemu jest duża skalowalność. Ważne jest, aby handlery były od siebie niezależne, a więc nie powinny wywoływać metod innych handlerów. Późniejsze wykonanie zmian staje się bezpieczniejsze [12]. public interface ICommandHandler<TCommand> where TCommand : class, ICommand { void Handle(TCommand command); void Validate(TCommand command); } Listing 3.4 Interfejs handlera komendy Kod na Listingu 3.4 przedstawia przykładowy interfejs handlera, gdzie wykorzystanym typem generycznym jest klasa komendy. Ciało metody Handle() zawiera zainicjalizowanie agregatu za pomocą repozytorium, a następnie wykonanie na nim logiki biznesowej. Dzięki Domain-Driven Design cała logika biznesowa znajduje się w metodzie agregatu lub w wyspecjalizowanych serwisach domenowych. Można także do interfejsu dodać kolejną metodę 22

Validate(), która sprawdzałaby czy komenda ma wszystkie wymagane pola przed wykonaniem metody Handle(). Podczas przetwarzania handlera są generowane zdarzenia domenowe, które zawierają informacje zasilające stronę do odczytu. 3.7. Walidacja komend Dane podane przez użytkownika na wypełnionym formularzu powinny zostać walidowane, a więc sprawdzone czy nie zawierają nieprawidłowych danych lub formatów. Walidacja może być wykonana po stronie klienta (np. przeglądarki), jak również po stronie serwerowej. Taka walidacja sprawdza poprawność samej komendy, która następnie zostanie przesłana do wykonania. Odebrana komenda, która miała dobrze wypełnione dane wcale nie musi zostać przyjęta przez procesor komend, gdyż stan obiektu, na który wpływa mógł się zmienić w momencie wypełniania danych. Inna komenda mogła zostać odebrana i przyjęta, która sprawiła, że kolejna została odrzucona. Jeśli inna komenda nigdy by nie przyszła prawdopodobnie nie byłoby tego problemu i poprawna z punktu widzenia danych komenda zostałaby przyjęta. Z przedstawionych powyżej powodów ważne jest, aby komendy były możliwe jak najbardziej poprawne przed ich wysłaniem. Można to osiągnąć np. poprzez zapytania do bazy odczytującej przed wysłaniem komendy do handlera w celu sprawdzenia czy stan obiektu się nie zmienił [19]. Tworząc podsystem oparty na CQRS trzeba wziąć pod uwagę również fakt, iż komendy które wykonują się asynchronicznie oraz mogą się nie powieźć, powinny w jakiś sposób poinformować użytkownika o niepowodzeniu. Dobrym sposobem jest np. oznajmienie użytkownikowi, że otrzyma wiadomość z potwierdzeniem wykonania operacji. 3.8. Spójność strony odczytującej - Eventual Consistency Strona zapisująca i odczytująca może korzystać z tego samego źródła danych. Jednak lepszą wydajność i oddzielenie między tymi komponentami można osiągnąć, gdy zastosuje się osobne bazy danych. Należy jednak poradzić sobie z problemem synchronizacji pomiędzy obiema stronami [23]. Komenda, która jest przetwarzana zmienia stan bazy danych strony zapisującej. Dane te muszą również zostać odzwierciedlone po stronie odczytującej, gdyż nie są one ze sobą spójne do czasu ich uaktualnienia. Po stronie odczytującej występuje wtedy tzw. Eventual consistency. Informacje, które będą pobierane ze strony odczytującej przez klienta mogą być przestarzałe nawet o kilka minut. Ważne jest, aby było to akceptowalne przez biznes oraz było to w świadomości użytkownika końcowego [16, 19]. Aby zadbać o silną spójność, trzeba operację uaktualnienia baz danych objąć w transakcję. Wiąże się to jednak z problemem wydajności oraz dostępności całego systemu. Każda ze stron musi być przez pewien okres zablokowana, dopóki obie nie będą gotowe na zatwierdzenie transakcji. Im więcej jest baz danych do uaktualnienia, tym więcej czasu może potrwać cała transakcja [16]. Dodatkowo, jeśli chociaż jedna operacja nie powiedzie się, wszystko musi być przywrócone do poprzedniego stanu. 23

Korzystając z możliwości eventual consistency najlepiej użyć mechanizmu kolejek. Transakcja obejmuje wtedy jedynie aktualizację bazy danych do zapisu oraz dodanie komunikatu do kolejki [26]. Strona zapisująca jest wówczas całkowicie odseparowana od stron odczytujących, a więc nie jest zależna od potencjalnych nieudanych ich uaktualnień (np. z powodu braku dostępu do zasobu). Kolejkę traktuje się jak bufor, który następnie jest rozpropagowywany [17]. 3.9. Współpraca wielu użytkowników Tworzone systemy informatyczne są wykorzystywane przez wielu użytkowników jednocześnie. Praca wielu osób na tym samym obiekcie, w tym samym czasie jest problematyczna z technicznego punktu widzenia. W architekturze N-warstwowej należałoby wtedy zablokować na pewien czas zasób, na którym operuje użytkownik, tak aby inny użytkownik w tym samym czasie na nim nie pracował. Dzięki zastosowaniu CQRS można zniwelować lub całkowicie usunąć problem współpracy (ang. collaboration) wielu użytkowników na wspólnych danych [19]. Przykładem, który doskonale obrazuje wspomniany problem jest sytuacja, gdy dwóch lub więcej użytkowników próbuje zakupić pewien ograniczony ilościowo produkt. W przypadku klasycznego podejścia rozwiązaniem tego problemu jest objęcie całej operacji w transakcję. Użytkownik, który pierwszy otworzy taką transakcję otrzyma swój produkt. Jednak zastosowanie transakcyjności sprawia, że przez pewien czas zablokowany jest dostęp do danych i pewne operacje mogą czekać na swoją kolej. Mogą wystąpić również klasyczne problemy współbieżności takie jak zakleszczenie lub zagłodzenie. W przypadku zastosowania podejścia CQRS komendy, które będą przychodziły od użytkownika można zakolejkować. Wiadomości znajdujące się w kolejce będą następnie pobierane i przetwarzane. Pobieranie tych wiadomości można zaimplementować tak, aby były one pobierane z kolejki w sposób sekwencyjny, a więc tylko przez jeden wątek. Dzięki temu problem współbieżności pracy wielu osób w ogóle nie występuje. Sposób w jaki przetwarzane będą komendy może być podyktowany samą domeną lub pewnym scenariuszem. W niektórych przypadkach komendy będą przetwarzane stosując pewne reguły takie jak kto pierwszy ten lepszy, ostatni wygrywa lub priorytet wiadomości [17]. 24

4. EVENT SOURCING 4.1. Znaczenie zdarzeń w systemie Wymaganiem tworzonego systemu może być zapisywanie czasu i użytkownika dokonującego zmian w modelu domenowym. Najłatwiejszym sposobem zapewnienia tego wymagania jest dostawienie dwóch informacji do naszego modelu: daty ostatniej zmiany i przez kogo została ona wykonana. Taka zmiana zapewnia podstawowe zagwarantowanie spełnienia wymagania biznesowego. Można również wprowadzić logowanie zdarzeń. Oba te rozwiązania niosą za sobą pewne ograniczenia tj.: przeważnie nie jest trzymana pełna historia wszystkich operacji w systemie (z powodu kosztów utrzymania) oraz nie można uzyskać stanu systemu w momencie zajścia pewnego zdarzenia [12]. Przykładowo, klient kupuje produkty w sklepie internetowym. Dodaje do koszyka dwa produkty, a następnie dodaje trzeci oraz usuwa ten pierwszy. Znając stan aplikacji jedynie po dokonaniu tych operacji tracimy wiedzę na temat tego co się wydarzyło. Czasami warto zastanowić się, dlaczego klient usunął ze swojego koszyka taki produkt. Możliwe, że produkt ten był za drogi i należy sprzedawać go w promocyjnej cenie [27]. Przykład ten doskonale ilustruje, iż z punktu widzenia biznesowego czasami ważne jest, aby znać kontekst, kiedy zdarzenie miało miejsce, czyli jaki był stan systemu, gdy dane zdarzenie nastąpiło oraz jaki stan systemu był zaraz po wykonaniu tej operacji i całą sekwencję takich zdarzeń [12]. Idea zapamiętywania każdego zdarzenia jest zaimplementowana w systemach kontroli wersji np. Git, Subversion, Mercurial. Każda zmiana w pliku zostaje utrwalona, a więc można śledzić zmiany w całym repozytorium. Dzięki systemom kontroli wersji można zobaczyć historię zmian pliku i prześledzić dokonane zmiany [18]. 4.2. Zdarzenia w systemie Zdarzenia domenowe reprezentują to, co stało się w systemie. Są one zapisywane w formie przeszłej w czasie dokonanym np. OrderPlacedEvent. Dlatego każde zdarzenie jest niezmienne, stałe nie można zmienić już tego co zostało dokonane. Jedynym sposobem na zmianę lub cofnięcie stanu jest wygenerowanie kolejnego zdarzenia np. OrderCancelledEvent. Każde zdarzenie generuje wiadomość bezzwrotną, która pochodzi z jakiegoś źródła i może być odebrana przez wielu subskrybentów [16]. Zdarzenie posiada informacje kontekstowe, np. zdarzenie OrderPlacedEvent zawiera informacje dotyczące czasu złożonego zamówienia lub identyfikatory zamówionych przedmiotów. Przykładowe zdarzenie zostało pokazane na Listingu 4.1. 25

public class OrderPlacedEvent { public readonly Guid[] OrderedItemsGuids; public readonly DateTime DateOrdered; } public OrderPlacedEvent(Guid[] itemids) { OrderedItemsGuids = itemids; DateOrdered = DateTime.Now; } Listing 4.1 Zdarzenie zamówionych przedmiotów 4.3. Sposób działania Głównym konceptem Event Sourcing jest to, że każda zmiana stanu aplikacji jest powiązana z wysyłanym przez niego zdarzeniem zawierającym tę zmianę. Przychodząca komenda inicjuje wykonanie logiki biznesowej na agregacie. Wykonanie takiej logiki musi wygenerować chociaż jedno zdarzenie. Następnie każde takie zdarzenie jest przechowywane zachowując sekwencję, w jakiej zostały wywołane w systemie. Zatem przechowywana jest historia zmian stanu aplikacji [18]. Rozważając przykład dodawania przedmiotów do koszyka w sklepie internetowym, w systemie istniałby serwis z tą odpowiedzialnością. Przykładowy serwis pokazany jest na Listingu 4.2. public class CartService { public void AddProductToCart(Guid cartid, Guid productid) { Product product = ProductRepository.GetProductById(productId); Cart cart = CartRepository.GetCartById(cartId); cart.products.add(product); } } Listing 4.2 Serwis dodający produkt do koszyka Każde wywołanie serwisu najpierw pobiera stan koszyka, a następnie dodaje do niego nowy przedmiot. Po wykonaniu takiej logiki stan koszyka zmienia się. Dodając Event Sourcing do takiego systemu, zaimplementowany serwis musi wygenerować również zdarzenie ProductAddedEvent, które z kolei jest odpowiedzialne za tę zmianę. 26

Dzięki zastosowaniu Event Sourcing, można wdrożyć w życie mechanizmy, które opierają się na historii wykonanych zdarzeń dzienniku zdarzeń. Stan systemu korzystającego z Event Sourcing może zostać całkowicie usunięty i następnie odbudowany poprzez powtarzanie po kolei zdarzeń na systemie nie zawierającym początkowo żadnych danych. Można także cofnąć się do poprzedniego stanu systemu, który jest reprezentowany przez dane zdarzenie. Jeśli w systemie znajdzie się błąd, a zdarzenie zostało już wysłane, można je odwrócić, generując nowe zdarzenie. Następnie można powtórzyć te zdarzenia wraz z kolejnymi, tym razem dobrymi danymi [18, 28]. 4.4. Przechowywanie zdarzeń W standardowym podejściu przechowywanie danych wiąże się z podejściem strukturalnym. Tworzony jest diagram związków encji (ang. Entity Relationship Diagram), z którego tworzy się model bazy danych. Reprezentacją obiektu jest wtedy zbiór tabel połączonych relacjami. W przypadku Event Sourcing każde zdarzenia, które będą generowane przez tworzony system, są zapisywane w bazie danych nazywanym Event Storage. Będzie ona zawierała sekwencje zdarzeń dla danego agregatu, a więc potrzebne jest tylko kilka tabel sprawiając, że model danych zostaje uproszczony. Istnieje również struktura obiektów, mimo że nie jest widoczna bezpośrednio patrząc na model danych. Można ją zobaczyć, jeśli powtórzone zostałyby wszystkie zdarzenia od początku istnienia sytemu. Istotną zaletą stosowania Event Sourcing jest to, że model danych jest całkowicie odseparowany od modelu obiektowego. Jeśli zmieniona zostanie chociaż jedna klasa w systemie np. dodanie nowej właściwości, nie trzeba martwić się o zmianę struktury bazy danych [16, 23]. 4.5. Odwracanie zdarzeń Podczas pracy nad systemem informatycznym potrzebne jest zaimplementowanie funkcjonalności usunięcia pewnego obiektu z systemu. Obiekt ten, kiedyś dodano, a więc związane jest z nim jakieś zdarzenie. Korzystając z Event Sourcing nie można usunąć żadnego zdarzenia z Event Storage, gdyż każde kolejne zdarzenie, które po nim nastąpiło może korzystać z danych, które były wcześniej przetworzone przez inne zdarzenie. Usunięcie takiego zdarzenia skutkowałoby utratą spójności danych lub całkowicie uniemożliwiłoby powtórzenie sekwencji wszystkich zdarzeń. Dodatkowo utracona zostałaby historia zdarzeń. Sposobem rozwiązania tego problemu jest dodanie kolejnego zdarzenia, które jest odwrotnością poprzedniego. Kolejne zdarzenie musi posiadać dane, które sprawią, że pierwsze zdarzenie nigdy nie nastąpiło. Dla niektórych zdarzeń dokonanie odwrotnej transakcji jest stosunkowo proste. Tyczy się to zdarzeń, które opierają się ściśle na zmianie poszczególnej wartości np. zdarzenie dodania 50 zł do konta użytkownika. W celu odwrócenia tej operacji utworzone zostaje zdarzenie, w którym odejmuje się tę sumę. Jednakże jeśli zdarzenie ustawia 27

sumę konta użytkownika na konkretną wartość nie można w prosty sposób odwrócić takiego zdarzenia. Rozwiązaniem tego problemu jest dodanie wszystkich potrzebnych danych w zdarzeniu, które umożliwia jego odwrócenie. Dla typów wartościowych można obliczyć różnicę tych wartości. Dla innych typów można przetrzymywać ich poprzednią wartość [16, 18]. 4.6. Systemy zewnętrzne Współczesne systemy informatyczne komunikują się z zewnętrznymi systemami. Opieranie się na zdarzeniach sprawia w tym przypadku problemy. Po pierwsze zewnętrzny system wcale nie musi być przystosowany do współpracy z systemem korzystającym z wzorca Event Sourcing, a więc nie będzie kompatybilny z możliwością powtarzania zdarzeń. System zewnętrzny prawdopodobnie nie rozróżnia czy przetwarzanie zdarzenia odbywa się po raz pierwszy czy przetwarzamy je ponownie. Wykonanie logiki podczas powtarzania zdarzeń może zmienić stan zewnętrznego systemu. Rozwiązaniem takiego problemu jest wprowadzanie dodatkowego obiektu, który byłby odpowiedzialny za komunikację z systemami firm trzecich tzw. Gateway. Taki obiekt sprawdza czy odbywa się powtarzanie zdarzeń przed wysłaniem komunikatu do innych systemów. Zewnętrzne systemy mogą zmieniać swój stan wtedy, gdy wywołuje się procedurę, która ma jedynie zwrócić wynik zapytania. Dodatkowo niektóre systemy zwracają różne wyniki w zależności od czasu w jakim zostały wysłane do niego żądania. Na przykład zapytanie o aktualną pogodę może zwrócić różne wyniki w momencie pierwszego przetwarzania i podczas powtórzenia całej historii zdarzeń. Komunikacja z niektórymi zewnętrznymi systemami może nie być, aż tak problematyczna, gdyż mogą zwracać dane historyczne oczekują daty w żądaniach. Jeśli jednak tak nie jest, należy zapamiętywać odpowiedzi z takiego systemu. Można do tego użyć wcześniej zaproponowanego obiektu Gateway, który trzyma historię wszystkich odpowiedzi z systemu. Odpowiedzi te są następnie używane podczas powtarzania zdarzeń [18]. 4.7. Implementacja Event Storage Jak było wcześniej wspomniane budowany Event Storage składa się z kilku tabel. Omawiany przykład będzie używał diagramów związków encji. Pierwszą encją, która należy utworzyć to encja Events obrazująca zdarzenia. Zawiera ona: unikalny identyfikator agregatu, do którego się odwołuje, zserializowany obiekt zdarzenia, wersję, która jest unikalnym, sekwencyjnym numerem dla danego agregatu. Można dodać do niej kolejne atrybuty takie jak czas zajścia danego zdarzenia lub kontekst w jakim zaszło to zdarzenie (kto je wykonał, z jakiego serwisu aplikacyjnego). Atrybuty te są w pełni opcjonalne i zależą od wymagań tworzonego systemu. Tabela, która zostanie stworzona z danej encji powinna być indeksowana po identyfikatorze danego agregatu w celu optymalizacji zapytań. 28

Każdy agregat znajdujący się w systemie ma swoją reprezentację w Event Storage. Dlatego należy stworzyć encję Agregatu zawierającą identyfikator (GUID) agregatu, jego typ oraz wersję. Wersja to numer ostatniego zdarzenia powiązanego z tym agregatem. Jest ona opcjonalna, lecz w celach optymalizacyjnych warto przetrzymywać tę wartość w sposób zdenormalizowany. Typ agregatu to nazwa agregatu. Diagram związków encji podstawowego Event Storage przedstawiony został na Rys. 4.1. Rys. 4.1 Diagram związków encji podstawowego Event Storage Dla tak stworzonej przechowalni zdarzeń można zdefiniować operacje odczytu zdarzeń oraz zapisywania nowego zdarzenia [16]. W pierwszym przypadku wystarczy jedne proste zapytanie, które pobiera wszystkie zdarzenia dla danego agregatu. Przykładowa procedura składowana przedstawiona została na Listingu 4.3. CREATE PROCEDURE GetAggregateEvents @aggregateguid nvarchar(36) AS SELECT * FROM EVENTS WHERE AGGREGATE_GUID = @aggregateguid ORDER BY VERSION ASC; Listing 4.3 Zapytanie SQL zwracające posegregowane zdarzenia dla podanego agregatu Jeśli potrzebny jest powrót do historycznego stanu systemu należy pobrać wszystkie zdarzenia korzystając z wersji agregatu lub daty wywołania tego zdarzenia. Kolejną operacją jaka jest potrzebna to dodawanie nowego zdarzenia. W tym przypadku należy sprawdzić, czy został wcześniej stworzony agregat. Jeśli nie, to tworzony jest nowy rekord agregatu z wersją równą 0. Następnie sprawdzana jest optymistyczna współbieżność, a więc wersja danego agregatu z tą, którą oczekujemy. Jeśli się powiedzie następuje iteracja przez wszystkie zdarzenia oraz ich zapis. Na końcu aktualizowana jest wersja agregatu. Każda taka operacja objęta jest w transakcji [16]. Przykładowa procedura w języku Transact-SQL przedstawiona jest na Listingu 4.4. 29

CREATE PROCEDURE InsertEventsForAggregate @aggregateguid nvarchar(36), @aggregatetype nvarchar(100), @events AS dbo.eventlist READONLY, @expectedversion int = 0 AS DECLARE @versionnumber int = null DECLARE @eventdata nvarchar(max) = null SELECT @versionnumber = Version FROM AGGREGATES WHERE AggregateGUID = @aggregateguid BEGIN Transaction if @versionnumber IS NULL BEGIN INSERT INTO AGGREGATES (AggregateGUID, Version, Type) VALUES (@aggregateguid, 0, @aggregatetype) END SET @versionnumber = 0 if @versionnumber!= @expectedversion BEGIN DECLARE @msg nvarchar(max) = 'Concurrency problem while inserting events for aggregate '+ @aggregatetype+' (aggregateguid = '+@aggregateguid+')' ;THROW 99001, @msg, 1; ROLLBACK END DECLARE EVENT_CURSOR CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY FOR SELECT eventdata FROM @events OPEN EVENT_CURSOR FETCH NEXT FROM EVENT_CURSOR INTO @eventdata WHILE @@FETCH_STATUS = 0 BEGIN SET @versionnumber = @versionnumber + 1; INSERT INTO EVENTS(AggregateGUID, Data, Version, DateProcessed) VALUES(@aggregateGuid, @eventdata, @versionnumber, getdate()) UPDATE AGGREGATES SET Version = @versionnumber WHERE AggregateGUID = @aggregateguid END CLOSE EVENT_CURSOR DEALLOCATE EVENT_CURSOR COMMIT Listing 4.4 Procedura zapisująca zdarzenia 4.8. Snapshots zrzuty aktualnego stanu agregatu W przypadku Event Sourcing istnieje jeden bardzo duży problem wydajnościowy. Odtworzenie wszystkich zdarzeń od początku trwania systemu dla agregatu, na którym wykonywanych było wiele operacji, jest bardzo kosztowne pod względem czasowym. 30

Mechanizmem ograniczającym ilość operacji do odtworzenia są snapshoty, czyli zrzuty aktualnego stanu agregatu [29]. Aby wprowadzić tę koncepcję należy zmodyfikować sposób w jakim ładowane są agregaty, aby brały pod uwagę wykonane zrzuty stanu. Wprowadzona zostanie nowa tabela do Event Storage zawierająca unikalny identyfikator agregatu, jego wersję oraz aktualny stan w formie zserializowanej. Metoda serializacji nie jest rzeczą znaczącą można użyć dowolnej np. binarnej, XML lub JSON. Rys. 4.2 przedstawia zmodyfikowaną wersję poprzedniego diagramu ERD uzupełnionego o encję Snapshots. Rys. 4.2 Diagram związków encji zawierający tabelę Snapshots Stosując technikę zrzutów agregatów, wprowadza się proces, który co pewien określony czas tworzy je automatycznie. Proces ten jest procesem systemowym lub długo żyjącym zadaniem (ang. long-running task). Wykonuje on zapytania do Event Storage, aby pobrać wszystkie agregaty wraz z ich zdarzeniami, które przekroczyły dopuszczalną ilość zdarzeń od ostatniego zrzutu. Dzięki temu pobieramy agregaty, które powinny mieć zrobiony nowy zrzut stanu [18]. Dopuszczalną ilość zdarzeń można łatwo obliczyć poprzez operację odejmowania wersji agregatu ostatniego zrzutu od aktualnej wersji agregatu. Następnie na tak pobranych agregatach wykonuje się powtarzanie zdarzeń, aby potem zapisać je jako nowy snapshot [16]. W celu optymalizacji dostępu do agregatu, zamiast mechanizmu zrzutów stanu można skorzystać z mechanizmu cache ującego, który przetrzymuje stan agregatu w pamięci. Odtwarzanie zdarzeń działa jedynie w przypadku ładowania nowego agregatu, którego nie ma w cache u lub w przypadku, gdy system uruchomiany jest na nowo [17]. Nie każdy system od razu potrzebuje mechanizmu snapshotów. Może być wprowadzony później jako ulepszenie wydajności [16]. 4.9. Wersjonowanie zdarzeń W trakcie tworzenia systemu wymagania zmieniają się, a co za tym idzie zdarzenia w systemie również ulegają modyfikacjom. Niektóre zdarzenia mogą nie być już potrzebne, bo nie są wywoływane przez żadną część aplikacji. Do systemu mogą zostać dodane nowe typy zdarzeń. Dla systemu, który nie jest jeszcze używany i nadal jest w trakcie powstawania można 31

pozwolić sobie na dowolne zmienianie kodu systemu wraz z danymi znajdującymi się w Event Storage. Niestety, gdy system jest już wdrożony na produkcji należy zwracać szczególną uwagę, aby nie wprowadzać zmian, które mogą negatywnie wpłynąć na działanie aplikacji korzystającej z Event Sourcing. Dodając nowe zdarzenie nie powinno ono mieć wpływu na pozostałe już istniejące zdarzenia. Do systemu dodawany jest nowy typ zdarzenia wraz z jego procedurą obsługi. Każde wygenerowanie takiego zdarzenia będzie dodawało nowy wpis do Event Storage, który będzie mógł być potem użyty ponownie. Zdarzenia, które są obecne w systemie, których już się nie wywołuje należy pozostawić, gdyż są niezmienne. Wygenerowane zdarzenia zapisane do Event Storage są wykorzystywane do zbudowania agregatu. Nie można ich usunąć. Procedury obsługi muszą być w stanie odtworzyć dane z takiego zdarzenia nawet wtedy, gdy nie są już generowane. Ostatnim scenariuszem z jakim można się spotkać jest zmiana już istniejącego zdarzenia. Event Storage może przetrzymywać starą wersję zdarzenia, a sam system generuje nową wersję. Następuje wtedy niedopasowanie tych zdarzeń. Podobna sytuacja może się zdarzyć, gdy inny kontekst związany korzysta ze starej wersji tego zdarzenia, a więc tworzony system musi być w stanie obsłużyć wiele wersji tego samego zdarzenia. W samych właściwościach klasy zdarzenia może nastąpić kilka rodzajów zmian. Zdarzenie może mieć dodaną nową właściwość, właściwość może zostać usunięta, zmienić swój typ lub wspierać większą liczbę dostępnych wartości (np. dodana jest nowa wartość w typie wyliczeniowym). Problemy te można rozwiązać poprzez trzymanie wielu klas z odpowiadającym różnym wersjom tego samego zdarzenia. Nazwa klasy może zawierać sufiks numeru wersji, np. ItemAddedToCart oraz ItemAddedToCart_V2. Nie jest to najlepsze rozwiązanie, ponieważ po kilku takich zmianach model domenowy jest zaśmiecony. Jednak taka zmiana nie wymaga ingerencji w kod infrastruktury systemu. Drugim podejściem jest stworzenie mechanizmu, który dla podanego starego zdarzenia przekształci je w nowe. Sam system korzysta wtedy jedynie z najnowszej wersji zdarzenia. Takie podejście rodzi problem umiejscowienia takiego mechanizmu. Można dodać filtry przekształcające zdarzenia w infrastrukturze systemu komunikatów lub w procedurach obsługi zdarzeń [17]. Analizując pobieranie samych zdarzeń z Event Storage należy zwrócić uwagę na translację zapisanych zdarzeń do obiektów. Niektóre sposoby serializacji zdarzeń pomagają w osiągnięciu takiego mechanizmu np. serializator notacji JSON może ignorować usunięte typy lub dodawać domyślne wartości dla właściwości, których nie ma w zserializowanym obiekcie. Niekiedy zdarzenia, które są wcześniej zdefiniowane muszą zostać podzielone na kilka innych lub złączone w jedno, np. zdarzenie AccountRegisteredEvent może zostać podzielone na trzy zdarzenia: AccountCreatedEvent, UserAddedEvent oraz UserActivatedEvent [30]. 32

4.10. Połączenie CQRS z Event Sourcingiem Wzorzec CQRS oraz Event Sourcing żyją ze sobą w symbiozie. Problemy występujące w jednym z wzorcu mają rozwiązania w drugim. W systemie, w którym występuje tylko Event Sourcing występuje problem odczytywania danych z Event Storage, gdyż nie ma dostępu do obecnego stanu w systemie [16]. Występuje w nim jedynie zwracanie zdarzeń nie można stworzyć zapytania o dokładne dane. Jeśli jednak programista ma do dyspozycji stronę odczytującą ze wzorca CQRS staje się to możliwe. Natomiast problemem występującym w CQRS jest koszt utrzymywania dwóch modeli baz danych strony odczytującej i zapisującej. Dochodzi do tego utrzymywanie modelu zdarzeń, który synchronizuje te dwa modele. Jednak korzystając z Event Sourcing model zdarzeń jest również mechanizmem przechowywania danych, dzięki czemu można nim zastąpić model danych strony zapisującej. Wykorzystując podejście Event Sourcing zapewnione jest łatwe utrzymanie bazy danych strony odczytującej, jeśli korzysta się z podejścia jednej tabeli dla jednego widoku. Oznacza to, że bazę danych strony odczytującej można wyczyścić z wszystkich danych oraz zmienić jej schemat. Mając zapisane zdarzenia w przechowalni zdarzeń, można ich użyć, aby wypełnić danymi nowo stworzone tabele. Tworzenie lub edycja struktury tabeli odpowiedzialnej za poszczególny widok jest wykonywana w izolacji od innych tabel. Taka tabela jest wypełniona danymi, które są dostarczane przez odpowiednie zdarzenia. To podejście pozwala na łatwe zmiany, dostosowując wymagania biznesu odnośnie interfejsu użytkownika [12]. 33

5. PROJEKT TESTOWANEGO SYSTEMU 5.1. Opis domeny W celu sprawdzenia użyteczności wzorca CQRS i Event Sourcing stworzono przykładowy system informatyczny, którego celem jest dostarczenie aplikacji internetowej do sprzedaży kodów umożliwiających aktywowanie gier komputerowych na różnych platformach dystrybucji cyfrowej. Za zarządzanie sprzedawanymi grami oraz kodami są odpowiedzialni pracownicy sklepu. Pracownik sklepu jest odpowiedzialny za dodawanie nowych gier do katalogu produktów. Wpisuje podstawowe informacje o sprzedawanej grze tj. nazwę, gatunek gry, platformę sprzedaży, opis oraz przykładowe zdjęcia. Po wprowadzeniu tych informacji może je edytować. Dodatkowo określa cenę wraz z marżą. Sklep sprzedaje kody do gier, tak więc pracownik ma możliwość ich wprowadzenia. Z czasem cena gry ulega zmianie, więc system udostępnia możliwość zmiany ceny. Gra może nie być dostępna w dniu, w którym będzie dodawana do katalogu, dlatego przed wystawieniem jej do sprzedaży musi zostać aktywowana przez pracownika sklepu. Klient wchodząc na portal ma możliwość przeglądania katalogu dostępnych gier. Może wyszukiwać danej gry korzystając z dostępnej opcji szukania. Po wejściu na szczegóły gry może zobaczyć dotyczące je informacje. Wybrana gra może zostać dodana do koszyka. Podczas przeglądania koszyka klient widzi wykaz wszystkich gier, które do niego dodał. Może je usunąć lub zmienić ich ilość. Klient nie potrzebuje konta, aby dodawać przedmioty do koszyka, lecz potrzebuje go, aby zakupić dane produkty. Po zamówieniu danych produktów klient dokonuje płatności wykorzystując numer zamówienia w tytule przelewu. Po otrzymaniu takiej płatności kody do gry są widoczne w historii zamówień. Każde zamówienie ma określony czas na dokonanie płatności. Po upływie tego czasu zamówienie jest anulowane. 5.2. Dekompozycja domeny Domena została podzielona na dwa konteksty związane: zarządzanie produktami oraz zamówienia. Każdy z tych kontekstów ma swoją osobną aplikację (dla pracowników oraz klientów). Wysokopoziomowy diagram związków pomiędzy danymi kontekstami pokazany został na Rys. 5.1. Relacje między kontekstami symbolizują przepływ danych (zdarzeń). Zarządzanie produktami Zamówienia Rys. 5.1 Podział domeny na dwa konteksty związane 34

5.3. Wszechobecny język Podczas tworzenia systemu wykorzystano terminy przedstawione w Tabeli 5.1. Są one używane w opracowanej dokumentacji i w kodzie napisanego systemu. Tabela 5.1 Wszechobecny język tworzonego systemu Termin Termin w j. angielskim Produkt Product Sprzedawane oprogramowanie komputerowe w postaci kodu aktywacyjnego Platforma sprzedaży Sale Platform Typ witryny internetowej lub programu, który sprzedaje kody do produktu cyfrowego (gry komputerowej, programu komputerowego) Cena jednostkowa Unit Price Cena podstawowa produktu Marża Margin Różnica między ceną całkowitą a ceną jednostkową zysk ze sprzedaży kodu aktywującego produkt Cena całkowita Total Price Cena, którą płaci klient kupujący kod aktywujący produkt Kod Code Ciąg znaków używany do aktywacji produktu na danej platformie sprzedaży (format kodu jest zależny od sprzedawanego produktu lub platformy sprzedaży) Ilość zasobu Stock Amount Ilość kodów przypisanych do produktu Opis Dostępna ilość Available Amount Ilość kodów dostępnych do sprzedaży Sprzedana ilość Sold Amount Ilość sprzedanych kodów Dostępny kod Available Code Kod, który klient może zarezerwować Zarezerwowany kod Reserved Code Kod, który nie został jeszcze sprzedany, czekający na płatność zamówienia Sprzedany kod Sold Code Kod przypisany do zakończonego zamówienia Zamówienie Order Zamówienie na produkty dokonane przez klienta Pozycja zamówienia Order Item Produkt znajdujący się w zamówieniu Złożone zamówienie Submitted Order Zamówienie oczekujące na płatność Zakończone zamówienie Anulowane zamówienie Completed Order Cancelled Order Opłacone zamówienie, mające przypisane kody zakupionych produktów Zamówienie nieopłacone w wyznaczonym czasie jest przedawnione Koszyk Cart Funkcjonalność umożliwiająca dodawanie produktów, które klient chce zamówić Pozycja koszyka Cart Item Produkt dodany do koszyka Koszyk nieaktywny Disabled Cart Koszyk, który jest nieaktywny z powodu wyczyszczenia wszystkich pozycji koszyka lub z powodu stworzenia zamówienia z koszyka 35

5.4. Przypadki użycia Dla potrzeb tworzonego systemu stworzono spis funkcjonalności, który jest przedstawiony na diagramach przypadków użycia (Rys. 5.2, Rys. 5.3, Rys. 5.4) oraz w tabelach je opisujących (Tabela 5.2, Tabela 5.3, Tabela 5.4, Tabela 5.5). Diagramy oparto na specyfikacji OMG dla języka UML 2.5 [31]. Tabela 5.2 Opis aktorów dla diagramów przypadków użycia Pracownik sklepu Klient zalogowany Klient niezalogowany System płatności Użytkownik niezalogowany Użytkownik zalogowany Opis aktorów Zewnętrzny, ożywiony, główny, aktywny Pracownik sklepu zarządza katalogiem produktów dodaje, modyfikuje, ustala cenę oraz aktywuje lub dezaktywuje poszczególne produkty. Odpowiada za dodawanie nowych kodów produktów. Zewnętrzny, ożywiony, główny, aktywny Klient przegląda produkty dostępne w katalogu sklepu. Dodaje produkty do koszyka, a następnie składa zamówienie. Po zapłaceniu kwoty zamówienia wykorzystuje kody do zakupionych produktów. Zewnętrzny, ożywiony, główny, aktywny Klient przegląda produkty dostępne w katalogu sklepu. Dodaje produkty do koszyka. Zewnętrzny, nieożywiony, główny, aktywny System płatności wysyła informację do systemu o dokonanych płatnościach. Zewnętrzny, ożywiony, drugorzędny, aktywny Użytkownik nie mający konta w systemie lub mający konto w systemie, lecz nie zalogowany do systemu. Zewnętrzny, ożywiony, drugorzędny, aktywny Użytkownik korzystający z systemu będąc zalogowanym na koncie utworzonym przy rejestracji. Rys. 5.2 Diagram przypadków użycia dla kontekstu związanego Zarządzanie produktami 36

Tabela 5.3 Opis przypadków użycia dla kontekstu związanego Zarządzanie produktami Opis przypadków użycia dla kontekstu związanego: Zarządzanie produktami Przeglądanie katalogu produktów Skrócony opis Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Funkcja pozwala pracownikowi sklepu na przeglądanie produktów dodanych do systemu oraz wyświetlenie ich podstawowych informacji <<extend>> wyświetlanie informacji o produkcie Pracownik sklepu Pracownik sklepu jest zalogowany Pracownik sklepu wybiera opcję Products management Pracownik sklepu przegląda dostępne produkty. Może wybrać produkt, aby zobaczyć szczegóły produktu. Ma możliwość zarządzania poszczególnymi produktami z tego widoku. Wyświetlone zostają produkty w tabeli z poszczególnymi akcjami dla danego produktu. Lista produktów jest stronicowana. Dodawanie nowego produktu Skrócony opis Pozwala na dodanie nowego produktu do katalogu produktów Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne <<include>> określenie ceny produktu Pracownik sklepu Pracownik sklepu jest zalogowany Pracownik sklepu wybiera opcję New product z widoku katalogu produktów Pracownik sklepu podaje nazwę produktu, wybiera platformę sprzedaży, gatunek gry, wpisuje krótki opis produktu, opis szczegółowy oraz adresy http do obrazków. Produkt zostaje dodany do katalogu. Określenie ceny produktu Skrócony opis Pozwala na automatyczne określenie marży oraz ceny produktu Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne System Pracownik sklepu określił cenę produktu podczas dodawania nowego produktu do katalogu Produkt został dodany do katalogu Zostaje określona marża wynosząca domyślnie 10% ceny wpisanej przez pracownika sklepu Produkt ma przypisaną cenę jednostkową oraz marżę 37

Opis przypadków użycia dla kontekstu związanego: Zarządzanie produktami Zmiana ceny produktu Skrócony opis Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Pozwala na zmianę ceny jednostkowej oraz marży dla danego produktu Pracownik sklepu Pracownik sklepu jest zalogowany Produkt jest dodany do katalogu produktów Pracownik sklepu wybrał opcję Change price z widoku katalogu produktów lub widoku wyświetlania szczegółowych informacji o produkcie Wyświetlone zostają informacje o cenie produktu (cena jednostokowa, procent i wartość marży, cena całkowita). Pracownik sklepu wpisuje cenę jednostkową oraz marżę po wpisaniu, których zostaje wyświetlony podgląd ceny po zmianie. Nowa cena jest akceptowana przez pracownika sklepu. Cena produktu zostaje zmieniona na nową. Edycja informacji na temat produktu Skrócony opis Pozwala zmienić informacje o produkcie Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Pracownik sklepu Pracownik sklepu jest zalogowany Produkt jest dodany do katalogu produktów Pracownik sklepu wybrał opcję Change item information z widoku katalogu produktów lub widoku wyświetlania informacji o produkcie Pracownik sklepu edytuje nazwę produktu, gatunek gry, skrócony opis, opis lub adresy URL do obrazków. Dane dotyczące produktu zostają zmienione Wyświetlanie informacji o produkcie Skrócony opis Pozwala na wyświetlenie szczegółowych informacji na temat produktu znajdującego się w katalogu Powiązane przypadki <<extend>> Edycja informacji na temat produktu użycia <<extend>> Zmiana ceny produktu <<extend>> Aktywacja produktu <<extend>> Dezaktywacja produktu <<extend>> Dodawanie kodów Aktorzy Pracownik sklepu Warunki początkowe Zdarzenia inicjujące Przebieg Pracownik sklepu jest zalogowany Produkt jest dodany do katalogu produktów Pracownik sklepu wybiera produkt z katalogu produktów Pracownik sklepu przegląda szczegóły produktu: nazwę produktu, platformę sprzedaży, gatunek, status produktu, obrazki, cenę jednostkową, wartość i procent marży, cenę całkowitą, całkowitą 38

Warunki końcowe Opis przypadków użycia dla kontekstu związanego: Zarządzanie produktami Przebiegi alternatywne Aktywacja produktu Skrócony opis Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Dezaktywacja produktu Skrócony opis Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Zarządzanie kodami Skrócony opis Powiązane użycia Aktorzy przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne liczbę kodów, dostępną liczbę kodów, sprzedaną liczba kodów i zysk ze sprzedaży Pozwala na aktywację produktu. Aktywny produkt jest widoczny przez klienta Pracownik sklepu Pracownik sklepu jest zalogowany Produkt jest dodany do katalogu produktów Produkt jest nieaktywny Pracownik sklepu wybiera opcję Activate brak Produkt zmienił swój status na aktywny Pozwala na dezaktywację produktu. Nieaktywny produkt jest niewidoczny przez klienta Pracownik sklepu Pracownik sklepu jest zalogowany Produkt jest dodany do katalogu produktów Produkt jest aktywny Pracownik sklepu wybiera opcję Deactivate Produkt zmienił swój status na nieaktywny Pozwala na wyświetlenie listy kodów powiązanych z danym produktem <<extend>> Dodawanie kodów Pracownik sklepu Pracownik sklepu jest zalogowany Produkt dodany jest do katalogu produktów Pracownik sklepu wybiera opcję Manage codes z widoku katalogu produktów lub widoku szczegółów produktu Wyświetlone zostają kody wraz z informacją czy zostały sprzedane 39

Dodawanie kodów Skrócony opis Powiązane użycia Aktorzy Opis przypadków użycia dla kontekstu związanego: Zarządzanie produktami przypadki Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Pozwala na dodanie kodów dla produktu Pracownik sklepu Użytkownik jest zalogowany Produkt jest dodany do katalogu produktów Użytkownik wpisuje listę kodów dla danego produktu Dla produktu zostają dodane nowe kody Rys. 5.3 Diagram przypadków użycia dla kontekstu związanego Zamówienia 40

Tabela 5.4 Opis przypadków użycia dla kontekstu związanego Zamówienia Opis przypadków użycia dla kontekstu związanego: Zamówienia Przeglądanie katalogu produktów Skrócony opis Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Pozwala na przeglądanie katalogu produktów dostępnych gier w sprzedaży <<extend>> wyszukiwanie produktów <<extend>> wyświetlanie informacji o produkcie Klient zalogowany, Klient niezalogowany Klient wybiera opcję Games Klient przegląda dostępne gry z katalogu produktów. Może wybrać daną pozycję, aby zobaczyć ich szczegóły. Wyświetlona zostaje lista produktów dostępnych w katalogu. Wyświetlana jest nazwa, skrócony opis, okładka gry, cena, platforma sprzedaży, gatunek gry. Wyszukiwanie produktów Skrócony opis Pozwala na wyszukiwanie produktu w katalogu Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Klient zalogowany, Klient niezalogowany Klient przegląda katalog produktów Klient wpisał dane o wyszukiwanej grze (nazwę produktu, cenę maksymalną, platformę sprzedaży lub gatunek gry) System przeszukuje listę produktów Wyświetlona jest lista gier spełniająca kryterium wyszukiwania. Wyświetlana jest nazwa, skrócony opis, okładka gry, cena, platforma sprzedaży, gatunek gry. Nie znaleziono produktu: Wyświetlana jest pusta lista Wyświetlanie informacji o produkcie Skrócony opis Pozwala na wyświetlenie szczegółów danego produktu znajdującego się w katalogu Powiązane <<extend>> Dodanie produktu do koszyka przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Klient zalogowany, Klient niezalogowany Klient wybrał opcję pokazania szczegółów danego produktu (z listy dostępnych produktów lub z koszyka na zakupy) Klient przegląda szczegóły dotyczące wybranej pozycji 41

Warunki końcowe Przebiegi alternatywne Opis przypadków użycia dla kontekstu związanego: Zamówienia Wyświetlane są szczegóły dotyczące danego produktu: nazwa produktu, okładka gry, platforma sprzedaży, gatunek gry, cena, opis i obrazki z gry Dodanie produktu do koszyka Skrócony opis Pozwala na dodanie wybranego produktu do koszyka Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Klient zalogowany, Klient niezalogowany Klient przegląda informację dotyczące produktu Klient wybiera opcję dodania produktu do koszyka Produkt został dodany do koszyka, wyświetlony jest widok koszyka Przeglądanie produktów w koszyku Skrócony opis Pozwala na przeglądanie produktów dodanych do koszyka Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg <<extend>> Wyświetlanie informacji o produkcie <<extend>> Zmiana ilości produktu <<extend>> Usunięcie produktu z koszyka <<extend>> Wyczyszczenie koszyka <<extend>> Złożenie zamówienia Klient zalogowany, Klient niezalogowany Klient wybiera opcję Cart lub dodał produkt do koszyka Klient przegląda produkty dodane do koszyka Warunki końcowe Zostaje wyświetlona lista gier dodanych do koszyka wraz z informacjami o cenie jednostkowej, ilości produktu, cenie całkowitej, całkowitej wartości koszyka Przebiegi alternatywne W koszyku nie znajduje się żaden produkt: Wyświetlana jest informacja o braku produktów w koszyku Zmiana ilości produktu Skrócony opis Pozwala na zmianę ilości produktu dodanego do koszyka Powiązane przypadki użycia Aktorzy Klient zalogowany, Klient niezalogowany 42

Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Opis przypadków użycia dla kontekstu związanego: Zamówienia Produkt jest dodany do koszyka Klient wybiera opcję Change quantity dla danego produktu z widoku koszyka Wyświetlana jest okładka z gry, nazwa produktu oraz obecna ilość wybranego produktu w koszyku. Klient wpisuje nową ilość. Zmieniła się ilość wybranego produktu w koszyku Klient wpisał wartość ujemną: Wyświetlany jest komunikat o złej wartości ilości produktu Klient wpisał wartość 0 : Produkt zostaje usunięty z koszyka Usunięcie produktu z koszyka Skrócony opis Pozwala na usunięcie wybranego produktu z koszyka Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Klient zalogowany, Klient niezalogowany Produkt jest oddany do koszyka Klient wybiera opcję Remove dla danego produktu z widoku koszyka System usuwa produkt z koszyka Koszyk nie zawiera wskazanego produktu Wyczyszczenie koszyka Skrócony opis Pozwala na usunięcie wszystkich produktów znajdujących się w koszyku Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Złożenie zamówienia Skrócony opis Powiązane przypadki użycia Klient zalogowany, Klient niezalogowany Przynajmniej jeden produkt dodany jest do koszyka Klient wybiera opcję Clear items in cart System usuwa produkty z koszyka Koszyk jest pusty Pozwala złożyć zamówienie na wybrane produkty, które znajdują się w koszyku 43

Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Opis przypadków użycia dla kontekstu związanego: Zamówienia Klient zalogowany Koszyk nie jest pusty Klient wybiera opcję Checkout na ekranie koszyka Na podstawie produktów znajdujących się w koszyku tworzone jest zamówienie Koszyk zostaje wyczyszczony, utworzone jest nowe zamówienie z przydzielonym numerem zamówienia kodów dla danego produktu: Zamówienie nie zostaje utworzone, koszyk nie jest wyczyszczony Przeglądanie złożonych zamówień Skrócony opis Pozwala na przeglądanie złożonych oraz wykonanych zamówień Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne <<extend>> Wyświetlanie zrealizowanych zamówień Klient zalogowany Klient zalogowany wybrał opcję Orders" Klient przegląda złożone zamówienia Wyświetlona zostaje lista wszystkich zamówień: data złożenia zamówienia, numer zamówienia, zamówione gry dla danego zamówienia (wraz z ilością), kwota zamówienia. Dla wykonanych zamówień wyświetlana jest data wykonania zamówienia. Klient nie złożył żadnych zamówień: Lista zamówień jest pusta Chociaż jedno zamówienie nie jest opłacone: Wyświetlany jest komunikat o niezapłaconym zamówieniu Wyświetlanie zrealizowanych zamówień Skrócony opis Pozwala na wyświetlenie wykonanego zamówienia i zakupionych kodów Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Klient zalogowany Zamówienie zostało złożone oraz opłacone Klient zalogowany wybiera zamówienie z listy zamówień Klient zalogowany przegląda wybrane zamówienie Wyświetlone są szczegóły danego zamówienia: Data złożenia, data wykonania, numer zamówienia, wartość zamówienia, zamówione produkty wraz z zakupionymi kodami 44

Przebiegi alternatywne Opis przypadków użycia dla kontekstu związanego: Zamówienia Opłacenie zamówienia Skrócony opis Pozwala na opłacenie zamówienia złożonego na produkty Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne System płatności W systemie złożone jest zamówienie System płatności wysyła zdarzenie o zapłaceniu kwoty na dany numer zamówienia System wyszukuje zamówienie o podanym numerze zamówienia, sprawdza czy kwota jest zgodna z wartością zamówienia. Zamówienie jest opłacone, szczegóły zamówienia widoczne są w zrealizowanych zamówieniach Płatność zrobiona na kwotę mniejszą niż wartość zamówienia: Zamówienie jest nieopłacone, płatność została odrzucona Rys. 5.4 Diagram przypadków użycia wspólnego dla obu kontekstów związanych 45

Tabela 5.5 Opis przypadków użycia wspólnych dla obu kontekstów związanych Rejestracja Skrócony opis Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Logowanie Skrócony opis Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Wylogowanie Skrócony opis Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Opis przypadków użycia wspólnych dla obu kontekstów związanych Pozwala na utworzenie konta w systemie <<include>> logowanie Użytkownik niezalogowany Użytkownik nie jest zalogowany Użytkownik wybrał opcję Register Użytkownik niezalogowany wpisuje e-mail, hasło, potwierdzenie hasła Użytkownik jest zalogowany w systemie e-mail użyty przy rejestracji jest już używany przez inne konto: Wyświetlony zostaje komunikat o zajętym adresie e-mail Pozwala na zalogowanie się użytkownika do systemu Użytkownik niezalogowany Użytkownik niezalogowany ma konto w systemie Użytkownik niezalogowany wybiera opcję Log in Użytkownik niezalogowany podaje adres e-mail oraz hasło Użytkownik niezalogowany zalogował się do systemu Dane logowania są nieprawidłowe: wyświetlony zostaje komunikat o nieprawidłowych danych logowania Pozwala na wylogowanie użytkownika zalogowanego Użytkownik zalogowany Użytkownik jest zalogowany Użytkownik zalogowany wybiera opcję Log off Użytkownik zalogowany został wylogowany z systemu 46

Zmiana hasła Skrócony opis Powiązane przypadki użycia Aktorzy Warunki początkowe Zdarzenia inicjujące Przebieg Warunki końcowe Przebiegi alternatywne Opis przypadków użycia wspólnych dla obu kontekstów związanych Pozwala na zmianę hasła przypisanego do konta Użytkownik zalogowany Użytkownik jest zalogowany w systemie Użytkownik wybrał opcję Change your password Użytkownik zalogowany wpisuje obecne hasło oraz 2 razy nowe hasło Hasło dostępu dla konta użytkownika zalogowanego zostało zmienione Stare hasło jest nieprawidłowe, nowe hasła nie są zgodne: Wyświetlony zostaje komunikat o błędach 5.5. Dwie architektury W celu zbadania różnicy pomiędzy architekturą korzystającą z CQRS i Event Sourcing a klasyczną architekturą korzystającą bezpośrednio z jednej głównej bazy, stworzono dwie wersje systemu. Logika biznesowa tych systemów jest identyczna, gdyż była budowana niezależnie od całego systemu wykorzystując wzorce taktyczne Domain-Driven Design. Diagram obu architektur został przedstawiony na Rys. 5.5 i Rys. 5.6. Rys. 5.5 Architektura systemu korzystająca z wzorca CQRS 47

Rys. 5.6 Architektura systemu korzystająca z klasycznego podejścia Kontekst związany zarządzania produktami jest taki sam dla obu systemów. Występuje w nim portal pracownika, który łączy się bezpośrednio z bazą danych. Generuje zdarzenia, do których są zasubskrybowane inne części systemu. Serwis przetwarzający zdarzenia z portalu klienta ma za zadanie zaktualizować dane dotyczące sprzedanych kodów. Dla kontekstu Zamówienia, dla systemu korzystającego z CQRS i Event Sourcing można wyróżnić stronę zapisującą, która przetwarza zdarzenia z kontekstu zarządzania produktami oraz zdarzenia dotyczące płatności. Jest odpowiedzialna za przetwarzanie komend z portalu klientów. Przetwarzanie generuje zdarzenia, które zapisywane są do Event Storage. Po zapisaniu tych zdarzeń są one redystrybuowane do serwisu obsługującego sagi płatności oraz strony odczytującej, gdzie zdarzenia zostają zapisane w bazie odczytującej w sposób zdenormalizowany (jedna tabela dla jednego widoku w aplikacji). Portal klientów korzysta jedynie z tej bazy i generuje komendy. Dla systemu standardowej architektury kontekst zamówień jest uproszczony. Zdarzenia przychodzące z kontekstu zarządzania produktami oraz zdarzenia dotyczące płatności są przetwarzane przez specjalny serwis, który bezpośrednio aktualizuje bazę danych. Reakcja na działania użytkownika znajduje się w portalu klientów. Ma on dostęp do wspomnianej bazy danych. Portal generuje zdarzenia dotyczące złożonych zamówień. W obu przypadkach występuje serwis obsługujący sagi płatności. Jest to mechanizm, który nasłuchuje wielu zdarzeń. Stan po otrzymaniu każdego zdarzenia może się zmienić i jest on utrwalany pomiędzy kolejnymi przychodzącymi zdarzeniami z uwagi na czas, który może upłynąć pomiędzy kolejnymi zdarzeniami. [10] W przypadku omawianego systemu, serwis ten tworzy obiekt sagi, gdy przyjdzie zdarzenie złożonego zamówienia. Zdarzenie otrzymanej płatności pochodzi z systemu zewnętrznego. Po otrzymaniu takiego zdarzenia dla danego zamówienia sprawdzana jest zgodność płatności z kwotą zamówienia. Gdy jest poprawna, 48

generowane zostaje zdarzenie zapłacenia pełnej kwoty zamówienia. Po pewnym okresie czasu, poszczególne sagi mogą wygasnąć. Kiedy to nastąpi, saga jest usuwana oraz wygenerowane zostaje zdarzenie przekroczenia czasu oczekiwania na zapłatę. Dodatkowo stworzono aplikację, która imituje system płatności. Generuje zdarzenia płatności dla zamówienia, które następnie jest obsłużone przez serwis obsługujący sagi płatności. 5.6. Zdarzenia w systemie Tworzony system oparty jest o architekturę zdarzeń. Zdarzenia są przekazywane pomiędzy kontekstami związanymi. Każdy kontekst czeka na przychodzące zdarzenia, aby wykonać akcję z nią powiązaną np. kontekst związany zarządzania produktami może wywołać zdarzenie ProductAddedEvent, które zostaje obsłużone przez kontekst zamówień. W celu całkowitego odseparowania zdarzeń pomiędzy poszczególnymi kontekstami, niektóre zdarzenia są powtórzone w obu kontekstach. Przychodzące zdarzenia z kontekstu związanego zarządzania produktami są obsłużone przez kontekst związany zamówień i generują nowe zdarzenia w tym kontekście, które z kolei są wykorzystywane przez podejście Event Sourcing. Wszystkie zdarzenia obsługiwane w systemie przedstawiono w Tabeli 5.6. Tabela 5.6 Zdarzenia w systemie Zdarzenia dla kontekstu: Zarządzanie produktami ProductAddedEvent Zdarzenie odpowiada dodaniu nowego produktu do systemu ProductActivatedEvent Zdarzenie odpowiada aktywowaniu produktu ProductDeactivatedEvent Zdarzenie odpowiada deaktywowaniu produktu ProductInformationChangedEvent Zdarzenie odpowiada zmianie informacji na temat produktu (nazwy, systemu sprzedaży, gatunku gry, skróconemu opisowi, opisowi lub zmianie URL-ów do obrazków) ProductPriceChangedEvent Zdarzenie odpowiada zmianie ceny produktu (cenie podstawowej i marży) NewCodeAddedToProductEvent Zdarzenie odpowiada dodaniu nowego kodu produktu Zdarzenia dla kontekstu: Zamówienia ProductCreatedEvent Zdarzenie odpowiada za dodanie nowego produktu do systemu ProductActivatedEvent Zdarzenie odpowiada aktywowaniu produktu ProductDeactivatedEvent Zdarzenie odpowiada deaktywowaniu produktu ProductInformationChangedEvent Zdarzenie odpowiada zmianie informacji na temat produktu (nazwy, systemu sprzedaży, gatunku gry, skróconemu opisowi, opisowi lub zmianie URL-ów do obrazków) 49

Zdarzenia dla kontekstu: Zamówienia ProductPriceChangedEvent Zdarzenie odpowiada zmianie w cenie produktu (cenie podstawowej i marży) ProductCodeAddedEvent Zdarzenie odpowiada dodaniu nowego kodu związanego z produktem ProductCodesReservedEvent Zdarzenie odpowiada zarezerwowaniu kodu dla produktu dla złożonego zamówienia ProductCodesUnreservedEvent Zdarzenie odpowiada zniesieniu rezerwacji na kod dla produktu, którego zamówienie wygasło ProductCodeSoldEvent Zdarzenie odpowiada sprzedaży kodu dla produktu dla zamówienia, które zostało opłacone CartCreatedEvent Zdarzenie odpowiada stworzeniu nowego koszyka na produkty dla klienta zalogowanego i niezalogowanego CartItemAddedEvent Zdarzenie odpowiada dodaniu nowego produktu do koszyka CartItemQuantityChangedEvent Zdarzenie odpowiada zmianie ilości produktu w koszyku CartItemRemovedEvent Zdarzenie odpowiada usunięciu produktu z koszyka CartDisabledEvent Zdarzenie odpowiada zmianę statusu koszyka na nieaktywny CartUserAssignedEvent Zdarzenie odpowiada przydzieleniu koszyka, który należał do użytkownika niezalogowanego użytkownikowi zalogowanemu CodesAssignedEvent Zdarzenie odpowiada przydzieleniu kodów dla zamówienia OrderPlacedEvent Zdarzenie odpowiada stworzeniu nowego zamówienia OrderItemAddedEvent Zdarzenie odpowiada dodaniu nowego produktu do zamówienia OrderSubmittedEvent Zdarzenie odpowiada złożeniu zamówienia zamówienie gotowe do opłacenia OrderFullyPaidEvent Zdarzenie odpowiada zapłaceniu pełnej kwoty zamówienia PaymentMadeEvent Zdarzenie odpowiada zarejestrowaniu płatności dla zamówienia OrderExpiredEvent Zdarzenie odpowiada przekroczeniu czasu na zapłacenie zamówienia OrderCompletedEvent Zdarzenie odpowiada zmianie statusu zamówienia na zrealizowane OrderCancelledEvent Zdarzenie odpowiada zmianie statusu zamówienia na anulowane CodesAssignedEvent Zdarzenie odpowiada przyznaniu kodów do gier dla pojedynczego produktu dla danego zamówienia 50

6. IMPLEMENTACJA 6.1. Wykorzystywane technologie i narzędzia Oba projekty zostały zaimplementowane w języku C# wykorzystując platformę.net. Aplikacje webowe (portal pracowników i portal klientów) wykorzystują framework ASP.NET MVC 5. Każda baza danych wykorzystuje serwer baz danych Microsoft SQL Server. Dostęp do danych jest realizowany za pomocą narzędzi ORM. Pozwalają one na mapowanie klas.net na tabele w bazie danych pozwalając na łatwe zapisywanie i odczytywanie danych, eliminując ręczne pisanie skryptów SQL oraz kodu ADO.NET. Dla serwisu obsługującego sagi płatności jest to narzędzie ORM Entity Framework. W pozostałych przypadkach jest to NHibernate. Wybór ten dla klas domenowych jest podyktowany używaniem DDD pozwala na zachowywanie enkapsulacji poprzez tworzenie klas mapujących do pól chronionych klas domenowych. W celu dystrybucji zdarzeń użyty został serwer wymiany wiadomości RabbitMQ. Jest to projekt open source zaimplementowany w języku funkcyjnym Erlang. RabbitMQ rozróżnia kilka bytów: producenta program, który wysyła wiadomości, kolejkę miejsce, gdzie przechowywane są wiadomości otrzymane od producenta, konsument program, który oczekuje na wiadomości, znajdujące się w kolejce. RabbitMQ działa w dwóch trybach wysyłania wiadomości bezpośrednio do poszczególnej kolejki lub w trybie publikowania/subskrybenta, gdzie poszczególni konsumenci subskrybują do poszczególnych wiadomości, które publikuje producent. Dzięki czemu producent nie musi wiedzieć, gdzie wysyłana jest wiadomość. RabbitMQ używany jest w celu dystrybucji zdarzeń w systemie [32]. Kolejnym narzędziem wykorzystywanym w tworzonych systemach jest MassTransit. To open-source owy framework do tworzenia rozproszonych aplikacji w.net. Współpracuje z systemem kolejkowania RabbitMQ. Pozwala programiście konfigurować kolejki, wysyłać oraz odbierać wiadomości, konfigurować współbieżne odczytywanie i przetwarzanie wiadomości. Sprawia, że odczytywanie z kolejek jest niezawodne, gdyż przetwarzane wiadomości wywołujące błędy są przenoszone do kolejki błędnych wiadomości, z której mogą być ponownie przetworzone. MassTransit obsługuje też mechanizm Sagi. Zaimplementowano integrację z Quartz.NET w celu wysyłania wiadomości zgodnie z danym harmonogramem (np. o danej godzinie). W tworzonym systemie MassTransit tworzy kolejki, wysyła i odbiera wiadomości, jak również obsługuje płatności dla zamówień wykorzystując Sagę. Serwisy, które korzystają z MassTransit powinny być hostowane jako Usługa Systemu Windows. Stworzony projekt korzysta w tym celu z frameworka TopShelf [33]. 51

6.2. Zależności w utworzonym rozwiązaniu Diagram przedstawiony na Rys. 6.1 pokazuje zależności pomiędzy modułami w rozwiązaniu. W Tabeli 6.1 przedstawiony został spis wszystkich modułów wraz z krótkim opisem. Rys. 6.1 Diagram zależności bibliotek i modułów w stworzonym rozwiązaniu Tabela 6.1 Opis bibliotek oraz modułów w utworzonym rozwiązaniu Moduł / Biblioteka DddCommon SharedKernel WebTestPaymentPortal Opis / zawartość Biblioteka zawierająca klasy bazowe lub interfejsy wspólne dla wszystkich projektów, które zawierają klasy domenowe wykorzystujące Domain-Driven Design Biblioteka, zawierająca klasy wspólne dla wszystkich kontekstów związanych Portal pozwalający generować płatności dla zamówień ProductManagement kontekst związany Zarządzanie produktami ProductsManagement.Contracts Zawiera zdarzenia wychodzące z kontekstu związanego Zarządzanie produktami ProductManagement.Core Biblioteka zawierająca logikę biznesową oraz klasy domenowe ProductManagementService Usługa przetwarzająca zdarzenia przychodzące z kontekstu Zamówienia CqrsShop.Web Portal pracowników aplikacja internetowa pozwalająca na zarządzanie produktami i kodami 52

Moduł / Biblioteka Opis / zawartość Orders kontekst związany Zamówienia Orders.Common Zawiera klasy wspólne dla wszystkich projektów z kontekstu związanego Zamówienia Orders.Contracts Zawiera zdarzenia dotyczące kontekstu związanego Zamówienia OrdersSagasManager Usługa zarządzająca sagami zamówień. Reaguje na zdarzenia dotyczące stworzonego zamówienia i wykonywanych płatności Orders.Core Biblioteka zawierająca logikę biznesową oraz klasy domenowe Kontekst związany Zamówienia architektura CQRS + Event Sourcing QueryRepository Biblioteka udostępniająca możliwość odczytu oraz zapisu do bazy danych strony odczytującej OrdersManagementReadService Usługa przetwarzająca zdarzenia, które zostały wygenerowane przez OrdersManagementWriteService (stronę zapisującą). Odpowiada za stronę odczytującą z wzorca CQRS. Orders.Commands Biblioteka zawierająca wszystkie komendy, które wywołuje użytkownik Customer.Web Portal klienta aplikacja internetowa pozwalająca na przeglądanie, dodawanie do koszyka i zamawianie produktów przez klientów, dostosowana do architektury korzystającej z CQRS OrdersManagementWriteService Usługa przetwarzająca komendy z portalu klienta jak i zdarzenia pochodzące z innych kontekstów. Odpowiada za stronę zapisującą z wzorca CQRS Orders.WriteSideEventRepeater Program pozwalający na powtórzenie zdarzeń ze strony zapisującej, tak aby mogły być przetworzone jeszcze raz po stronie odczytującej. Pozwala na dowolne zmiany bazy danych strony odczytującej. Kontekst związany Zamówienia klasyczna architektura Customer.Web.Standard Portal klienta aplikacja internetowa pozwalająca na przeglądanie, dodawanie do koszyka i zamawianie produktów przez klientów OrdersEventReceiver Usługa przetwarzająca zdarzenia, pochodzące z innych kontekstów lub z usługi przetwarzającej sagi płatności 6.3. Model obiektowy Model obiektowy został utworzony mając na uwadze wzorce taktyczne Domain-Driven Design. Logika biznesowa trzymana jest w agregatach i jest oddzielna dla każdego kontekstu związanego lecz wspólna dla obu stworzonych architektur. Poniżej zostały przedstawione diagramy klas dla obiektów domenowych w tworzonym systemie. Biblioteka DddCommon zawiera klasy wspólne dla wszystkich bibliotek korzystających z DDD. Diagram klas dla tej biblioteki został przedstawiony na Rys. 6.2 oraz w Tabeli 6.2. 53

Tabela 6.2 Opis klas biblioteki Ddd.Common Nazwa klasy Opis Entity Klasa reprezentująca encję ValueObject Klasa reprezentująca obiekt wartościowy IDomainEvent Interfejs przedstawiający zdarzenie domenowe AggregateRoot Klasa reprezentująca korzeń agregatu główną encję agregatu IEventDispatcher Interfejs pozwalający na wysyłanie zdarzeń DomainEvent Statyczna klasa pozwalająca na wysyłanie zdarzeń, używana we wszystkich projektach generujących zdarzenia IRepository Interfejs mający podstawowe metody służące do zachowywania lub odczytu stanu ze źródła danych Repository Klasa reprezentująca repozytorium dla korzenia agregatu, wykorzystująca framework NHibernate Rys. 6.2 Diagram klas biblioteki Ddd.Common 54

Klasy z biblioteki SharedKernel są wspólne dla wszystkich kontekstów związanych. Diagram klas tej biblioteki jest przedstawiony na Rys. 6.3 i w Tabeli 6.3. Tabela 6.3 Opis klas biblioteki SharedKernel Nazwa klasy Opis GameGenre Typ wyliczeniowy przedstawiający gatunki gier SalePlatform Typ wyliczeniowy przedstawiający platformy sprzedaży, na której można wykorzystać zakupione kody Rys. 6.3 Diagram klas biblioteki SharedKernel Biblioteka ProductManagement.Core zawiera logikę biznesową oraz obiekty domenowe dla kontekstu związanego Zarządzanie produktami. Diagram klas wraz z opisem został przedstawiony na Rys. 6.4 i w Tabeli 6.4. Tabela 6.4 Opis klas biblioteki ProductManagement.Core Nazwa klasy Opis Product Klasa reprezentująca produkt w kontekście związanym zarządzania produktami, korzeń agregatu ProductCode Klasa reprezentująca kod do produktu, który można wykorzystać na danej platformie sprzedaży ProductService Serwis domenowy pozwalający na zarządzanie produktem ProductRepository Klasa pozwalająca na pobranie lub zapisanie produktu 55

Rys. 6.4 Diagram klas biblioteki ProductManagement.Core 56

Biblioteka Orders.Core zawiera logikę biznesową oraz obiekty domenowe dla kontekstu związanego Zamówienia. Diagram klas wraz z opisem został przedstawiony na Rys. 6.5 i w Tabeli 6.5. Tabela 6.5 Opis klas biblioteki Orders.Core Nazwa klasy Opis EsDddCommon::AggregateRoot Klasa bazowa dla korzeni agregatów Product ProductCode CodeStatus Cart CartItem Order OrderItem CartService OrderService wykorzystujących Event Sourcing Klasa produktu wykorzystywana w kontekście związanym Zamówienia Klasa reprezentująca kody przypisane do danego produktu Typ wyliczeniowy reprezentujący stan kodu Klasa reprezentująca koszyk na zakupy Klasa reprezentująca pozycję koszyka Klasa reprezentująca zamówienie złożone przez klienta Klasa reprezentująca pozycję zamówienia Klasa reprezentująca operacje dozwolone na klasie koszyku Klasa reprezentująca operacje dozwolone na klasie zamówienia 57

Rys. 6.5 Diagram klas biblioteki Orders.Core 58

6.4. Model fizyczny bazy danych Model danych został wygenerowany na podstawie klas wykorzystując framework NHibernate. Poniżej, na Rys. 6.6-6.9, przedstawiono modele fizyczne bazy danych dla poszczególnych kontekstów związanych i architektur. Rys. 6.6 Model fizyczny bazy danych dla kontekstu związanego Zarządzanie produktami Rys. 6.7 Model fizyczny bazy danych dla kontekstu związanego Zamówienia i standardowej architektury 59

Rys. 6.8 Model fizyczny bazy danych strony odczytującej dla kontekstu związanego Zamówienia i architektury CQRS Rys. 6.9 Model fizyczny strony zapisującej dla kontekstu związanego Zamówienia i architektury CQRS 60

7. TESTY WYDAJNOŚCIOWE 7.1. Przedmiot i cel badań Przedmiotem badań jest architektura wykorzystująca CQRS i Event Sourcing oraz standardowa architektura, nie korzystająca z tych wzorców. Celem badania jest ukazanie różnic w wydajności tych rozwiązań. W tym celu stworzono testy wydajnościowe wykorzystując program JMeter, generujący żądania do serwera. Przedstawiono platformę testową, metodę badania oraz wyniki badań. 7.2. Platforma testowa Testy wydajnościowe przeprowadzano na dwóch jednostkach obliczeniowych połączonych siecią lokalną. Diagram wdrożenia przedstawiony jest na Rys. 7.1. Wszystkie aplikacje, usługi Windows oraz baza danych SQL została uruchomiona na pojedynczym komputerze. W tabeli przedstawiono jej parametry. Rys. 7.1 Diagram wdrożenia testowanego systemu Tabela 7.1 Parametry komputera serwera Procesor Intel Core i7-2630qm @ 2.00GHz Pamięć RAM 8 GB DDR3 Dysk Twardy Samsung SSD 850 EVO 500GB System Operacyjny Windows 10 7.3. Metoda badania W programie JMeter utworzono dwie grupy użytkowników: przeszukujących treści oraz kupujących produkty na stronie. Uruchomiono dwa rodzaje testów wydajnościowych: Mały ruch sieciowy 30 użytkowników przeglądających oraz 10 użytkowników kupujących (łącznie 40 użytkowników), 61

Duży ruch sieciowy 120 użytkowników przeglądających oraz 30 użytkowników kupujących (łącznie 150 użytkowników). Scenariuszem testowym dla użytkownika przeglądającego było: 1. wejście na stronę główną, 2. wejście na spis sprzedawanych produktów, 3. wyszukanie produktu, 4. wejście na stronę z detalami produktu. Scenariusz dla użytkownika kupującego był następujący: 1. wejście na stronę główną, 2. zalogowanie się użytkownika, 3. wejście na spis sprzedawanych produktów, 4. wejście na stronę z detalami produktu, 5. dodanie produktu do koszyka, 6. przeglądnięcie koszyka, 7. zamówienie produktu, 8. wejście na stronę z spisem zamówień, 9. zrobienie płatności, 10. wejście na stronę z detalami zamówienia. Każdy z tych testów został uruchomiony przez 10 minut dla każdej z architektur. 7.4. Wyniki badań - porównanie wydajności dwóch architektur Wyniki z przeprowadzonych testów stanowią podstawę do porównania dwóch architektur. Rys. 7.2 Żądania na sekundę - standardowa architektura (40 użytkowników) 62

Rys. 7.3 Żądania na sekundę - architektura CQRS (40 użytkowników) Rys. 7.4 Żądania na sekundę - standardowa architektura (150 użytkowników) 63

Rys. 7.5 Żądania na sekundę - architektura CQRS (150 użytkowników) Dokonując analizy żądań na sekundę stwierdzono, że dla małej ilości użytkowników obie architektury przyjmują podobną liczbę żądań. Różnice pojawiają się w przypadku dużej ilości użytkowników. Dzięki zastosowaniu CQRS serwer oddelegowuje wykonywanie logiki biznesowej do innego serwisu, dlatego aplikacja przetwarza więcej żądań na sekundę. Uzyskany wzrost wydajności wyniósł ponad 50%. Wykresy dla poszczególnych architektur przedstawiono na Rys. 7.3 - Rys. 7.6. 64

Rys. 7.6 Opóźnienie odpowiedzi - standardowa architektura (40 użytkowników) Rys. 7.7 Opóźnienie odpowiedzi - architektura CQRS (40 użytkowników) 65

Rys. 7.8 Opóźnienie odpowiedzi - standardowa architektura (150 użytkowników) Rys. 7.9 Opóźnienie odpowiedzi - architektura CQRS (150 użytkowników) Wykresy opóźnień odpowiedzi ukazują, ile czasu zajęło serwerowi wykonanie żądania (czas połączenia + czas wykonania żądania). Porównując obie architektury zauważono, że już dla 40 użytkowników czasy opóźnień są kilkukrotnie większe. Jest to spowodowane tym, że serwer dla standardowej architektury przetwarza wszystkie żądania synchronicznie. 66

Dodatkowo standardowa architektura korzysta z znormalizowanej bazy danych wczytuje za każdym razem cały agregat, mimo że nie potrzebuje z niego wszystkich informacji do wyświetlenia. Wyniki badań przedstawiają Rys. 7.7- Rys. 7.10. Rys. 7.10 Czas odpowiedzi - standardowa architektura (40 użytkowników) Rys. 7.11 Czas odpowiedzi - architektura CQRS (40 użytkowników) 67

Rys. 7.12 Czas odpowiedzi - standardowa architektura (150 użytkowników) Rys. 7.13 Czas odpowiedzi - architektura CQRS (150 użytkowników) Dokonując analizy czasu odpowiedzi żądań zaobserwowano, że dla standardowej architektury dla 150 użytkowników czasy odpowiedzi żądań wynosiły do 2 sekund. Natomiast dla architektury korzystającej z CQRS 60 milisekund. Informacje te zawarto na Rys. 7.11 - Rys. 7.14. 68

Rys. 7.14 Percentyle czasu odpowiedzi dla żądań - standardowa architektura (40 użytkowników) Rys. 7.15 Percentyle czasu odpowiedzi dla żądań - architektura CQRS (40 użytkowników) 69

Rys. 7.16 Percentyle czasu odpowiedzi dla żądań - standardowa architektura (150 użytkowników) Rys. 7.17 Percentyle czasu odpowiedzi dla żądań - architektura CQRS (150 użytkowników) Rezultaty badań na Rys. 7.15 - Rys. 7.18 przedstawiają percentyle czasu odpowiedzi dla poszczególnych żądań. Określają, ile wyników (procentowo) było niższych lub wyższych od pewnego czasu odpowiedzi. Zagregowane wyniki zostały przedstawione w Tabeli 7.2. 70

Tabela 7.2 Zagregowane wyniki 90%, 95%, 99% percentyli dla poszczególnych architektur i liczby użytkowników Typ architektury Liczba użytkowników Percentyl 90% Percentyl 95% Percentyl 99% Standardowa 40 285 ms 483 ms 1230 ms CQRS 40 29 ms 35 ms 53 ms Standardowa 150 2663 ms 3132 ms 4336 ms CQRS 150 35 ms 44 ms 101 ms Rys. 7.18 Przepustowość w bajtach - standardowa architektura (40 użytkowników) 71

Rys. 7.19 Przepustowość w bajtach - architektura CQRS (40 użytkowników) Rys. 7.20 Przepustowość w bajtach - standardowa architektura (150 użytkowników) 72

Rys. 7.21 Przepustowość w bajtach - architektura CQRS (150 użytkowników) Porównując ilość bajtów odebranych z serwera przez 10 minut stwierdzono, że dla architektury wykorzystującej CQRS serwer przetwarza więcej żądań. Im więcej użytkowników tym architektura CQRS obsługuje więcej żądań, a tym samym zwiększa przepustowość serwera. Petar Rajković, Dragan Janković i Aleksandar Milenković podjęli próbę zwiększenia wydajności systemu aplikując wzorzec CQRS do istniejącego systemu medycznego. Jedną z metod badania było zbadanie ilości kilobajtów zwracanych przez serwer bazy danych [34]. Przeprowadzono podobne badanie dla zaimplementowanych architektur. Wyniki zostały przedstawione w Tabeli 7.3. Różnice w wielkości danych są spowodowane ilością potrzebnych agregatów, które muszą zostać wczytane, aby wyświetlić informacje użytkownikowi. Architektura CQRS dla strony odczytującej, w przeciwieństwie do standardowej architektury, korzysta z zdenormalizowanej bazy danych. Dzięki temu ilość zwróconych bajtów jest zdecydowanie mniejsza. Tabela 7.3 Ilość danych zwróconych przez serwer bazy danych dla obu architektur Standardowa Architektura CQRS Różnica Zapytanie architektura (strona odczytująca) [B] [KB] [B] [KB] [B] [KB] Detale produktu 4199 4,10 4030 3,94 169 0,17 Koszyk 40564 39,61 591 0,56 39973 39,04 Lista zamówień 41019 40,06 743 0,73 40276 39,33 Detale zamówienia 41019 40,06 1075 1,05 39944 39,01 73