Sourcing zdarzeń, jedno zdarzenie, stan dwóch agregatów zmieniony

10

Próbuję nauczyć się DDD i powiązanych tematów. Wpadłem na pomysł prostego, ograniczonego kontekstu, aby zaimplementować „bank”: istnieją konta, pieniądze można wpłacać, wypłacać i przenosić między nimi. Ważne jest również, aby zachować historię zmian.

Zidentyfikowałem podmiot konta i pozyskanie tego zdarzenia byłoby dobrze śledzić zmiany w nim. Inne byty lub obiekty wartości nie mają znaczenia dla problemu, więc nie wspomnę o nich.

W przypadku wpłat i wypłat - jest to stosunkowo proste, ponieważ zmodyfikowano tylko jeden agregat.

Podczas transferu jest inaczej - dwa agregaty muszą zostać zmodyfikowane przez jedno zdarzenie MoneyTransfers . DDD przestaje modyfikować modyfikowanie wielu agregatów w jednej transakcji. Z drugiej strony zasada pozyskiwania zdarzeń polega na stosowaniu zdarzeń do podmiotów i modyfikowaniu stanu na ich podstawie. Gdyby zdarzenie mogło być przechowywane po prostu w bazie danych, nie byłoby problemu. Aby jednak nie dopuścić do jednoczesnej modyfikacji jednostek pochodzących ze zdarzeń, musimy zaimplementować coś zmieniającego strumień zdarzeń każdego agregatu (aby zachować granice transakcji). Z wersjonowaniem wiąże się kolejny problem - nie mogę używać prostych struktur do przechowywania zdarzeń i odczytywania ich z powrotem w celu zastosowania ich do agregacji.

Moje pytanie brzmi - jak mogę połączyć te trzy zasady: „jedna agregacja jedna transakcja”, „zdarzenie-> zmiana agregacji” i „jednoczesne zapobieganie modyfikacjom”?

cocsackie
źródło

Odpowiedzi:

7

Podczas transferu jest inaczej - dwa agregaty muszą zostać zmodyfikowane przez jedno zdarzenie MoneyTransfers.

Przekazywanie pieniędzy to odrębny czynność od aktualizacji ksiąg rachunkowych.

MoneyTransferred
AccountCredited
AccountDebited

Ćwiczenie, które w końcu mnie uwolniło, uświadomiło sobie, że AccountOverdrawnjest to zdarzenie, opisuje stan konta bez względu na innych uczestników tej wymiany, więc musi istnieć polecenie uruchomienia konta, które je tworzy.

Nie można racjonalnie wyprowadzić stanu jak AccountOverdrawnz modelu odczytu, ponieważ prawdopodobnie nie możesz wiedzieć, czy widziałeś już wszystkie zdarzenia - tylko sam agregat ma pełny widok historii w danym momencie.

Odpowiedź oczywiście znajduje się w tym wszechobecnym języku - konta są uznawane lub obciążane w celu odzwierciedlenia zobowiązań banku wobec klientów.

W porządku, ale oznacza to, że powinienem również wykorzystywać zdarzenia AccountCredited i AccountDebited do wpłat i wypłat, więc rejestruję nie tylko przyczynę zmiany, ale zmianę spowodowaną przez inne działanie. Gdybym chciał cofnąć akcję, nie mogłem, ponieważ nie wszystkie zdarzenia są zarejestrowane.

Nie jestem do końca pewien, co następuje, ponieważ masz (w przypadkach takich jak ten) naturalny identyfikator korelacji, który jest samym identyfikatorem transakcji.

Po drugie - oznacza to, że muszę użyć czegoś takiego jak saga.

Nieco inna pisownia: potrzebujesz czegoś takiego, jak człowiek, który wydaje właściwe polecenia .

Istnieją co najmniej dwa sposoby, aby to zrobić. Jednym z nich byłoby, aby subskrybent nasłuchiwał MoneyTransferredi wysyłał dwa polecenia do ksiąg rachunkowych.

Inną alternatywą byłoby śledzenie przetwarzania transakcji jako oddzielnego agregatu - pomyśl o tym jako o liście kontrolnej wszystkich rzeczy, które należy wykonać od czasu transakcji. W związku z tym MoneyTransferredmoduł obsługi zdarzeń wysyła ProcessTransaction, który planuje wykonanie pracy i sprawdza, jakie prace zostały zakończone.

VoiceOfUnreason
źródło
W porządku, ale oznacza to, że powinienem również używać zdarzeń AccountCredited i AccountDebited do wpłat i wypłat, więc rejestruję nie tylko przyczynę zmiany, ale zmianę spowodowaną przez inne działanie. Gdybym chciał cofnąć akcję, nie mogłem, ponieważ nie wszystkie zdarzenia są zarejestrowane. Jak mogę to zrobić (przyczyna zdarzeń)? Po drugie - oznacza to, że muszę użyć czegoś takiego jak saga. Jak zatem modelować transfer? W tej chwili mam metodę przelewu na konto. Po wywołaniu publikuje wydarzenie MoneyTransfers . Nie wiem od czego zacząć coś takiego jak saga.
cocsackie
Prawda -> AccountCredited i AccoundDebited następnie MoneyTransferred ? Pierwsze rozwiązanie aktualizuje zarówno agregatów w jednej transakcji (bez gwarancji konsystencja dowolnego rodzaju)? Nie ma również agregatu, który mógłby opublikować MoneyTransfers -> brak korelacji. Drugie rozwiązanie wydaje się lepsze - ProcessTransaction może publikować MoneyTransfers i aby uniknąć wielokrotnej modyfikacji agregacji w jednej transakcji, mogę publikować zdarzenia z Konta po dokonaniu transakcji. Przepraszam za bycie wybrednym. Trudno jest zrozumieć początkującym - nie można użyć tylko jednego wzoru bez drugiego.
cocsackie
1

Ważny szczegół w zrozumieniu rachunków transakcyjnych: balanceatrybut accountjest w rzeczywistości przykładem denormalizacji. Jest tam dla wygody. W rzeczywistości saldo konta jest sumą jego transakcji i tak naprawdę nie potrzebujesz samego konta, aby mieć saldo.

Mając to na uwadze, przekazanie pieniędzy nie powinno polegać na aktualizacji, accountale na wkładaniu transaction.

Biorąc to pod uwagę, istnieje jeszcze jedna ważna zasada: czynność dodawania a transactionpowinna być atomowa z aktualizacją (zdormalizowanego pola bilansu) account.

Teraz, jeśli rozumiem pojęcie agregatów DDD, wydaje się, że istotne są następujące kwestie:

Agregacja jest logiczną granicą rzeczy, które mogą ulec zmianie w transakcji biznesowej w danym kontekście. Agregat może być reprezentowany przez pojedynczą klasę lub przez wiele klas. Jeśli więcej niż jedna klasa stanowi agregat, wówczas jedną z nich jest tak zwana klasa główna lub jednostka. Cały dostęp do agregatu z zewnątrz musi odbywać się za pośrednictwem klasy root.

Pod względem projektu DDD sugerowałbym:

  1. Istnieje jeden agregat reprezentujący przeniesienie

  2. Agregat składa się z następujących obiektów: transfer (obiekt główny); obiekt główny jest powiązany z dwiema listami transakcji (po jednej dla każdego konta); a każda lista transakcji jest połączona z jednym kontem.

  3. Cały dostęp do transferu powinien medytować obiekt główny (the transfer).

Jeśli próbujesz wdrożyć obsługę transferu asynchronicznego, Twój główny kod powinien martwić się o utworzenie transferu w stanie „oczekującym”. Możesz mieć inny wątek lub zadanie, które faktycznie przenosi pieniądze (wkładanie do historii transakcji, a tym samym aktualizowanie sald) i ustawia przelew na „zaksięgowany”.

Jeśli chcesz wdrożyć blokującą transakcję transferu w czasie rzeczywistym, logika biznesowa powinna utworzyć transferobiekt, który koordynowałby inne działania w czasie rzeczywistym.

Jeśli chodzi o zapobieganie problemom z współbieżnością, pierwszym zleceniem powinno być wstawienie transakcji debetowej do listy transakcji dla konta źródłowego (oczywiście aktualizacja salda). Musiałoby to być wykonywane atomowo na poziomie bazy danych (za pomocą procedury składowanej). Po zaksięgowaniu obciążenia reszta przelewu powinna zakończyć się powodzeniem bez względu na problemy z współbieżnością, ponieważ nie powinna istnieć żadna reguła biznesowa uniemożliwiająca uznanie konta docelowego.

(W prawdziwym świecie konta bankowe mają koncepcję postu z notatkami, który obsługuje koncepcję leniwego zatwierdzania dwufazowego. Utworzenie tego postu jest lekkie i łatwe, a także można je bez problemu wycofać. Konwersja memo post na twardy post ma miejsce, gdy pieniądze faktycznie się poruszają - nie można tego przywrócić - i reprezentują drugą fazę zatwierdzenia dwufazowego, występującą dopiero po sprawdzeniu wszystkich reguł sprawdzania poprawności).

John Wu
źródło
0

Jestem również na etapie uczenia się. Z punktu widzenia realizacji, właśnie tak czuję, że wykonasz tę akcję.

Dispatch TransferMoneyCommand, który wywołuje następujące zdarzenia [MoneyTransferEvent, AccountDebitedEvent]

Uwaga: zanim pojawi się te zdarzenie, należy przeprowadzić powierzchowną weryfikację poleceń i weryfikację logiki domeny, tj. Czy konto ma wystarczającą saldo?

Utrwal zdarzenia (z wersją), aby upewnić się, że nie występują problemy ze spójnością. Zauważ, że może istnieć inne równoległe polecenie (takie jak wycofanie wszystkich pieniędzy), któremu udało się odnieść sukces i zapisać zdarzenia przed tym, więc bieżący stan agregatu może być nieaktualny, a zatem zdarzenia są wywoływane w starym stanie i są niepoprawne. Jeśli zapisywanie zdarzeń się nie powiedzie, będziesz musiał ponowić polecenie od początku.

Po pomyślnym zapisaniu zdarzeń w bazie danych możesz opublikować dwa zdarzenia, które zostały zgłoszone.

AccountDebitedEvent usunie pieniądze z konta płatnika (zaktualizuje stan zagregowany i wszelkie powiązane modele widoku / projekcji)

MoneyTransferEvent uruchamia Saga / Process Manager.

Zadaniem menedżera sagi / procesu będzie próba uznania rachunku odbiorcy, jeśli to się nie powiedzie, będzie musiał zwrócić saldo z powrotem płatnikowi.

Menedżer Saga / Procesu opublikuje CreditAccountCommand, który jest stosowany do rachunku odbiorcy, a jeśli zakończy się powodzeniem, zostanie wygenerowany AccountCreditedEvent.

Z punktu widzenia pozyskiwania zdarzeń, jeśli chcesz cofnąć to działanie, wszystkie zdarzenia w tej transakcji będą miały identyfikator korelacji / związku przyczynowego jako oryginalny TransferMoneyCommand, którego możesz użyć do wywołania zdarzeń dla operacji cofania / cofania.

Możesz sugerować wszelkie problemy lub potencjalne ulepszenia powyższego.

Shayan C.
źródło