Event sourcing - pytania

1

Mam parę pytań o event sourcing, ponieważ to hasło znam głównie nazwy, z tekstów na stronach. Generalnie o event sourcing w ogóle nie myślę na co dzień, ale również coraz cześciej spotykam coraz więcej haseł, które nawiązują do event sourcing i chciałbym chociaż trochę (poprzez tą dyskusję) zrozumieć podstawowy tok rozumowania, jaki prowadzi do tego, by w projekcie zastosować event sourcing.

Zebrałem kilka pytań, jakby ktoś mógł na nie odpowiedzieć to byłoby super :)

  1. W jakich okolicznościach zrodziła się ta technika? Jaki główny problem miała rozwiązać?

  2. W jakim momencie zaczyna się event sourcing? Czy np jak mam usługi, które komunikują się poprzez wysyłanie wiadomości po kolejce (zamiast przez bazę) to już jest event sourcing?

  3. Na niektórych stronach event sourcing trochę sprowadza się do budowania pipeline i nie rzadko przy użyciu funkcyjnych języków. Dlaczego oprogramowanie zmierza w tym kierunku? Domyślam się, że pipeline łatwiej się skaluje, bo dokłada się kolejne workery, ale czym to różni się od typowego skalowania? Chodzi o jakąś kompozycyjność, której nie dostrzegam?

  4. Z event sourcing trochę powiązana jest wzorzec/technika CSRQ. Tzn osobno projektujemy zadania do odczytu, osobno do zapisu. Z czego to tak naprawdę wynika, co tracimy/zyskujemy idąc w CSRQ? Chodzi o to, że odczyty są ze slaves, a zapisy lecą do mastera?

  5. Kiedy event sourcing bardziej przeszkadza niż pomaga? Jakie nowe problemy wprowadza ta technika?

  6. Ostatnie hasło jakie kojarzy mi się z event sourcing to bitemporal. Rozumiem, że jakaś baza wtedy ma wgląd do historycznych danych, tzn nie nadpisuje starych danych, lecz tworzy nową wersję - w czym to jest lepsze od postgresql i robienia insertów zamiast updateów?

1

Ad. 4 ES jest dodatkiem do CQRS. Mogę sobie wyobrazić CQRS bez ES, ale trudno mi sobie wyobrazić ES bez CQRS
Ad. 6 Masz pełną historię a nie tylko ostatni stan

1
  1. Jest wtedy, jeśli baza eventów jest głównym source of truth i możesz z niej w każdym momencie odbudować obecny stan
7

ad 4/troche off-top

Największym problemem z CQRS jest to, że sporo osob uważa CQRS za jednoznaczne z skomplikowanymi machanizmami replikacji, duplikacji struktur danych, maperów, koniecznie potrzebna jest też Kafka i doktorat.
W rzeczywistości po prostu umieszczasz metody które zwracają dane bez ich modyfikacji w jednej klasie/interfejsie a metody które mają skutki uboczne w innej klasie/interfejsie - co właściwie jest zgodna z rozsądkiem segregacja kodu.

3
  1. W jakich okolicznościach zrodziła się ta technika? Jaki główny problem miała rozwiązać?

Pytanie trochę bez sensu bo tak jak w większości przypadków nie ma jednoznacznego punktu w czasie kiedy dany wzorzec/technologia się zrodziła. Problemy jaki rozwiązuje to np. zapewnienie "only source of truth" w rozproszonym systemie napędzanym zdarzeniami (event-driven) czy też audyt wszystkiego co zachodzi w systemie. Dodam jeszcze że niewiarygodnie to ułatwia dochodzenie błędów np. w produkcji ponieważ patrząc na sam audyt zdarzeń można zobaczyć co dokładnie się stało.

  1. W jakim momencie zaczyna się event sourcing? Czy np jak mam usługi, które komunikują się poprzez wysyłanie wiadomości po kolejce (zamiast przez bazę) to już jest event sourcing?

Nie, wtedy masz tzw. event-driven architecture. Niestety oba pojęcia są często mylone. Event sourcing masz wtedy kiedy to budowaniu stanu obiektu "odgrywasz" eventy powiązane z tym obiektem, zamiast zapisywać i ładować ten obiekt w całości. Jakiekolwiek zmiany stanu (np. aktualizacja nazwiska obiektu Person) są reprezentowane w postaci eventów.

  1. Na niektórych stronach event sourcing trochę sprowadza się do budowania pipeline i nie rzadko przy I życiu funkcyjnych języków. Dlaczego oprogramowanie zmierza w tym kierunku? Domyślam się, że pipeline łatwiej się skaluje, bo dokłada się kolejne workery, ale czym to różni się od typowego skalowania? Chodzi o jakąś kompozycyjność, której nie dostrzegam?

Nie wiem o co chodzi z tym pipeline. Podejrzewam że chodzi o moduły reagujące na zdarzenia, ale to ma więcej wspólnego z event-driven niż z ES.

  1. Z event sourcing trochę powiązana jest wzorzec/technika CSRQ. Tzn osobno projektujemy zadania do odczytu, osobno do zapisu. Z czego to tak naprawdę wynika, co tracimy/zyskujemy idąc w CSRQ? Chodzi o to, że odczyty są ze slaves, a zapisy lecą do mastera?

Tak jak kolega wyżej napisał chodzi o CQRS. Jest to potrzebne ponieważ- jak już wyżej wspomniałem- w ES stan obiektu budowany jest na postawie eventów, a więc ładowanie eventów dla każdego obiektu kiedy np. chcemy załadować listę obiektów do widoku byłoby niezbyt wydajne. Tak więc w ramach CQRS można budować model na podstawie eventów w systemie, i model ten nie musi odzwierciedlać obiektów które te eventy wywołały. Można tworzyć np. model dla konkretnego widoku.

  1. Kiedy event sourcing bardziej przeszkadza niż pomaga? Jakie nowe problemy wprowadza ta technika?

Kiedy na przykład mamy prostsze aplikacje, nie mamy event-driven architecture (co nie znaczy że przy event-driven trzeba zawsze używać ES) czy też nie ma potrzeby/nie można przechowywać audytu zdarzeń zachodzących w systemie.

  1. Ostatnie hasło jakie kojarzy mi się z event sourcing to bitemporal. Rozumiem, że jakaś baza wtedy ma wgląd do historycznych danych, tzn nie nadpisuje starych danych, lecz tworzy nową wersję - w czym to jest lepsze od postgresql i robienia insertów zamiast updateów?

Pierwszy raz spotykam się z tym określeniem.

6

Jaki główny problem miała rozwiązać?

Taki że nie gubisz informacji w systemie. Update czy Delete znane z CRUDa, zmieniają aktualny stan, ale jednocześnie nigdzie nie ma "historii" (o ile sam jej nie zaimplementujesz), co więcej nie ma też nigdzie informacji "co" spowodowało zmianę. Wiesz tylko że aktualny stan to X.
Wyobraź sobie taki klasyczny przypadek koszyka sklepowego. Możesz dodawać i usuwać przedmioty a "baza" trzyma aktualny stan koszyka. Po finalizacji zakupów w bazie masz stan koszuka w chwili zakupu, więc wiesz co użytkownik kupił.
Ale nie wiesz już np. jakie przedmioty użytkownik tam wkładał i wyjmował, a może warto? Może chciał kupić X ale nie ma pieniędzy chwilowo i warto mu zaproponować za jakiś czas kupno X? No ale w modelu z bazą nie możesz, bo w ogóle nie masz tej informacji.
Może np. użytkownik dorzucił do koszuka przedmiot Y bo gdzieśtam na stronie była reklama/rekomendacja. Znów, nie wiesz, bo w bazie masz tylko informacje co kupiono, a nie skąd ten przedmiot wziął się w koszyku.

Alternatywnie, w ES zapisujesz sobie listę zdarzeń które doprowadziły do pewnej sytuacji -> wiesz że użytkownik dodał a potem usunął X z koszyka, wiesz że dodanie Y do koszyka wywołał event powiązany z kliknieciem w rekomendacje etc. Nic nie stoi na przeszkodzie żeby zrobić replay wszystkich eventów i np. przepchać je przez jakąś nową logikę.

Zauważ ze sama "historia" nie wystarczy, bo nadal brakuje ci informacji o tym "co spowodowało zmianę"!

1

Zamiast wózka sklepowego ja wolę przykład z magazynem, którego używał sam Greg Young:

  • Produkty wjeżdżają do magazynu i są skanowane (dodawane do DB)
  • Produkt jest umieszczany na półce
  • Produkt opuszcza magazyn

I teraz sytuacja, co jeśli jakaś paczka nie została zeskanowana na wjeździe? W większości CRUDów, trzeba przerwać pracę, pójść na zaplecze, dodać produkt, wrócić i wysłać dalej. W przypadku event sourcingu bardziej istotny jest fakt zeskanowania paczki, nie wysyłasz zapytania "dodaj paczkę X", tylko "zeskanowałem paczkę X w celu wysłania jej" (zauważ, że stwierdzasz fakt, który już jest, nie prosisz, mówisz, że coś się stało). System wtedy wewnętrznie musi wiedzieć co zrobić by system działał. Zakładamy, że nie da się zeskanować nieistniejącej paczki, więc takowa musiała istnieć wcześniej, więc całe "ręczne" dodawanie jej jest bez sensu bo przecież ona fizycznie stoi przed nami, więc musi tutaj być.

0

No fajnie to brzmi w teorii ale praktycznego coś znaleźć w miarę prosto zrobionego to nie łatwo.

https://github.com/asc-lab/dotnet-cqrs-intro/tree/master/CqrsWithEs

Analizując krok po kroku co tu się dzieje utknąłem w AggregateRoot w metodzie:

private void ApplyChange(Event @event, bool isNew) {
            this.AsDynamic().Apply(@event);
            if (isNew) _changes.Add(@event);
        }

W jakim celu są takie konstrukcje?

chodzi mi głównie o PrivateReflectionDynamicObject;

Bo tak klasa dla mnie to jakiś overengineering.

0

To wygląda jak jakiś koszmarny helper pozwalający na ustawianie prywatnych rzeczy przez refleksję. Fajna rzecz, ale nie gdy mówimy o DDD, w którym encje powinny być niemutowalne. A do tego ten DynamicObject, to to jest niechybny gwałt na silnym typowaniu, więc lepiej się od tego trzymać z daleka.

2

Panowie (i panie?), nad czym się tu rozwodzicie? Przecież to jest dosyć proste, a komentarze typu witamy w DDD świadczą tylko o arogancji wynikającej z niewiedzy i braku zrozumienia. Swoją drogą to chyba najgorszy rodzaj arogancji, coś w stylu "lepiej być głupim i milczeć niż odezwać się i potwierdzić swoją głupotę".

Po pierwsze: po co mieszać DDD do dyskusji która tyczy się event sourcingu?
Po drugie: nie znam kontekstu z którego wyrwany jest kod podany przez @szydlak, ale zakładam że znajduje się on w klasie bazowej dla agregatów, a więc dostarcza pewną funkcjonalność tak aby agregaty dziedziczące (a więc modele biznesowe) mogły skupić się na warstwie biznesowej, i niczym innym. Agregaty oparte o ES zazwyczaj właśnie mają metody o nazwie Apply lub Rehydrate, lub obie, które są przeciążone i przyjmują eventy tak aby mogły zbudować swój stan. Np. agregat który przyjmie i przetworzy CustomerCreated z Name "Waldek" i CustomerAddressChanged z Address "Waldkowa 123" będzie miał stan Name: Waldek, Address: Waldkowa 123. Chodzi o to że typów agregatów można mieć wiele, a logika w bazowym AggregateRoot aplikuje eventy dynamicznie, zakładając że klasa dziedzicząca dostarcza metodę obsługującą dany event (w przeciwnym razie event nie powinien w ogóle trafić do agregatu, a jeśli trafia to mamy problem gdzieś w warstwie infrastruktury).

Z perspektywy użytkownika tej metody, wygląda to mniej więcej tak:

class Customer extends BaseAggregateRoot
{
    string name;
    string address;

    public Customer(string name)
    {
        ApplyChange(new CustomerCreated(name), true);
    }

    public ChangeAddress(string address)
    {
        ApplyChange(new CustomerAddressChanged(address), true);
    }

    public void Apply(CustomerCreated e)
    {
        this.name = e.Name;
    }

    public void Apply(CustomerAddressChanged e)
    {
        this.address = e.Address;
    }
}

Warstwa infrastruktury z kolei operowała by na takiej zasadzie:

var events = //get events for aggregate
var aggregate = //create aggregate instance

foreach (var event in events)
{
    aggregate.ApplyChange(event, false);
}

Fajna rzecz, ale nie gdy mówimy o DDD, w którym encje powinny być niemutowalne.

To jest zwyczajnie nie prawda. To że coś nie może być zmodyfikowane z zewnątrz (brak publicznych seterrów) nie oznacza że jest niemutowalne.

1
Aventus napisał(a):

Agregaty oparte o ES zazwyczaj właśnie mają metody o nazwie Apply lub Rehydrate, lub obie, które są przeciążone i przyjmują eventy tak aby mogły zbudować swój stan. Np. agregat który przyjmie i przetworzy CustomerCreated z Name "Waldek" i CustomerAddressChanged z Address "Waldkowa 123" będzie miał stan Name: Waldek, Address: Waldkowa 123. Chodzi o to że typów agregatów można mieć wiele, a logika w bazowym AggregateRoot aplikuje eventy dynamicznie, zakładając że klasa dziedzicząca dostarcza metodę obsługującą dany event

I do tego jest potrzebny PrivateReflectionDynamicObject, który wrapuje coś biznesowego, trzyma wewnętrznie jako object i coś tam bruździ refleksją?

To jest zwyczajnie nie prawda. To że coś nie może być zmodyfikowane z zewnątrz (brak publicznych seterrów) nie oznacza że jest niemutowalne.

Masz rację, to był mój skrót myślowy odnoszący się do sytuacji, którą zauważyłem.
Nie tyle niemutowalne, co nie powinny być modyfikowane z zewnątrz. No ale na szczęście jest refleksja, więc można sobie wszystko elegancko zmienić, i co najważniejsze - ani kompilator ani IDE tego nie pokaże. Wspaniałe programowanie, zgodne z najnowszymi wzorcami. :D :D :D

I ja nie neguję wzorca jako takiego, mam po prostu wrażenie, że ktoś nie bardzo ogarnia statyczne typowanie i uprawia drutowanie zamiast zrobić porządnie.

2

@somekind

I do tego jest potrzebny PrivateReflectionDynamicObject, który wrapuje coś biznesowego, trzyma wewnętrznie jako object i coś tam bruździ refleksją?

Nie przeglądałem kodu, odnosiłem się tylko do fragmentu wklejonego wyżej. Faktycznie, wrapowanie tego wydaje się bez sensu skoro wystarczy zrobić rzutowanie na dynamic. Co nie zmienia faktu że nie ma to nic wspólnego z DDD, a poza tym jest udziwnieniem jakiejś konkretnej osoby. Więc tamto konkretne rozwiązanie nie ma również nic wspólnego z ES.

Nie tyle niemutowalne, co nie powinny być modyfikowane z zewnątrz. No ale na szczęście jest refleksja, więc można sobie wszystko elegancko zmienić

W przypadku metody ApplyChange chodzi o możliwość zaaplikowania eventu na jakiejś konkretnej instancji (to ES, nie DDD). Sam obiekt bazowy jest agnostyczny odnośnie jaka to konkretna instancja. Agregaty w DDD domyślnie działają na zasadzie command in, event out, gdzie event sygnalizuje zmianę stanu obiektu. Stąd też ES i DDD idą często w parze.

I ja nie neguję wzorca jako takiego, mam po prostu wrażenie, że ktoś nie bardzo ogarnia statyczne typowanie i uprawia drutowanie zamiast zrobić porządnie.

Ja natomiast nie bronię kodu w tamtym linku (nawet go nie widziałem). Twierdzę jedyne że dynamiczne aplikowanie eventów nie jest niczym niezwykłym, szczególnie jeśli mamy mieć kod skupiony na domenie biznesowej a nie technicznych szczegółach. To jak te dynamiczne aplikowanie jest zaimplementowane to już inna sprawa.

Myślę że to sprowadza się do tego co już napisałem- podany przykład w żaden sposób nie powinien rzutować na DDD ani ES. Natomiast sama idea aplikowania eventów na konkretnej instancji przy ładowaniu obiektu jest jak najbardziej normalna w ES.

0
Aventus napisał(a):

Faktycznie, wrapowanie tego wydaje się bez sensu skoro wystarczy zrobić rzutowanie na dynamic.

No tak, to faktycznie dużo poprawi. ;]

W przypadku metody ApplyChange chodzi o możliwość zaaplikowania eventu na jakiejś konkretnej instancji (to ES, nie DDD).

Więc niech ta instancja ma po prostu taką metodę.

To jak te dynamiczne aplikowanie jest zaimplementowane to już inna sprawa.

No jak dla mnie bardzo istotna, bo wsrywanie dynamic do kodu i jeżdżenie po biznesowym kodzie refleksją to proszenie się o kłopoty, i to nie zależnie czy to jest ES z DDD, czy DDD bez ES, czy ADM z TS, czy cokolwiek innego.

2

Ok już rozumiem o co Ci chodzi. Instancje mają przeciążone metody dla konkretnych eventów (sprawdź przykładowy kod który podałem). I owszem, można by w sposób jawny załadować kolekcję eventów implementującą IEvent, a następnie sprawdzać konkretny typ każdego eventu, rzutować i wywoływać odpowiednią metodę na instancji. Rzeczy tylko w tym że będziesz musiał pisać wiele kodu, i to dla każdego typu obiektu. Przy złożonych i rozproszonych systemach gdzie możesz mieć dziesiątki lub setki agregatów oraz setki lub tysiące eventów będziesz musiał powielać kod, zalewając go technicznymi szczegółami zamiast tym na czym ten kod powinien się skupiać- logice biznesowej. Zamiast tego stosuje się podejście agnostyczne, gdzie załadowane eventy są dynamicznie aplikowane do konkretnej instancji. Jest to całkowicie standardowe podejście, nie ma w tym nic niezwykłego ani nie jest proszeniem się o kłopoty bo to się stosuje i świetnie się to sprawdza. Rozumiem że dla kogoś kto widzi to pierwszy raz może się to wydawać dziwne, ale jest tak dla tego że to po prostu coś nowego (nowe dla czytelnika, nie w branży). Dla mnie też kiedyś przejście z WinForms na MVC wywoływało dyskomfort, bo w WinForms mogłem w IDE jednym kliknięciem nawigować do handlera kontrolki, a w MVC nagle się okazało że te "połączenie" nie istnieje w kodzie, bo akcja w HTML idzie po requescie HTTP do kontrolera.

1
Aventus napisał(a):

Ok już rozumiem o co Ci chodzi. Instancje mają przeciążone metody dla konkretnych eventów (sprawdź przykładowy kod który podałem). I owszem, można by w sposób jawny załadować kolekcję eventów implementującą IEvent, a następnie sprawdzać konkretny typ każdego eventu, rzutować i wywoływać odpowiednią metodę na instancji.

Myślę, że nie trzeba rzutować ani sprawdzać konkretnych typów, jeśli mamy ogarnięte interfejsy i klasy bazowe.

Zamiast tego stosuje się podejście agnostyczne, gdzie załadowane eventy są dynamicznie aplikowane do konkretnej instancji. Jest to całkowicie standardowe podejście, nie ma w tym nic niezwykłego ani nie jest proszeniem się o kłopoty bo to się stosuje i świetnie się to sprawdza.

Jak dla mnie, to ten problem powinien zostać rozwiązany generycznie, a nie dynamicznie.

Czy implementacja ES w językach nieposiadających dynamicznego typowania (dajmy na to Java i Scala) jest niemożliwa? Jakoś nie wydaje mi się.

0

@somekind:

Myślę, że nie trzeba rzutować ani sprawdzać konkretnych typów, jeśli mamy ogarnięte interfejsy i klasy bazowe.

Możesz w wolnym czasie podać jakiś pseudokod który by to wyjaśniał? Coś jako alternatywa do kodu który podałem wcześniej. Jestem szczerze ciekaw, bo wydaje mi się że to co proponujesz nadal nie skalowałoby się.

Jak dla mnie, to ten problem powinien zostać rozwiązany generycznie, a nie dynamicznie.

Znów- mogę prosić o przykład?

Czy implementacja ES w językach nieposiadających dynamicznego typowania (dajmy na to Java i Scala) jest niemożliwa? Jakoś nie wydaje mi się.

Jedną z alternatyw dla rzutowania na typ dynamiczny jest użycie jakiegoś lookup list z typem eventu i delegatem do handlera. Tyle tylko że rozwiązanie z typem dynamicznym jest bardziej zgrabne i wymaga znacznie mniej kodu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1