⚔️ Moc i magia Domain-Driven Design w świecie Heroes III: EventStorming, Event Modeling, stawianie granic i wysoka jakość bez code review

Wykorzystane grafiki: Heroes of Might and Magic III, do której prawa ma firma Ubisoft Entertainment SA oraz Heroes III Board Game od Archon Studio.

🇬🇧 This post (divided into smaller chunks) is also available in English on LinkedIn (CLICK)

⚔️ Heroes of Domain-Driven Design

Ten wpis jest częścią serii, w której opowiadam Ci o stoczonych bitwach w realnych projektach z wykorzystaniem metodyki Domain-Driven Design. Tłumaczę wykorzystane podejścia poprzez analogie występujące w świecie Heroes III. Najwięcej skorzystasz, zaczynając od POPRZEDNIEGO POSTA.

Astrologowie ogłaszają - Event Modeling.

Dzięki odpowiedniemu planowaniu podczas Event Modelingu zaoszczędzisz setki godzin kodowania i zapewnisz jakość swoich projektów.

🤔 Jak grać, żeby wygrać?

Sposobów na zaprojektowanie systemu i modelowanie jest wiele. Jednak oczywiście nie każda metoda jest równie skuteczna. Gdyby istniało jedno uniwersalne rozwiązanie, wszyscy byśmy je stosowali.

To nie wiedza ekspertów jest przekładana na kod, ale zrozumienie developerów.

Ten cytat daje do myślenia. Kiedy eksperci domenowi funkcjonują w kategoriach procesów biznesowych, a programiści starają się upchnąć je do tabel w bazie danych, to mamy poważny problem. Developerzy są niczym dziecko chcące wepchnąć kwadratowy klocek do okrągłego otworu — to nie może się udać. A wszyscy myślą, że doszli do porozumienia, dopóki właściwie nie zwizualizujemy naszego pojmowania procesów w modelowanym systemie. Dopiero wtedy widać wielkie rozbieżności i ta świadomość pozwala nam zadawać odpowiednie pytania, aby lepiej zrozumieć, co trzeba wykonać.

Wycinek

Metody planowania oparte o wizualizację, jak EventStorming czy Event Modeling poprzez przerzucenie myśli na ścianę (czy MIRO Board) umożliwiają tzw. shared understanding. Zrozumienie poprzez zamodelowanie procesu zdarzeniami i zobrazowanie ich jest o wiele lepsze niż długie tekstowe opisy.

Mogliście to zaobserwować w poprzednim wpisie na przykładzie bohatera w tawernie i na mapie — łączenie tych kontekstów w jeden “tabelaryczny” model utrudniało czytanie i rozwój kodu. To, co miał w głowie programista, zupełnie rozmijało się z tym, jak operują eksperci w różnych kontekstach — oni w ogóle nie łączyli tych dwóch obszarów, a nawet nie byli ich świadomi.

Dlatego potrzebujemy metody planowania, która zminimalizuje rozdźwięk między wiedzą ekspertów, zrozumieniem developerów i finalnym kodem. To klucz do efektywnej współpracy wszystkich zaangażowanych stron.

📋 Kryteria dobrego planowania

Aby takie wspólne zrozumienie i ewoluowanie systemu wraz z rozwojem biznesu było możliwe, istotne jest użyć metody, dzięki której:

  • na początek skupisz się na istocie problemu, a nie rozwiązaniu i odkryjesz najbardziej skomplikowane miejsca;
  • spojrzysz na system z różnych perspektyw takich jak: backend, frontend, ux/ui, analityka, estymacje;
  • zobrazujesz procesy biznesowe, a nie tylko statyczne struktury danych;
  • zbudujesz model zrozumiały dla programistów, jak i dla osób nietechnicznych;
  • wyrazisz model w taki sposób, aby mógł być przełożony bezpośrednio do kodu;
  • zweryfikujesz kompletność założonych celów, wymagań biznesowych i przepływu danych w projekcie (unikniesz rozmów na ostatnią chwilę w stylu: “na to nie ma jeszcze designu” / “dodaj mi jeszcze to pole do tego API”)
  • zobrazujesz zależności między częściami systemu i zaplanujesz jakie prace można prowadzić równolegle;
  • oszacujesz czas potrzebny na wykonanie poszczególnych funkcjonalności, a także będziesz w łatwy sposób śledzić postęp projektu;
  • weźmiesz pod uwagę możliwe wektory zmiany, aby ich wprowadzenie było relatywnie tanie;
  • możesz eksperymentować z architekturą i walidować jej założenia na whiteboardzie, co jest nieporównywalnie tańsze niż zmiany w kodzie;
  • szybko wdrożysz do projektu nowych członków zespołu i przekażesz im jasny sposób pracy;
  • zapewnisz jakość jeszcze przed napisaniem pierwszej linijki kodu;
  • ustandaryzujesz implementację, co pozwoli na łatwe wdrożenie w projekt i automatyzację powtarzalnych czynności.

Dlaczego to takie ważne? Bo oszczędza mnóstwo czasu na niepotrzebnej robocie. A jak wiemy — czas to pieniądz!

♻️ Nie metoda, ale proces

Czy taka metoda, spełniająca wszystkie powyższe kryteria, w istnieje? Zaryzykuję stwierdzenie, że NIE… Czy da się określić, że któraś jest lepsza od innej? To zależy… dlatego najpierw spisaliśmy kryteria, na które będziemy zwracać uwagę.

Analogicznie, jak nie ma jednego uniwersalnego modelu dla wszystkich kontekstów, tak samo różne sposoby planowania i projektowania pozwalają patrzeć na system z różnych perspektyw i odpowiadać na inne pytania (pamiętasz przykład kolejki jednostek w walce?).

Dlatego warto mieć szeroki wachlarz możliwości i zastosować mix komplementarnych metod, aby osiągnąć wyznaczone dla planowania cele. Nadrzędną zasadą jest dla mnie, aby skupiać się nie na strukturach danych, ale zachowaniach systemu, z którego ostatecznie owe struktury także wynikną.

Znając Moc metodyki Domain-Driven Design i innych z nią związanych technik, warto dołożyć do tego trochę Magii. Magii? Nie wystarczy przeczytać jednego artykułu, czy słyszeć o jakimś narzędziu, żeby powiedzieć “umiem to stosować”. Owa Magia to intuicja, bazująca na wielu wykonanych projektach, której nie da się zamknąć w jakiejś skończonej liczbie kroków, ale podpowiada nam jak wszystko razem połączyć. Łącząc te dwa elementy proces wytwarzania oprogramowania, zaczyna się kształtować niczym układanka w Heroes III przy odkrywaniu kolejnych obelisków w poszukiwaniu Grala.

Wycinek

Odkrywanie kolejnych elementów układanki prowadzi nas do znalezienia najważniejszego artefaktu w świecie Heroes III - Graala.

Ważne jest, abyś opracował proces, który będziesz mógł przekazać współpracownikom i który będzie Cię prowadził do tych celów. W kolejnych wpisach będę stosował takie praktyki jak EventStorming, Event Modeling czy Context Mapping, aby dalej odkrywać fascynujący świat Heroes III. Pokażę Ci mój proces bazujący właśnie na tych technikach, dzięki czemu uzyskamy założone cele, spisane w powyższej liście kryteriów.

💡 Twoja perspektywa

Co Ty na to? Zgadzasz się z przedstawioną listą kryteriów? A może warto dodać jeszcze jakieś punkty? Co wg. Ciebie oznacza “dobre planowanie”? Daj znać w komentarzach! Nie piszę tego bloga, tylko żeby mówić, jak “masz żyć” / możesz pracować, ale nauczyć się czegoś też od Ciebie! Już w kolejnych akapitach wprowadzę Cię w mój proces tworzenia oprogramowania, a w kolejnych wpisach z chęcią wziąłbym pod uwagę aspekty, które Ty uważasz za ważne. Co było istotne w czasie planowania Twoich projektów? Albo, o czym zapomnieliście i nie skończyło się to dobrze?

🔸 EventStorming Big Picture

Software development is a learning process. Working code is a side effect.

~ Dan North

Jaka jest wartość dobrego-czystego kodu, napisanego na czas i w budżecie… ale przez kogoś, kto nie zrozumiał problemu do rozwiązania? Z pewnością równa 0… można taki program od razu wyrzucić do kosza.

EventStorming pomaga w procesie zwanym „knowledge crunching”, czyli wyciągania wiedzy od osób, które ją mają. Stosując tę metodę, będziesz w stanie wyssać wiedzę z głów ekspertów domenowych, znaleźć ich problemy i zaproponować rozwiązanie za pomocą znanych Ci technologii.

Ekspertami domenowymi nazywamy osoby, które są specjalistami w obszarze, jaki modelujemy. W branży medycznej może to być lekarz, przy paczkomatach będzie to kurier czy osoba na sortowni albo odbiorca paczek itd. W przypadku gry będą to autorzy jej zasad oraz osoby w nią grające.

Cytat z początku tego akapitu nieprzypadkowo znalazł się w książce Introducing EventStorming, bo właśnie ten proces odkrywania i nauki zasad rządzących domeną jest tutaj najistotniejszy. Wspomniana książka jest niezaprzeczalnie najlepszym źródłem, jeśli chcesz zgłębić podstawy EventStormingu prosto od Alberto Brandoliniego — twórcy tej metody. Nie patrz na to, że jest dokończona tylko w 70%. To nie ma znaczenia 😀. EventStorming ma tyle różnych składników i odmian, że ciężko byłoby postawić ostatnią kropkę. Przeczytałem ją w tej formie i naprawdę BYŁO WARTO!. Szczegółowy opis przeprowadzonego EventStormingu na przykładzie realnego kursu programowania znajdziesz w moim wpisie poniżej:

Dlatego nie będę wchodził teraz w detale, co zostało wykonane krok-po-kroku w przypadku domeny Heroes, ale zaprezentuję Ci wynik warsztatu. Pamiętaj też o tym, że najwięcej skorzystasz, nie patrząc na statyczną grafikę, ale biorąc udział w takich sesjach EventStormingu i dyskutując z innymi. Karteczka na ścianie jest jedynie wstępem do rozmowy i pogłębionej analizy.

 EventStorming Big Picture dla Heroes III.

EventStorming Big Picture dla Heroes III (wycinek tablicy).

Przeprowadziliśmy burzę mózgów i nanieśliśmy na tablicę (powyżej widzisz jej kawałek) zdarzenia występujące w analizowanej domenie. Korzystając z wiedzy ekspertów, wyznaczyliśmy też wstępnie procesy biznesowe, jakie zachodzą w świecie Heroes III. Takie jak: odmierzanie czasu, rekrutację jednostek czy prowadzenie bitew.

Następnie będziemy nanosić przepływ danych w systemie na widoczne zdarzenia i odkrywać ich przyczyny. Później sprawdzimy, czy nie brakuje nam żadnych danych i system jest możliwy do implementacji. To zakończy nasz proces “uczenia” czy “pochłaniania wiedzy domenowej”, dzięki czemu wykonanie działającego kodu będzie już tylko formalnością. Na sam koniec zdefiniujemy granice spójności i wszystko przełożymy na działający kod aplikacji.

🔹 Event Modeling (aka Single-flow Event Storming)

Co dalej po Big Picture EventStormingu? Bazując na moim doświadczeniu, chciałbym polecić Ci Event Modeling. Był używany przeze mnie przy wielu projektach w połączeniu z EventStormingiem, Context Mappingiem i opisanymi w poprzednim wpisie technikami. Z założeniami tej metody zapoznasz się m.in. w tym krótkim kursie. Dzięki połączeniu wspomnianych technik byłem w stanie opracować proces planowania, który spełnia wszystkie kryteria wymienione na liście powyżej. Z naciskiem na łatwość przełożenia projektu do działającego kodu. Po przeprowadzeniu Big Picture EventStorming przechodzę od razu do Event Modelingu (zazwyczaj nie stosuję EventStormingu Process i Design Level), aby zamodelować rozwiązanie dla wybranego procesu biznesowego.

🤔 Czy to tylko EventStorming + BPMN?

Niektórzy opisują Event Modeling jako EventStorming połączony z BPMN. Na bazie teoretycznych dyskusji, ciężko rozstrzygnąć, czy jest to dobre porównanie. Dlatego zaraz zobaczysz, ile da Ci ta metoda w praktyce, modelując proces z domeny Heroes III. Początkowo Event Modeling został zdefiniowany jako Single-flow Event Storming (więcej o tym możesz dowiedzieć się TUTAJ). Ja zazwyczaj zamiast Process i Design Level przeprowadzam właśnie Event Modeling — używając tej samej notacji, jaką zaproponował Alberto Brandolini.

🎯 Istota Domain-Driven Design

Najważniejszym zadaniem Domain-Driven Design jest pokonanie barier między “osobami technicznymi” a ludźmi biznesu / ekspertami domenowymi. Event Modeling wykorzystuje naturalną dla człowieka zdolność opowiadania historii (zamiast statycznych diagramów jak Design Level) i nie wymaga żadnych specjalnych kompetencji — w takim warsztacie może wziąć udział każdy, niezależnie od zajmowanego stanowiska. Tutaj nie stosujemy w notacji polityk czy agregatów, które wprowadzają zamęt w ludziach biznesu i łamią “naturalny bieg historii”. Pomimo tego możemy wyznaczyć agregaty (i nie tylko), a notacja jest wciąż przekładana 1 na 1 do kodu, co zobaczysz w kolejnych częściach tego wpisu.

🍕Event Modeling jest jak pizza?

Z EventStormingiem (jak powiedział jego autor: Alberto Brandolini) jest jak z pizzą: podstawa jest ta sama, ale każdy dodaje swoje ulubione składniki. Myślę, że tak samo można powiedzieć o Event Modelingu. Obserwując nawet kilka osób, które o nim piszą, możesz odnieść wrażenie “mieszania się w zeznaniach”. Nie istnieje jednak żaden standard czy książka, do której mógłbym Cię odesłać, istotne jest jedynie doświadczenie praktyków. Dlatego przejdźmy od razu do przykładu i pokażę Ci, w jaki sposób ja stosuję tę technikę w moich projektach. W międzyczasie upewnij się, że obserwujesz na LinkedIn osoby, które niemal codziennie dzielą się swoim doświadczeniem z Event Modelingiem:

Jeśli Ty biegle posługujesz się inną metodą: np. EventStorming Process Level czy Domain Storytelling i osiągasz podobne rezultaty, to też jest OK (nikt nie ma “monopolu na prawdę”). Koniecznie podziel się w komentarzu swoimi sposobami, abym ja i inni mogli rozbudowywać swój wachlarz możliwości!

👾 Przykład: proces rekrutacji jednostki

Nie pracujemy w waterfallu. Nie musisz przed startem implementacji wiedzieć już wszystkiego. W myśl zasady “small design up-front” zaczynamy modelować pierwszy proces, a potem kolejne. Wszystko składamy ze znanych także z EventStormingu building blocków:

  • Zdarzenie (ang. Event) - (pomarańczowa karteczka) - ważny biznesowy fakt, zmieniający stan systemu, powstaje w reakcji na Komendę;
  • Komenda (ang. Command) - (niebieska karteczka) - akcja w systemie, intencja zmiany, decyzja podjęta przez użytkownika albo inną część systemu;
  • Widok (ang. View / Read Model) - (zielona karteczka) - informuje użytkownika o stanie systemu, zmienia się w reakcji na Zdarzenie.

Dzięki temu, modelując proces rekrutacji jednostki z Heroes III, powstał poniższy diagram. W realnym projekcie umieścisz tam mockupy od UX/UI Designera, tak jak ja zrobiłem ze screenami z gry.

Event Modeling pozwala nam opowiedzieć historię jak film

Reprezentacja procesu rekrutacji jednostki. Event Modeling pozwala nam opowiedzieć historię jak film i połączyć wszystkie warstwy systemu: akcje użytkownika, projekty interfejsów, REST API, zapis danych itp. na jednym diagramie. Dzięki temu wiemy, że nie ma luk w wymaganiach. Wynikiem jest projekt, który przekładamy 1 do 1 na kod.
(Kliknij obrazek, aby powiększyć)

Aby przejść z BigPicture EventStormingu, do Event Modelingu musimy wykonać kroki przedstawione poniżej.

  1. Dla zidentyfikowanego procesu biznesowego ułóż zdarzenia chronologicznie, aby opowiadały historię. Możesz wziąć jedno zdarzenie i zastanowić się, co musi się wydarzyć przed nim i co po nim.
  2. Przyporządkuj do zdarzeń ich przyczyny, czyli komendy, które były ich przyczyną.
  3. Użytkownik wywołuje komendę na UI, dlatego nanieś elementy interfejsu użytkownika i w jaki sposób wywołują komendę. Zadbaj o to, aby interfejs wynikał z Twojego modelu domeny, a nie model z tego, jak wygląda UI (frontend zmienia się częściej niż reguły w Twojej domenie);
  4. Zdefiniuj Widoki (bazujące na zdarzeniach), aby wiedzieć jakich danych potrzebujemy do przekazania użytkownikowi (lub innej części systemu), na podstawie których będą podejmowane decyzje o wywoływaniu kolejnych komend.
  5. Do widoków przyporządkuj interfejs użytkownika, aby umożliwić mu podejmowanie kolejnych decyzji na podstawie wyświetlanych danych. Zazwyczaj jakieś projekty już są przygotowane, więc dobrze się nimi posiłkować budując wspólne rozumienie wraz z biznesem. Upewnisz się, że system ma wszystkie potrzebne do prezentacji dane.
  6. Do wszystkich elementów dodaj potrzebne do ich opisania atrybuty, aby zweryfikować czy informacje przepływające przez system są kompletne.

Tak wykonany EventModeling będzie opowiadał historię Twojego systemu, jak kolejne klatki w filmie. W szczegóły tej metody będziemy wchodzić już za chwilę i także w kolejnych wpisach na bardziej rozbudowanych przykładach.

🛂 Granice modelu i autonomia

Kiedy modelujesz, stawiaj sobie przed oczami główny cel, jakim są autonomiczne moduły. To, w jaki sposób je zaimplementujesz, jest kwestią drugorzędną. Do tego doszliśmy już w poprzednim wpisie serii TUTAJ. Jeśli chodzi o realizację, to moduł będzie np. pakietem w Twoim monolicie (nigdy! nie dziel projektu na services / controllers / models - może poza kilkoma wyjątkami jak przeglądarka do bazy danych, więcej TUTAJ) lub nawet osobnym mikroserwisem. Na tym etapie nie ma to znaczenia.

W jaki sposób upewnić się, że właściwie wyznaczyliśmy procesy i granice modułów? Skupmy się dalej na omawianym przykładzie procesu rekrutacji jednostki i jego wizualizacji za pomocą zdarzeń. Teraz wspólnie prześledzimy pytania i odpowiedzi, jakie padły na sesji modelowania, aby utwierdzić się co do tego, że zdarzenia AvailableCreaturesChanged i CreatureRecruited możemy wyizolować w osobnym module.

  • Co musi się wydarzyć przed rekrutacją jednostki (CreatureRecruited)? Musimy wiedzieć, ile jest aktualnie dostępnych jednostek do zrekrutowania (AvailableCreaturesChanged).
  • A co jeszcze wcześniej? Musi zostać wybudowane siedlisko jednostki w mieście, co powoduje oczywiście AvailableCreaturesChanged.
  • Czy to jedyny sposób na zmianę dostępnych jednostek? Nie, to się dzieje tylko raz przy zbudowaniu, potem w pierwszym dniu tygodnia (DayStarted { week = any, day = 1}) liczba się odnawia.
  • Czy zawsze nowy tydzień odnawia liczbę dostępnych jednostek o tyle samo? Zazwyczaj tak, ale są też Astrologowie, którzy mogą ogłosić, że symbolem tego tygodnia będzie np. Anioł — wtedy jego liczba jest większa.
  • A czy może się tak zdarzyć, że ta liczba się zmniejszy wraz z nowym tygodniem? Oczywiście! Jeśli jest to tzw. “tydzień plagi”, wszystkie niezrekrutowane jednostki znikają i liczba jest równa 0.

Zadając tego typu pytania, zidentyfikowaliśmy alternatywne wejścia do procesu rekrutacji, czyli de facto powody zaistnienia zdarzenia AvailableCreaturesChanged. Jest to kolejna heurystyka wyznaczania granic, komplementarna do omawianych perspektyw z poprzedniego wpisu. W konsekwencji, aby nasz model był autonomiczny i niepowiązany ściśle z np. procesem rozbudowy miasta, powinniśmy oddzielić go grubą kreską na zdarzeniu AvailableCreaturesChanged od reszty systemu — takie zdarzenie w EventStormingu możemy nazwać pivotal event.

W praktyce oznacza to, że inne modele czy moduły, które mają wpływ na dostępność jednostek (jak wybudowane budowle czy “astrologowie ogłaszają”) będą się komunikować z modułem rekrutacji, wywołując komendę (np. IncreaseAvailableCreatures) skutkującą wspomnianym zdarzeniem. Dzięki czemu, dodawanie kolejnych powodów zmiany dostępnych jednostek (jak np. artefakty bohatera), nie będzie wpływać na tę część systemu.

Wzorzec automatyzacji - Event Modeling.

Wzorzec automatyzacji - Event Modeling.

Na Event Modelingu możesz to zaprojektować jako wzorzec Automatyzacji (nazywany też To-Do List) widoczny powyżej (przedstawiany jako robot), który obserwuje zdarzenia Astrologers Proclaimed i kiedy spełniają day warunek (symbolem tygodnia jest jakaś jednostka), wykonuje komendę IncreaseAvailableCreatures. W ten sposób możesz integrować niezależne moduły, które w końcu nie funkcjonują w próżni i poprzez interakcje między nimi muszą realizować bardziej złożone procesy.

🏛️ Open-Closed Architecture

Odkryte zależności między zdarzeniami zobrazowałem na uproszczonym diagramie modułów poniżej. Spójrz na razie na jego lewą część. Widzisz, że zidentyfikowaliśmy juz 3 powody, dla których może wydarzyć się AvailableCreaturesChanged.

Astrologowie ogłaszają - Event Modeling.

Możliwy podział na moduły za pomocą identyfikacji zdarzeń i procesów zachodzących między nimi.

Nie chcemy, aby implementacja każdego powodu wymagała zmian w module Creatures Recruiting, który jest konceptem dosyć stałym i nie przewidujemy częstych zmian w jego prostej logice. Z racji innego tempa i motywacji zmian zamykamy go jako osobny komponent naszej aplikacji. Programista może teraz implementować taki moduł, nie martwiąc się kalendarzem czy astrologami, to wykona inny zespół a później jedynie użyje API wystawionego przez Creatures Recruiting do zmiany dostępnych jednostek w siedliskach za pomocą komendy: IncreaseAvailableCreatures.

Teraz spójrz na część prawą z wydzielonymi modułami Armies i Creatures Recruiting. Posłuchaj, skąd wzięły się takie granice:

  • Co się dzieje po zrekrutowaniu jednostki w siedlisku? Zostaje ona dodana do armii.

Po takiej odpowiedzi moglibyśmy skończyć pracę i w jednym module aplikacji zamknąć rekrutowanie i dodawanie do armii, brzmi logiczne. Ale jako modelarze musimy drążyć dalej i zadać ważne pytania o alternatywy, co prowadzi do odkrycia nowych procesów.

  • Czy to jedyny sposób na dodanie jednostki do armii? Nie, jest też możliwe, że podczas podróży na mapie jakieś jednostki okażą się stronnikami i do nas dołączą.
  • Skoro mogę dodawać jednostki, to mogę też je usuwać? Oczywiście, np. wynik bitwy może Ci dostarczyć informacji, ile jednostek straciłeś.

Tę rozmowę możnaby ciągnąć jeszcze dalej, ale już taki bieg wydarzeń pozwolił nam na zidentyfikowanie kolejnych modułów (jak np. Adventure Map) i wyznaczenia granic między nimi. Dzięki takiemu podziałowi zmniejszamy obniżenie kognitywne osób, które będa je realizować. Osoba odpowiedzialna za mapę nie musi znać szczegółów rekrutacji czy zarządzania armią. Będzie wiedziała jedynie, że kiedy jednostki dołączają, ma zlecić modułowi Armies ich dodanie poprzez komendę IncreaseAvailableCreatures. Analogicznie jak poprzednio z dostępnością jednostek: jeśli pojawią się kolejne sposoby na zwiększanie armii, jak np. zaklęcia, nie będzie to wymagało ani znajomości szczegółów implementacji innych modułów, ani wprowadzania w nich zmian.

Dzięki temu zyskujemy Open-Closed Principle na poziomie nie tylko klas, ale też naszej architektury, a praca może się toczyć szybciej, bo będziemy w stanie podzielić zespoły na niezależne strumienie.

Zapamiętaj: pytanie o to, co musiało się wydarzyć wcześniej/później oraz alternatywne wejścia i wyjścia z danego procesu są jedną z użytecznych heurystyk do identyfikacji granic modelu.

Czy to jednak nie jest złamanie zasady YAGNI (You aren’t gonna need it)? O tym często nadużywanym akronimie, będzie też w kolejnych częściach serii.

🗺️ Mapa Kontekstów

Jak widzisz, podział modułów nie jest tylko kwestią techniczną, ale jest ściśle powiązany z zarządzaniem projektem, terminami i organizacją pracy — implementacja autonomicznego modułu rekrutacji jednostek będzie niezależna od innych toczących się prac programistycznych. Np. moduł świadomy odwiedzającego miasto bohatera i posiadanych przez niego artefaktów będzie mógł zlecać zwiększenie lub zmniejszenie liczby dostępnych jednostek, bez znajomości szczegółów całego procesu rekrutacji. Umożliwia to zrównoleglenie prac i znaczne przyśpieszenie implementacji.

Taki podział możemy wyrazić, np. stosując technikę Mapy Kontekstów (ang. Context Mapping). Dzięki temu zobrazujemy też zależności między modułami i zespołami — kto będzie musiał się do kogo dostosować, a gdzie potrzebne są wspólne ustalenia i partnerstwo.

Astrologowie ogłaszają - Event Modeling.

Możliwy podział na moduły i relacje między nimi. Rozbudujemy i zweryfikujemy wraz z postępem planowania i analizy.

Więcej o mapowaniu kontekstów w praktyce i co z tym wspólnego ma Hydra, zobaczysz w kolejnym wpisie TUTAJ.

👨‍💻 Event Modeling: Przekładanie modelu 1 do 1 na kod

Niech przemówi kod! Teraz kiedy za pomocą opisanych heurystyk potwierdziliśmy (lub ulepszyliśmy) wcześniejsze założenia, przystępujemy do implementacji. Omówimy, w jaki sposób przekładać Event Modeling na kod, aby realizacja była już tylko formalnością.

Na razie zajmiemy się backendem w Kotlinie, ale wykorzystane wzorce są tak generyczne, że z mojego doświadczenia możesz ich użyć w Java, C#, JavaScript, TypeScript, Ruby, PHP i zapewne też innych językach, z którymi jeszcze nie miałem przyjemność pracować. Dzięki temu w Twój kod będą mogli z łatwością wchodzić programiści innych technologii, stosujący te same wzorce.

Do dzieła! Zobacz jak karteczki symbolizujące komendę i event przekładają się na klasy, a linia między nimi to czysta funkcja (ang. pure function) bez side effectów.

Na powyższym przykładzie rozpatrujemy stronę zapisu (write slice).

Na powyższym przykładzie rozpatrujemy stronę zapisu (write slice).
(Kliknij obrazek, aby powiększyć)

Na powyższym przykładzie widzisz dwa rodzaje sliceów (po polsku to chyba “wycinków”?) z Event Modlelingu:

  • Write Slice: Command -> Event
  • Read Slice: Event -> View

W tym poście skupimy się jedynie na tym pierwszym.

Użyta funkcja decide jest składową wzorca Decider, którego interfejs w Kotlinie wygląda jak poniżej. Stan (klasa Dwelling) jest wyliczany na potrzeby procesowania komendy z przeszłych zdarzeń, za pomocą funkcji evolve. Jeśli jeszcze nie miałeś okazji użyć tego wzorca, to najlepsze znane mi wprowadzenie znajdziesz tutaj Fraktalio (fmodel).

interface Decider<in Command, State, Event> {
    val decide: (Command, State) -> List<Event>
    val evolve: (State, Eevent) -> State
    val initialState: State
    
    // funkcje pomocznie, nie wsytępujące we wzorcu:
    fun decide(events: Collection<E>, command: C) = decide(command, evolve(events))

    private fun evolve(givenEvents: Collection<E>): S =
        givenEvents.fold(initialState) { state, event -> evolve(state, event) }
}

Decider w rzeczywistości służy nam do zapewnienia natychmiastowej spójności reguł i niezmienników systemu — czyli implementuje koncept Agregatu. Jest to bardziej funkcyjna forma, która umożliwia też bezproblemowe przełączanie się między sposobami utrwalania danych: Event Sourcingiem lub tradycyjnym snapshotem aktualnego stanu. Preferuję nawet tę nazwę niż wprowadzony przez Erica Evansa Aggregate, który mylnie sugeruje, że jedynie “agreguje” jakieś dane czy obiekty.

🎖️Zdarzenie obywatelem pierwszej kategorii

Tutaj zdarzenia są prawdziwymi first-class citizen. Skupiamy się na zachowaniach systemu, a stan jest szczegółem implementacyjnym — koniecznym, aby po właściwej sekwencji zdarzeń i wykonaniu komendy, nasz system zareagował odpowiednio kolejnym zdarzeniem. W Kotlinie zdarzenia i komendy modelujemy jak poniżej.

sealed interface DwellingCommand {
    val dwellingId: DwellingId

    data class RecruitCreature(
        override val dwellingId: DwellingId,
        val creatureId: CreatureId,
        val recruit: Amount
    ) : DwellingCommand

    data class IncreaseAvailableCreatures(
        override val dwellingId: DwellingId,
        val creatureId: CreatureId,
        val increaseBy: Amount
    ) : DwellingCommand
}

sealed interface DwellingEvent {
    val dwellingId: DwellingId

    data class CreatureRecruited(
        override val dwellingId: DwellingId,
        val creatureId: CreatureId,
        val recruited: Amount,
        val totalCost: Cost
    ) : DwellingEvent

    data class AvailableCreaturesChanged(
        override val dwellingId: DwellingId,
        val creatureId: CreatureId,
        val changedTo: Amount
    ) : DwellingEvent
}

Dzięki zastosowaniu zdarzeń zachowanie systemu specyfikujemy za pomocą testów, które mają powtarzalną, sformalizowaną i czytelną formę: given (przeszłe zdarzenia) -> when (komenda) -> then (oczekiwane zdarzenia). Jak widzisz, one także bazują na zdarzeniach. Nie zakładamy w nich stanu początkowego ani końcowego, ale jedynie zdarzenia poprzedzające komendę i zdarzenia oczekiwane po jej wykonaniu.

Na powyższym przykładzie rozpatrujemy stronę zapisu (write slice).

Aktualnie implementowany slice jest oznaczony na żółto, slice wykonany na zielono, a te jeszcze nie zaimplementowane na czerwono.
(Kliknij obrazek, aby powiększyć)

Dokładnie wyrażony w scenariuszu testowym slice z modułu Creature Recruitment (RecruitCreature -> CreatureRecruited) z rekrutacją Anioła:

private val portalOfGlory = dwelling(angelId, costPerTroop = resources(GOLD to 3000, CRYSTAL to 1))

@Test
fun `given Dwelling with 2 creatures, when recruit 2 creature, then recruited 2 and totalCost = costPerTroop * 2`() {
    // given
    val givenEvents = listOf(AvailableCreaturesChanged(dwellingId, angelId, changedTo = Amount.of(2)))

    // when
    val whenCommand = RecruitCreature(dwellingId, angelId, recruit = Amount.of(2))

    // then
    val thenEvents = portalOfGlory.decide(givenEvents, whenCommand)
    val expectedRecruited = CreatureRecruited(
        dwellingId,
        angelId,
        recruited = Amount.of(2),
        totalCost = Cost.resources(GOLD to 6000, CRYSTAL to 2)
    )
    assertThat(thenEvents).containsExactly(expectedRecruited)
}

Implementacja tego zachowania w kodzie produkcyjnym wygląda następująco:

fun dwelling(creatureId: CreatureId, costPerTroop: Cost): IDecider<DwellingCommand, Dwelling, DwellingEvent> = Decider(
    decide = ::decide,
    evolve = { state, event ->
        when (event) {
            is CreatureRecruited -> state.copy(availableCreatures = state.availableCreatures - event.recruited)
            is AvailableCreaturesChanged -> state.copy(availableCreatures = event.changedTo)
        }
    },
    initialState = Dwelling(creatureId, costPerTroop, Amount.zero())
)


private fun decide(command: DwellingCommand, state: Dwelling): List<DwellingEvent> =
    when (command) {
        is RecruitCreature -> {
            if (state.creatureId != command.creatureId || command.recruit > state.availableCreatures)
                emptyList()
            else
                listOf(
                    CreatureRecruited(
                        command.dwellingId,
                        command.creatureId,
                        command.recruit,
                        state.costPerTroop * command.recruit
                    )
                )
        }

        is IncreaseAvailableCreatures -> listOf(
            AvailableCreaturesChanged(
                command.dwellingId,
                command.creatureId,
                command.available
            )
        )
    }

Skoro atrybuty czy stan reprezentowany przez klasę Dwelling nie są istotne, to możemy ją dowolnie refaktorować i to w ogóle nie wpływa na testy! Dzięki temu podział atrybutów i znanych ze świata realnego konceptów na reprezentacje w różnych klasach wyniknie naturalnie. Jeśli dane sekwencje zdarzeń nie będą miały na siebie wpływu, to nie ma żadnego argumentu za tym, żeby łączyć je jeden strumień albo zapisywania danych, które nie wpływają na zachowania. Łączenie odczytów i zapisów w jeden model często niebywale komplikuje rozwiązanie. Dlatego wyświetlaniem zajmiemy się później, aby nie zaciemniać projektowanego modelu.

Dzięki zastosowaniu sealed interface do implementacji, to kompilator pilnuje, abyśmy obsłużyli wszystkie komendy (w funkcji decide) i zdarzenia (w funkcji evolve) implementujące dany interfejs. Całość kodu z tego przykładu znajdziesz TUTAJ.

Jeśli zapomnimy obsłużyć jakieś zdarzenie, to naszą pamięć odświeży kompilator.

Jeśli zapomnimy obsłużyć jakieś zdarzenie, to naszą pamięć odświeży kompilator.
(Kliknij obrazek, aby powiększyć)

⚖️ Reguły biznesowe i przykłady

Zapewne zastanawiasz się skąd w kodzie metody decide wzięły się ify. Albo inaczej: jak w Event Modelingu wyrazić reguły biznesowe? EventStorming miał na to specjalną żółtą karteczkę. Tutaj idziemy w stronę przykładów. Dzięki temu nawet biznes może Ci potwierdzić czy tak to ma działać przed napisaniem nawet 1 linijki kodu. Zmusi to także ekspertów domenowych do wymyślania kolejnych przykładów i pomoże Ci nawet zidentyfikować edge case, zamiast jedynie mądrze wyglądającego kiwania głowami, kiedy napiszesz regułę. Jest to w myśl podejścia Behavior-Driven Development, a specyfikacje przyjmują właśnie formę Given-When-Then. Poniżej przykład dla command RecruitCreature. Dla ułatwienia przyjąłem, że kiedy operacja nie jest dopuszczalna, nic się nie dzieje. Nie są produkowane żadne zdarzenia ani rzucane wyjątki.

Szczegółowe przykłady zachowań opisujące reguły biznesowe dla diagramu Event Modelingu.

Szczegółowe przykłady zachowań opisujące reguły biznesowe dla diagramu Event Modelingu.
(Kliknij obrazek, aby powiększyć)

Event Modeling pozwala nam zweryfikować kompletność informacji w naszym systemie. Kiedy spojrzysz na zdarzenia powyżej, to czy widzisz pewną lukę? Zastanów się chwilę…

Powstaje pytanie: skąd wiadomo, jaki jest koszt rekrutacji jednostki? Kiedy jest to określane? Nie ma tej informacji w żadnym zdarzeniu. Rozpatrzeniem tego przypadku zajmiemy się w dalszych częściach tej serii. Na potrzeby tego przykładu załóżmy, że koszt jednostki pochodzi z jakiejś “konfiguracji” albo jest “hardcodowany”.

🤖 ChatGPT? Zanieś, przynieś… wygeneruj testy!

Dodatkowo, dla modeli LLM banalnie proste jest wygenerowanie testów jednostkowych z tak przygotowanych scenariuszy. Możesz niesamowicie przyśpieszyć sobie pracę, stosując prompty jak ja w poniższym przykładzie. Na pewno da się to zrobić nawet lepiej :)

Generowanie testów jednostkowych z Event Modelingu za pomocą ChatGPT.

Generowanie testów jednostkowych z Event Modelingu za pomocą ChatGPT.
(Kliknij obrazek, aby powiększyć)

👼 Jakość bez Code Review? To możliwe!? Jeszcze jak!

Implementacja już gotowa, więc pojawia się naturalne pytanie: co z code review? Kiedy mowa o zapewnieniu jakości wytwarzanego oprogramowania, zazwyczaj pojawia się wątek, ile osób musi zaakceptować każdego Pull Requesta. Jednak w momencie Code Review jest już za późno na skupienie się na jakości, a przynajmniej na zrobienie tego relatywnie tanio.

Myśleć o jakości, gdy kod został już napisany — to tak jak w Heroes III analizować strategię bitwy, kiedy znany jest już jej wynik — Twoja przegrana.

Jak wygląda standardowy, uproszczony proces wytwarzania oprogramowania? Możesz zobaczyć poniżej (zakładamy, że pracujemy iteracyjnie — powtarzamy, to dla każdego zadania).

Astrologowie ogłaszają - modularyzacja

Code Review — jeśli spełnia swoje zadanie, to jest też przerywane implementacją, bo w końcu powinniśmy wprowadzić proponowane zmiany.

Bitwa o jakość na etapie planowania

Najważniejszą zmianą, jaką możesz zrobić, stosując Event Modeling (EventStorming Design Level, czy jakąkolwiek inną technikę zapewniającą podobne rezultaty) i wiedząc, jak karteczki przekładają się na kod… jest przesunięcie nacisku zapewnienia jakości z końca procesu, na jego początek. Możnaby to nazwać “design review” lub “architecture review”.

Kiedy mleko się już wylało, byłoby trzeba posprzątać i nalać nowe. Lepiej w ogóle nie doprowadzać do takiej sytuacji. Jaka to korzyść z code review, kiedy po 3 dniach pisania developer dowiaduje się, że trzeba zmieniać całą koncepcję? Nawet jeśli racja jest po stronie “recenzenta” to i tak już nikt nie zapłaci 2 razy za tą samą robotę.

Dodatkowo świadomość wykonawcy, że wszystko, co zrobił, nadaje się “do kosza”, może prowadzić do konfliktów w zespole, jeśli jest to odbierane osobiście: “on nie mówi, że mój kod może być lepszy, ale że to ja jestem złym programistą”. I mamy sytuację przegrana-przegrana.

Góra lodowa Code Review

Dlaczego tak się dzieje? Logiczne jest planować strategię walki po rozponaniu klasy problemu, ale jeszcze przed bitwą, a my jako programiści — robimy zupełnie na odwrót.

Code review jest jak góra lodowa. Jeśli nie mamy kontekstu sprawdzanego kodu, np. jesteśmy w innym zespole, znamy używane technologie, ale nie szczegóły procesu biznesowego — to na czym możemy się skupić? Czy formatowanie jest poprawne? Albo czy są jakieś testy? Nawet nie wiemy, co powinny testować… Takie sprawdzenia są niewiele warte i mogą, a nawet muszą zostać zautomatyzowane!

strologowie ogłaszają - modularyzacja

Gdzie więc leży jakość i tym samym pieniądze? Na samym dole naszej góry lodowej. Istotne jest zapewnić, aby nasz system był modularny, poprawnie realizował proces biznesowy i łatwosć wprowadzania późniejszych zmian - bo przecież o to chodzi “na koniec dnia”. Dzięki temu nasza aplikacja będzie mogła się rozwijać, a biznes więcej zarabiać.

Dzięki Event Modelingowi możesz zmarginalizować rolę code review. Nieporównywalnie tańsze jest przesunięcie karteczki, które trwa kilka sekund niż zmiana całej koncepcji w kodzie. Tym bardziej, gdy kod został już napisany i biznes zapłacił za jego powstanie kilka dni pracy developera! Wyobraź sobie “radość” Twojego kolegi z zespołu, gdy myśli, że skończył pracę, a Ty uświadamiasz go, że tak naprawdę wszystko jest do zmiany. Dlatego przeprowadzaj eksperymenty i sprawdzaj atrybuty jakościowe swojego modelu na whiteboardzie zamiast już wykonanym programie.

Niepopularna opinia: A może bez code review?

Jeśli Twój zaprojektowany model może być przetłumaczony jednoznacznie do kodu, to code review staje się już tylko formalnością weryfikacji wykonania wcześniej opracowanego modelu zgodnie z ustaleniami. Wtedy proces wytwarzania oprogramowania ulega zmianie: wydłuża nam się część planowania, ale sama implementacja jest o wiele sprawniejsza. Jedynie egzekwujemy czy w kodzie jest to, co wcześniej razem z zespołem zaprojektowaliśmy.

Astrologowie ogłaszają - modularyzacja

Wiedza o modelowaniu pozwala na szybkie eksperymenty i sprawdzanie jakości modelu na whiteboardzie. Dzięki temu unikniesz rzucania się sobie do gardeł podczas code review.

W bardziej zdyscyplinowanym zespole code review może w ogóle nie być potrzebne, szczególnie jeśli dojdzie do tego pair-programming i feature flagi. Oczywiście zakładając, że kształtujecie swój sposób pracy i macie nad nim pęłną kontrolę, a nie jest to np. projekt open source. Tym samym podniesiecie jakość oprogramowania i zaoszczędzicie mnóstwo czasu programistów, a czas — to pieniądz, czyli czysty win-win. Będziecie realizować już wcześniej zaplanowaną strategie walki, która doprowadzi Was do wygranej waszego projektu.

🕒 Modelowanie 24h/7

Nie czekaj z ćwiczeniem modelowania na kolejny wpis! Wyjdź teraz z domu i obserwuj, jak działa świat przesiąknięty technologią wokół Ciebie:

  • Wybierasz się w podróż pociągiem. Jak zamodelowałbyś proces rezerwacji biletów? Jak zadbasz o to, aby było to wydajne i nie było możliwości dwóch pasażerów na 1 miejsce? Lepiej niż w PKP :) ?
  • Publikujesz ofertę pracy. W jaki sposób powinno to wyglądać? Czy ofertę zawsze trzeba dodać przez kreator? A może ktoś ją importuje z excela? Jakie nowe akcje są możliwe na opublikowanej ofercie?
  • Wsiadasz na rower miejski. Czy wiesz jakie procesy tam zachodzą? Jak jest zorganizowany odbiór rowerów zostawionych poza stacjami?

Zakładam, że nie programujesz (jeszcze!) hipernapędu czy teleportu, więc zapewne inni rozwiązali już podobne problemy, jakie Ty teraz napotykasz. Ćwicz swoją intuicję, wystawiaj się na różne przypadki, a po czasie zaczniesz zauważać analogie w Twoich projektach. Oczywiście lepiej robić to razem niż samemu… dlatego, jeśli chciałbyś dalej ze mną przechodzić przez modelowanie Heroes III, zapisz się na listę mailingową TUTAJ. Zawsze otrzymasz informacje o nowym wpisie, a także zaproszenia na spotkania LIVE, gdzie razem będziemy dalej eksplorować domenę Heroes III i przekładać ją na kod.

A może chcesz wprowadzić Event Modeling w Twojej organizacji? Skontaktuj się ze mną najlepiej na LinkedIn albo pisz email na: mateusz@nakodach.pl

❤️ Inni też tym żyją

Jeśli już teraz chcesz dowiedzieć się więcej, o planowaniu bazującym na założeniach Event Modelingu i przekładaniu diagramów na kod, to zobacz:

2025 Mateusz Nowak | Polityka prywatności| Regulamin sklepu