Grafika została złożona ze zrzutów ekranu, zrobionych podczas rozgrywki w grę Heroes of Might and Magic III, do której prawa ma firma Ubisoft Entertainment SA.
🇬🇧 This post (divided into smaller chunks) is also available in English on LinkedIn (CLICK)
Mógłbym to wszystko opisać na przykładzie kina czy koszyka zakupowego? Na pewno! Jednak po co wciąż wałkować ten sam temat…? Czas wejść w świat bohaterów, magii, elfów i innych fantastycznych stworzeń.
Poniżej widzisz kawałek Event Modelingu autonomicznego modelu rekrutacji jednostek w Heroes III. Ale jak doszliśmy do tego momentu!? I co w tym przypadku oznacza “model” i to jeszcze “autonomiczny”!? Przecież nikt nie rekrutuje Aniołów na samym początku gry… Tak samo nikt nie wie od razu, jak wygląda dany proces biznesowy i jakie abstrakcje użyć do jego wyrażenia.
Strudzony skomplikowanym programowaniem Herosie! Czas wyjść z tawerny i zagłębić się w świat mocy (sprawdzonych wzorców i heurystyk) oraz magii (zwanej też intuicją, wynikającą z doświadczenia) modelowania w myśl Domain-Driven Design. 🧙♂️
Reprezentacja procesu rekrutacji jednostki.
Event Modeling pozwala nam opowiedzieć historię jak film i połączyć różne 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. Zobrazowane zależności pokazują, w jakich miejscach praca może toczyć się równolegle. Wynikiem jest projekt, który przekładamy 1 do 1 na kod i nie tracimy czasu na dyskusje podczas code review.
(Kliknij obrazek, aby powiększyć)
Mój pan od fizyki nie raz mawiał, że po rozwiązaniu setek zadań z rysowaniem wektorów, teraz wszędzie widzi działające na świat siły. Spotykając różnorodne rozwiązania IT na każdym kroku, mój mózg działa podobnie. Nawet kiedy stoję w kolejce do fast-fooda, to w mojej głowie już rozrysowują się potencjalne procesy i interakcje między modułami systemów obsługujących zamawianie jedzenia i organizację kuchni.
Odpowiednie moduły pomogą Ci odkryć szanse na nowy produkt i potencjalne miliony dla przedsiębiorstwa. W XXI wieku software jest nierozerwalnie związany z biznesem i może być motorem jego rozwoju (lub zupełnie przeciwnie — ostatnim gwoździem do trumny).
Niedawno po kilku latach przerwy odpaliłem Heroes III i do razu zacząłem zastanawiać się, w jaki sposób zaprogramowałbym coś podobnego na bazie moich doświadczeń z komercyjnych projektów (nie programuję gier, ale automatyzuję procesy biznesowe, takie jak: publikacją ogłoszeń o pracę, praca w call center i w banku itp.).
Wróciły też flashbacki z moich pierwszych komercyjnych projektów i błędów, jakie popełniałem, podchodząc do programowania, bez właściwego procesu zdobywania wiedzy (ang. knowledge crunching) i planowania. Choć to tylko (w tym przypadku aż!) gra, to można w niej znaleźć wiele analogii do realnych procesów biznesowych, które będę odkrywał razem z Tobą (pierwsze już w tym wpisie) i przekładał na kod.
Statystyki bohatera wpływają na jednostki w bitwie, a wynik bitwy przecież na armię bohatera. Bohatera możemy zatrudnić w tawernie, która może być zbudowana w mieście. Armię za to rekrutujemy w siedliskach jednostek, które mogą, ale nie muszą być w mieście. Tawerna zresztą tak samo. Dostępność jednostek odnawia się, co tydzień. No, chyba że astrologowie ogłaszają tydzień plagi. Jeszcze oczywiście musimy mieć na ich zakup zasoby, które są zbierane na mapie… Aaaa… i pamiętaj też rozwijać bohatera! - to tylko kawałek podanych wymagań, gdyby spytać “eksperta” o procesy, jakie zachodzą w świecie Heroes III.
Wyobraźcie sobie wyraz twarzy mojej żony, kiedy chciałem, aby zagrała ze mną i pierwszy raz tłumaczyłem jej zasady gry jako taki “ekspert”. Wymiękła po chwili… Nie inaczej jest, kiedy tzw. biznes próbuje nam przekazać swoje wymagania co do projektu, jeśli nie będziemy mieli głowy na karku. Dlatego naszym zadaniem (i odpowiedzialnym obowiązkiem) jest przeprowadzenie procesu knowledge crunching, czyli zdobycia wiedzy domenowej od osób, które ją mają. Musimy nauczyć się zadawać właściwe pytania w odpowiednim momencie, aby nie dać się przytłoczyć wiedzą ekspertów. Na szczęście istnieją do tego sprawdzone metody, opracowane przez community Domain-Driven Design. Zastosuję niektóre z nich, a więcej znajdziesz na GitHubie DDD Crew. Ten wpis jest pierwszym z serii. W kolejnych będziemy wchodzić na wyższy poziom Gildii Magów i bardziej zagłębiać się w przywołane techniki.
Model to nie kopia realnego świata, ale jego reprezentacja zrobiona w danym celu: rozwiązania konkretnego problemu i odpowiedzenia na specyficzne pytania. Jako programiści ciągle tworzymy tzw. abstrakcje, aby móc zrozumieć i zaimplementować wymagania projektowe. Jednak z różnych modeli (czy abstrakcji) korzystasz codziennie, może nawet nie zdając sobie z tego sprawy. Gdy jedziesz samochodem, to linie tworzące drogę na nawigacji nie są przecież tą ulicą, po której rzeczywiście jedziesz, ale odpowiadają Ci na pytanie: “jak najszybciej dojechać do celu”.
W takim razie, jaki to “dobry model”? Zależy, kto pyta. Zamawiając kuchnię na wymiar, otrzymałem dwa modele: jeden rysunek techniczny (po lewej) i drugi w 3D (po prawej). Jeśli chcę zobaczyć, jak zgrywają się kolory i wyobrazić sobie czy układ urządzeń będzie optymalny — to model 3D jest właściwy. Jednak dla osoby, która ma wycinać i składać meble będzie on bezużyteczny, bo nie ma na nim podanych wymiarów. To przykład na 2 różne reprezentacje tej samej rzeczywistości — obie użyteczne, ale w różnych kontekstach.
Model jest właściwy tylko w danym kontekście.
Czy wiemy już, jaki to “dobry model”? Wciąż można powtórzyć jak mantrę, standardową odpowiedź konsultanta: “to zależy”. Jednak ważne jest wiedzieć — od czego zależy? Od tego, jakie jest pytanie.
Daleko nie trzeba szukać, ponieważ świetnego przykładu dostarcza nam poniższy screenshot z Heroes III, gdzie możemy zauważyć dwie reprezentacje tej samej “rzeczywistości”: walki między armiami jednostek.
Oczywiście słowo “rzeczywistość” nie jest tutaj do końca odpowiednie w przypadku gry, jednakże metody Domain-Driven Design, mogą być stosowane i tutaj. W dzisiejszych czasach jest to naturalne — różne biznesy (np. portale z ogłoszeniami pracy, sklepy internetowe, banki etc.) są tak ściśle związane z oprogramowaniem, że mijaniem się z prawdą byłoby stwierdzić: jako programiści modelujemy tylko coś “realnego”, istniejącego poza komputerami.
Dany model jest wyspecjalizowany do odpowiedzi na konkretne pytania.
Przedstawiony ekran pokazuje jednocześnie dwa różne modele tej samej sytuacji w walce, ale odpowiadające na inne pytania.
Złożone z sześciokątów (podobnie jak kwadraty w szachach), po których poruszają się jednostki. Rzut oka na ten model pozwala odpowiedzieć na pytania takie jak:
Widoczny na dole, obrazujący kolejne ruchy jednostek i rundy walki.
Dany model jest wyspecjalizowany do odpowiedzi na konkretne pytania.
Tutaj mamy bardzo wyspecjalizowany model, służący praktycznie do odpowiedzi na jedno pytanie:
Dzięki takiej reprezentacji, jak na dłoni mamy odpowiedź, którą co prawda udałoby nam się wyciągnąć z poprzedniego modelu, ale z dużo większym nakładem pracy. Wymagałoby to od nas analizy statystyk każdej z jednostek (jej szybkości), co i tak by nie wystarczyło, bo wpływ na kolejność ruchu mają też np: posiadane przez bohatera artefakty czy rzucone czary. Dodatkowo jednostka może w każdej turze czekać, co sprawia, że ląduje na końcu danej rundy — bez widocznej kolejki każdy z graczy musiałby to spamiętać.
Model jest znacznie uproszczony w stosunku do pola walki i zawiera mniej informacji (np. nie musimy znać siły ataku jednostek — bo nie ma to wpływu na kolejność ruchu), ale lepiej spełnia swoje zadanie.
W nomenklaturze Event Sourcingu możnaby powiedzieć, że model Battle Queue to projekcja zdarzeń takich jak CreatureMoved
, CreatureAttacked
, CreatureWaited
, SpellCasted
etc.
W oryginalnej wersji gry był dostępny tylko widok pola walki (bez paska inicjatywy). Jednak kolejne rozszerzenia / mody do gry dodały tę funkcję na podstawie doświadczeń graczy. To przykład na zautomatyzowanie procesu, który wcześniej był wykonywany w głowie graczy, zabierając mnóstwo czasu i radości z gry.
Może w Twojej firmie też są np. Excele, na których tracone jest mnóstwo czasu i pieniędzy, dopóki ktoś (np. Ty) tego nie zautomatyzuje? Jeśli masz taką możliwość, to warto przesiąść się na jeden dzień poza dział IT i podsłuchać, na co narzekają osoby, których pracę masz upraszczać swoim oprogramowaniem. Być może znajdziesz HotSpoty szybciej, niż przyklejając karteczki na EventStormingu!
Czy da się wszystkie konteksty zobrazować przy pomocy jednego modelu? Tak często pokazują Ci tutoriale czy dokumentacja dla danego frameworka (niemożliwe jest streścić wszystkie książki o dobrych praktykach w takim miejscu). Niestety nawet deweloperzy z wieloletnim doświadczeniem wciąż próbują to powtórzyć w aplikacjach z rozbudowanymi procesami biznesowymi. I to się sprawdza, do momentu, kiedy zaczynasz w końcu gubić się w tym gąszczu ifów. Tłumaczą się wtedy na opak rozumianym “pragmatyzmem” i mówią: “potem to poprawimy” - a przecież wiesz, że to “potem” nigdy nie następuje. Dlatego już teraz zainwestuj czas w celu poszerzenia zakresu znanych Ci narzędzi, aby dobierać właściwe rozwiązania do danej klasy problemu. I koniecznie zastosuj je w praktyce — dopiero wtedy pokazują pełnię swoich możliwości!
Graal - najpotężniejszy artefakt w Heroes III, dający znaczną przewagę nad wrogami.
Prezentowane tutaj metody nie są Świętym Graalem, które zbawią każdy Twój projekt. Jednak najważniejsza jest świadomość alternatywnych sposobów, do tych które zazwyczaj stosujesz i umiejętność wyboru właściwego w danym kontekście. Jeśli nie wiesz, że istnieje wkrętarka, to wszystko będziesz wkręcać śrubokrętem, zanim ktoś nie powie Ci o jej istnieniu.
Pokażę Ci sposoby, dzięki którym będziesz mógł zapanować nad swoim kodem i ten efekt zagubienia nigdy nie nastąpi, albo zostanie ograniczony w granicach modułu. To tak jak z Twoją komodą w sypialni. Najważniejsze wiedzieć, co znajdziesz w jakiej szufladzie, a potem zastosować strategię odpowiednią do sytuacji: spodnie ładnie poskładasz, a skarpetki zapewne rzucisz luzem. Każdy, kto jednak próbował wykonać jeden wielki model, składający się z setek tabel (albo komodę z jedną wielką szufladą), nadający się do “wszystkiego” - prędzej czy później poległ (albo polegli programiści, którzy przyszli utrzymywać ten soft później). Skupmy się więc na podziale na logiczne podproblemy biznesowe, a nie na techniczne warstwy czy mikroserwisy. Dzięki temu każdy z tych problemów będzie można rozwiązać w prostszy (zapewne też tańszy) sposób i ustrzeć developerów od efektu przeładowania kognitywnego (ang. cognitive overload).
Ale wiesz, gdzie szukać.
Zrodlo: https://forum.pytamy.online/t/u-kogo-jest-taka-szuflada/2984
Model nie może istnieć bez żadnych granic, bo wtedy będzie odzwierciedlał świat realny (tak jak szuflady wyznaczają granice w komodzie). Wszystkie modele są niepoprawne, ale niektóre są użyteczne w danym kontekście (jak wspomniane projekty kuchni). Właśnie, aby znaleźć odpowiedni model do sytuacji, musimy spojrzeć na problem do rozwiązania z różnych perspektyw.
Jednostki w Heroes III mają swoje nazwy oraz określony poziom. Każda z nich należy do innej frakcji, ale są też jednostki neutralne. Niektóre jednostki można ulepszać. Każda jednostka ma określony koszt zrekrutowania, a także bazowy przyrost (ile można zrekrutować w każdym tygodniu). Jednostki mają określone statystyki, szczególnie istotne w czasie bitwy.
Kiedy skupiasz się na rzeczownikach i strukturach danych, a przed oczami już rysują Ci się tabelki bazy danych dla jednostki, to jakie pytania doprecyzowujące na podstawie tego opisu możesz zadać, aby zrobić lepszy “model”? I jakie odpowiedzi zapewne otrzymasz?
Skoro nam tak dobrze poszło z określeniem “co ma jednostka”, to moglibyśmy brnąć w to dalej i rozrysować całość bazy danych, ale zobacz co na to, mówi Król Julian.
Jednym plusem takiego podejścia jest to, że nie musiałem marnować na nie zbyt wiele czasu. Robotę za mnie zrobił ChatGPT i pięknie rozrysował tabelki. Takich programistów zapewne szybko zastąpi AI. Jednak Ty przecież chcesz się utrzymać w branży? To czytaj dalej.
Jeden wielki model bazy danych dla całych Heroes III wykonany przez ChatGPT. Tylko po co? Na studiach dostałbym pewnie za to 5, ale w praktyce należy się nie więcej niż 2.
Zaczynać projekt od schematu danych to jak otworzyć grę planszową (swoją drogą, niedawno wyszła wersja Heroes III w tej formie), obejrzeć zawartość: “OK, mam 50 kart, mam też planszę”… i próbować grać, w ogóle nie czytając instrukcji. Nic więc dziwnego, że z podobnym podejściem do programowania (bez zrozumienia domeny i zachodzących w niej procesów) wszędzie zastosujemy rozwiązanie nadające się do CRUDa — większość gier planszowych też ma zazwyczaj karty i planszę…
Czy z rozrysowanych tabelek wiesz już, co musisz zrobić najpierw? Albo gdzie można zrównoleglić pracę developerów? Czy mogą nad tym pracować osobne zespoły? Albo jakich zależności czy projektów interfejsu użytkownika jeszcze brakuje? Aż wreszcie: czy przybliża Cię to do zrozumienia zachodzących procesów biznesowych? Czy wiesz, po co jednostki mają poziomy, albo jaki jest sens w ich ulepszaniu? Najpierw skupmy się na zrozumieniu istoty problemu. Resztą, taką jak szczegóły bazy danych, zajmiemy się później.
W takim razie jak “obiektowo” zamodelować jednostkę? Poniżej widzisz wykonanie za pomocą Kotlina i obiekt sparsowany do JSONa (z przykładowymi wartościami).
data class Creature(
val id: String,
val level: Int,
val faction: Faction,
val growth: Int,
val upgrades: Set<Creature>,
val cost: Resources,
val attack: Int,
val defense: Int,
val damage: Range,
val health: Int,
val speed: Int,
val shots: Int = 0,
val size: Int = 1,
val spells: Set<Spell>,
val abilities: Set<SpecialAbility>,
)
{
"id": "Angel",
"level": 7,
"faction": "castle",
"growth": 1,
"upgrades": [
"Archangels"
],
"cost": {
"gold": 3000,
"crystal": 1,
"wood": 0,
"ore": 0,
"sulfur": 0,
"mercury": 0,
"gems": 0
},
"attack": 20,
"defense": 20,
"damage": {
"low": 30,
"high": 50
},
"health": 200,
"speed": 12,
"shots": 0,
"size": 1,
"spells": [],
"abilities": [
{
"type": "HATE",
"creatures": [
"Devil",
"ArchDevil"
]
},
{
"type": "ConstRaisesMorale",
"amount": 1
}
]
}
Zmienne są dobrze nazwane? Tak. Czy te wszystkie atrybuty należą do jednostki albo są z nią w relacji? Tak. W takim razie: czy coś w tym złego? Zanim odpowiemy sobie na to pytanie, posłuchajmy jeszcze dwóch dialogów programisty z ekspertami domenowymi:
Zadowolony, wykonujesz idealny model spełniający wymagania biznesowe, we współpracy z ekspertami, czujesz, że zęby zjadłeś na DDD, a to prawdziwy Clean Code:
data class Hero(
val id: HeroId,
val player: PlayerId
)
Po pewnym czasie rozmawiasz z kolejnym ekspertem:
Co się stanie teraz z Twoim kodem? Wprowadzasz modyfikację, bo przecież kod musi odzwierciedlać działanie biznesu.
data class Hero(
val id: HeroId,
val player: PlayerId?,
val cost: Resources // koszt najęcia bohatera
)
To w końcu mała zmiana — tylko dodanie możliwości nulla
w polu player
.
Czy to koniec pracy? W żadnym wypadku! Teraz wszystkie testy, jakie tworzyły instancję Hero, świecą się na czerwono i
muszą zostać zmienione (czyli nie chronią Cię przed regresją).
Dodatkowo wszędzie gdzie odwołujesz się do hero.player
wprowadzasz ifa sprawdzającego nulla.
Jak uderzają takie zmiany w Twój projekt?
Jeszcze pół biedy, jeśli chociaż stosujesz Kotlin (czy inny język z Null Safety) i poinformuje Cię o tym kompilator, a nie błąd na produkcji. Nawet jeśli się z tym uporasz, to teraz chcesz zmergować zmiany i… bam (jak to mówi mój 1-roczny syn)! Okazuje się, że inny programista już je nadpisał i mamy konflikt! A Twoje morale i efektywność pracy spadają niczym w armii bohatera w Heroes III, gdy pomieszamy różne frakcje. Czy nie prościej, zamiast modyfikować, byłoby zastosować Open-Closed Principle na poziomie Twojej architektury i mieć dwa osobne modele? Nad którymi bez problemu mogą pracować nawet oddzielne zespołu programistów? Przykład widzisz poniżej.
Właściwie podzielenie modelu zwiększa liczbę klas, ale ostatecznie tworzy więcej modeli, o mniejszej złożoności.
(Kliknij obrazek, aby powiększyć)
Inne atrybuty opisujące Bohatera będa potrzebne w kontekście tawerny, a inne podczas ruchu na mapie.
Dodaj po prostu kolejny namespace/moduł czy jak to się nazywa w Twoim środowisku i zrób 2 osobne klasy.
Nic Cię tutaj nie ogranicza. Chyba że niestety dalej myślisz tabelką Hero
i dodaniem kolumny nullable, albo relacjami między tabelkami.
Tak nas uczyli od początku i ze złymi nawykami walczy się najciężej. To właśnie tutaj czają się potężne smoki, ale jeśli je pokonasz — Ty i Twój projekt zgarniecie większe nagrody niż po pokonaniu Smoczej Utopii.
Smocza Utopia — W Heroes III, jeśli chcesz zdobyć wartościowe skarby, musisz pokonać strzegące ich smoki.
Kiedy słyszysz od ekspertów sprzeczne informacje na temat tego samego rzeczownika, to próbując pogodzić je w jednym modelu, wprowadzasz tzw. accidental complexity. Teraz każdy programista, który nawet zna prawie cały system (ale nie tawernę i najmowanie bohatera) spyta Cię: “stary, a dlaczego tutaj muszę sprawdzać null”? I to jeszcze najmniej groźna forma tego problemu… tylko oboje stracicie trochę czasu.
W efekcie problem istniejący w kodzie, ale nie w samej domenie — staje się cięższy do zrozumienia, niż jest w rzeczywistości. Czy jakiś z ekspertów Cię okłamuje albo jest niekompetentny? Nic z tych rzeczy! Przecież w tawernie możesz bohatera kupić, ale już nie można kupić żadnego z bohaterów poruszających się po mapie, bo należą do Ciebie lub innego gracza. Takie rozbieżności w zeznaniach “biznesu” to wymowny znak, że powinniśmy teraz porozmawiać o zachowaniach. Rzeczownik “bohater” ten sam, ale jego zachowania, reguły i operacje możliwe do wykonania są zupełnie inne, zależnie od kontekstu.
Posłuchaj teraz kilku wypowiedzi graczy Heroes III:
Kiedy modelujesz zachowania, a nie struktury danych, to możesz zadać znacznie więcej pytań, przybliżających Cię do zrozumienia istotnych elementów analizowanego procesu:
Nie ma niezawodnego sposobu, ani działającego zawsze i w 100% procesu na zdobycie wiedzy domenowej. Tutaj potrzebne jest doświadczenie i intuicja. Dlatego też praktykujmy modelowanie, gdzie tylko się da, nawet grając w gry. Bo to jest istota naszej pracy, klepanie kodziku to już jedynie formalność. Jednak możesz sobie pomóc, stosując wzorce, którzy wymyślili już inni i wykorzystując wiele technik strategicznego DDD, takich jak faza Big Picture EventStormingu.
Jak przeprowadzić taki warsztat opisałem dokładnie we wpisie 🍕 Przepis na udany EventStorming krok po kroku!. Teraz spójrz poniżej na zdarzenia, jakie zidentyfikowaliśmy w trakcie takiej sesji w domenie Heroes III. Obecnie skupimy się na wycinku, gdzie eksperci domenowi wspominali nam o jednostkach, czyli w luźnym tłumaczeniu z angielskiego: Creature.
Event Storming pozwala dostrzeć różne konteksty, w jakich działają eksperci domenowi.
(Kliknij obrazek, aby powiększyć)
Zaprezentowane karteczki symbolizują zdarzenia na różnym poziomie abstrakcji i odwołujące się do różnych kontekstów.
Przypomnij sobie jeszcze raz nasz cały “model” jednostki, oparty na rzeczownikach. Czy w każdym przypadku potrzebujemy wszystkich pokazanych atrybutów o jednostce, aby wiedzieć, czy coś może się wydarzyć? Czy możliwe ulepszenie zależy np. od wielkości jednostki w trakcie bitwy? Nie. Czy koszt jednostki zależy od punktów życia (ang. hit points)? Oczywiście, że nie. W takim razie nie powinniśmy mieszać tych rzeczy w jednym modelu. To brzmi jak oczywistość, ale tak zazwyczaj robione było legacy, które ktoś musi utrzymywać (mam nadzieję, że nie Ty)…
Analogiczny przykład dawał Udi Dahan w trakcie szkolenia “Learn Advanced Distributed Systems Design”:
Brzmi absurdalnie? No to nie łączmy listy rezerwacji w jeden obiekt z nazwą pokoju, nawet poprzez ORM i relacje na bazie. Grupuj w obiekty, tylko te dane, które zmieniają się razem i ich spójność natychmiastowa jest konieczna do spełnienia reguł biznesowych — czyli decydują czy jakaś operacja jest możliwa i czy wynikowe zdarzenie ma prawo zaistnieć. Dzięki temu otrzymasz bardziej wyspecjalizowane modele o niższej złożoności. Nie spowodujesz też przez optimistic locking sytuacji gdzie np. zmiana nazwy pokoju, wywłaszcza aktualizację z nową rezerwacją -> a tym samym Twój biznes traci pieniądze.
Zakładając, że model łączy w sobie dane i reguły, to czy we wszystkich kontekstach (jak np. Rekrutacja, Walka, Ulepszanie) potrzebujemy taki sam model jednostki? Już przecież wiesz, że nie. Każdy z nich będzie miał inne reguły i potrzebne do ich sprawdzenia zestawy danych też będą się różnić. Tylko ID będzie takie samo. Czyli na poziomie rozwiązania będziemy mieć różne klasy Creature, na których będzie można wykonać inne operacje. Aby zdecydować, czy dana operacja może być wykonana, nie potrzebujemy znać wszystkich atrybutów opisujących dany rzeczownik. Tak samo, jak ekspert domenowy nie musi wiedzieć wszystkiego o działaniu danego przedsiębiorstwa, ale wystarczy, że wie wszystko o swoim dziale i trochę o tych, z którymi musi bezpośrednio współpracować. Więc nie wyznaczaj klas na podstawie nazw, czy rzeczowników. Szukaj reguł spójności — co się zmienia razem, a co nie.
Dzięki temu uzyskasz też niski coupling i wysoką kohezję (a o tym pisał już nawet Uncle Bob w Clean Code). Pamiętaj oczywiście, że to nie są cele same w sobie. Strukturyzujemy kod, żeby go łatwo zrozumieć. Kiedy rozumiemy, to możemy szybko wprowadzić zmianę i uniknąć bugów. Do tego sprowadza się na koniec dnia praca programisty. Jeśli nie jesteś pewien(-na), co słówko kohezja (ang. cohesion) znaczy, to jest to dobry czas na przypomnienie. Jednak pamiętasz? To dobrze dla Ciebie i twoich współpracowników!
Do zapewnienia niezmienników w danym kontekście, nie potrzebujemy wszystkich możliwych danych, o konkretnym rzeczowniku.
(Kliknij obrazek, aby powiększyć)
Powyżej widzisz możliwy podział modelu jednostki ze względu na konteksty, w jakich występuje. Analogiczny obrazek pokazujący antywzorzec splątania wielu różnych modeli w jeden obiekt (dla produktu w systemie ecommerce) w książce Patterns, Principles, and Practices of Domain-Driven Design zmienił moje programistyczne życie już na zawsze. Jak dotąd właśnie tak bym postępował, nie wydzielając konkretnych modeli — zależnych od kontekstu i możliwych zachowań.
Przykład ze wspomnianej książki na implementację antywzorca "God object" dla produktu w ecommerce.
(Kliknij obrazek, aby powiększyć)
Co więc będzie istotne w konkretnych modelach jednostki dla danych kontekstów?
Jeśli weźmiemy do każdego zastosowania model jednostki z mapy (może tutaj sprawdzi się generyczny “shared”), będzie on zbyt prosty i nie spełni wymagań. Za to w drugą stronę będziemy mieli niepotrzebne skomplikowane i kod będzie dłuższy, nafaszerowany ifami a w efekcie trudniejszy do zrozumienia i wprowadzenia zmiany. Jednak skąd wiedzieć, że akurat te atrybuty powinny być razem? Skąd wziął się ten podział? Jak sobie poradzić z tyloma polami na raz? Odpowiedź brzmi: nie radzić. Lepiej pójść w drugą stronę i pozwolić, aby to potrzebne atrybuty wynikały z pożądanych zachowań systemu.
Patrzenie na system tylko z perspektywy danych i pominięcie zachowań sprawia, że nasze rozwiązanie będzie kulawe. Szczególnie kiedy mamy do czynienia z rozbudowaną logiką biznesową, a nie jedynie przeglądarką do bazy danych. Dominująca perspektywa BEING objawia się plątaniną ifów, z której ciężko się wygrzebać. Więcej na ten temat posłuchasz w tych odcinkach podcastu Better Software Design od Mariusza Gila:
Niestety wciąż, nawet osoby z wieloletnim doświadczeniem, kiedy prosi się je o “zaprojektowanie architektury” rysują prostokąty, łączą je strzałkami i zadowoleni uznają planowanie za zakończone. Jednak dopiero kiedy nałożymy na taki diagram zachowania i proces biznesowy (zdarzenia i komendy — o tym zaraz!), to wychodzi na jaw skrywana w tajemnicy plątanina powiązań i wzajemnych zależności. Mikroserwisy (buzzword alert!) nie uchronią Cię przed tym problemem, a co gorsza — nawet go pogłębią!
Nadrzędna zasada, o której już słyszałem w liceum, a nie zauważyłem, jak jest potężna: dziel i zwyciężaj! Dlaczego to takie ważne? Jak nie wiadomo, o co chodzi, to chodzi o pieniądze. A właśnie po to robimy nasz software - żeby zarabiał.
Twój model albo umożliwia szybkie pivoty, albo nie. Twój model albo oferuje możliwość nieplanowanej produktyzacji modułów, albo nie.
~ DomainDrivers.pl, Sławomir Sobótka i Jakub Pilimon
Brutalne, ale prawdziwe. Nie ma nic pomiędzy. Czy dany moduł mógłby działać samodzielnie i wnosić wartość? Czy możesz zrobić Proof of Concept nowego produktu, nie rozwalając całego systemu naokoło? Standardowo odpowiedź brzmi: nie, gdy brak jest jasno wyznaczonych granic części systemu. Ale gdzie są te dolary?
Takie rozwiązania stosują też najwięksi gracze na rynku. W przypadku Ubera początkowo core’owe usługi były tworzone jako specyficzne funkcjonalności dla kierowców i pasażerów. Kiedy firma zaczęła rozszerzać swój katalog produktów na inne branże niż przewóz osób (takie jak Uber Eats), zostały wydzielone możliwości (ang. capabilities) biznesowe, z których korzysta wiele produktów, takie jak: konto użytkownika, wyznaczanie trasy przejazdu, płatności, matchowanie pasażerów z kierowcami. Mamy więc poziom produktów, które jesteśmy w stanie komponować z komponentów niższego poziomu — możliwości/potencjału, jakie dostarcza nasz system.
Gdyby Uber, nie odseparował wyznaczania drogi i przejazdu od samego przewozu osób, to nie mógłby z łatwością wprowadzić możliwości dostawy paczek przez swoją aplikację. W tym przypadku architektura aplikacji jest właśnie (nawiązując do Heroes) prawdziwą kopalnią złota. Dzięki komponowaniu Capabilities (możliwość, jakie ma nasz biznes, np: wyznaczenia drogi i floty samochodów) mógł powstać nowy produkt na bazie istniejących rozwiązań, ale bez zmieniania innych części systemu, jak np. przewozy osób.
Możesz o tym myśleć tak: jeśli możliwością Twojego przedsiębiorstwa jest przewożenie czegoś samochodami, to możesz wykorzystać ten potencjał dla różnych produktów: działalność ala taxi, dostarczanie jedzenia czy paczek a może też przeprowadzek. Stosuj tę samą zasadę, wyznaczając niezależne komponenty w Twojej architekturze oprogramowania, aby napędzać rozwój produktów, które współtworzysz i otwierać nowe możliwości biznesowe.
Nie zdziwiło mnie więc kiedy Uber zaproponował mi kiedyś, że zamiast przewozu siebie… mogę przewieźć paczkę w ramach jednego miasta. Pomyślcie, co by się stało, gdyby przejazd był ściśle związany z pasażerem? Zapewne to samo co z naszym bohaterem w tawernie: powstałby model “przejazd pasażerski, bez pasażera” z niepotrzebnymi nullami. Więcej o architekturze Ubera możesz przeczytać w artykule Introducing Domain-Oriented Microservice Architecture.
Heroes III Board Game — Battlefield expansion. Dodatkowy produkt, który umożliwia Ci alternatywny sposób prowadzenia walki.
(Kliknij obrazek, aby powiększyć)
Gra planszowa Heroes III umożliwia dwa sposoby prowadzenia bitwy:
Dzięki odpowiedniej modularyzacji i odseparowaniu sposobu toczenia się walki od reszty elementów oraz elastyczność zasad (czyli procesów w grze) umożliwione zostało proponowanie dodatkowych produktów dla nowych i aktualnych klientów. Już Heroes V umożliwiały też toczenie pojedynków między bohaterami nie tylko w trybie scenariusza. Podobne praktyki można zastosować nie tylko w grach. Bardzo wnikliwie porusza ten temat Mariusz Gil na przykładzie sygnalizacji świetlnej, w swojej prezentacji Boiling Frogs 2020 - Mariusz Gil - N-warstwowe modele domenowe.
Dzięki odseparowaniu bitwy od innych modułów, możliwe było wykonanie dodatkowego trybu gry korzystającego z tych samych mechanizmów co scenariusze i kampania.
(Kliknij obrazek, aby powiększyć)
1 Jako programista możesz nie być tylko “wykonawcą”, których zapewne zastąpi AI, ale realnym partnerem biznesowym, otwierającym nowe możliwości. Projektantowi oprogramowania płaci się za to, żeby wprowadzanie potencjalnych zmian i nowych funkcjonalności nie było zbyt kosztowne. Co więcej, jeśli sam przygotujesz system, na potencjalne rozszerzenia to Ty będziesz mógł proponować nowe funkcjonalności, wiedząc dokładnie, co można wykonać i to w jakim czasie. Nie raz sam proponowałem zrobienie czegoś innego i zerwanie tzw. low hanging fruit, którego wykonanie zajęło kilka dni, niż wymagający wielomiesięcznej pracy projekt.
Nie raz w zespołach produktowych padają ustalenia: “zamiast prowadzić dyskusje bez końca, róbmy Proof of Concept”. Potem i tak nikt tego nie realizuje, bo nie umożliwia tego stan naszego systemu. Winą nie jest mityczny “dług technologiczny” (Technical debt isn’t technical - koniecznie zobacz tę prezentację!) rozumiany jako np. używanie zamierzchłej technologii, ale właśnie poplątany model.
Twoja firma kolejnym unicornem? Nie tylko dzięki Twojej pracy, ale oby nie pomimo niej.
Podział i modularyzacja systemu to nie jest fanaberia programistów, ale musi iśc w parze z dogłębnym zrozumieniem domeny biznesowej. Kiedy modelujesz, stawiaj sobie przed oczami nadrzędny cel, jakim są autonomiczne moduły. To, w jaki sposób je zaimplementujesz, jest kwestią drugorzędną. Jak więc dzielić, aby zwyciężać? Sprawdź część drugą tego wpisu, gdzie: wykonamy Event Modeling, model przełożymy na kod i zapewnimy odpowiednią jakość jeszcze przed code review!
Chcę Ci pokazać: mój obecny stan wiedzy, w jaki sposób realizuję projekty i mój tok myślenia, a także warsztat praktyk inżynierskich. W szczegóły stosowanych metod będę wchodził w osobnych wpisach.
Na potrzeby dydaktyczne, choć domena jest dosłownie fantastyczna, to o wiele jej bliżej do rzeczywistych projektów, niż przykładom, jakie zobaczysz na wielu konferencjach, w stylu “kino” czy wałkowany przez wiele książek “ecommerce”. Każde kino i koszyk działa inaczej, więc gdy to my wymyślamy wymagania biznesowe, wtedy kształtujemy nie tylko model, ale też rzeczywistość. Tutaj już jest ona określona poprzez zasady gry, a my musimy ją odkryć i właściwie zamodelować — jak w realnym projekcie. Chociaż oczywiście na wspomnianych standardowych przykładach powstały bardzo dobre książki i prezentacje niezwykle kompetentnych ludzi, od których ja też chłonę. Warto zobaczyć przykłady Oskara Dudycza, gdzie modeluje koszyk zakupowy na wiele różnych sposobów: .NET, NodeJS, Java.
Zapraszam Cię do tej kampanii, w której będziemy razem modelować świat Heroes III w oparciu, o praktyki takie jak DDD i Event Modeling. Czy taka “zabawa” ma naprawdę sens?
Na sam koniec przytoczę cytat, który idealnie tłumaczy cel tego wpisu i zmotywował mnie do jego ukończenia (draft powstał 2 lata temu)! Pochodzi z kursu DomainDrivers.pl, prowadzonego przez Sławka Sobótkę i Jakuba Pilimona.
Często w proces modelowania wkracza właśnie taki niespodziewany błysk intuicji, a ktoś z boku ma wrażenie wyjmowania królika z kapelusza. Ta do końca jeszcze niewyjaśniona zagadka intuicji jest najpewniej nieuświadomioną wiedzą, która pochodzi z doświadczenia. I trzeba jawnie przyznać, że intuicja sprzyja tym, którzy mieli okazję eksponować się na różne przypadki. Rozwiązanie wtedy jest zaskakującym skojarzeniem dwóch tak odległych dziedzin, że logiczny tok myślenia nie zawsze je łączy. Czyli im więcej przypadków widzisz, tym lepiej modelujesz. Ale uwaga, nie musisz zmieniać pracy, żeby te przypadki widzieć, obserwować i eksponować się na nie. Wystarczy po prostu obserwować nadmiar technologii, której na co dzień używamy i modelować to, czego używamy, ale w głowie. Takiego DDD nikt w swojej organizacji nie zabroni ci stosować.
Chcę wraz z Tobą budować tę intuicję, abyś gdy w rzeczywistym projekcie spotkasz analogiczny przypadek, Twój mózg od razu wiedział, jak zareagować.
Jeśli chcesz w tym czynnie uczestniczyć, to zapisz się na moją listę mailingową TUTAJ.
A tymczasem przejdźmy do kolejnej części, gdzie wykonamy Event Modeling i przełożymy go na implementację!
Wciąż za mało życiowych cheatów?
Zostaw swój adres e-mail i zobacz moje spojrzenie na codzienność programisty.
Na sam początek opowiem Ci o zetknięciu z Domain-Driven Design, zmianie myślenia i nowej erze mojego programistycznego ja.
Możesz liczyć na materiały o Event Sourcingu, Event Modelingu, DDD, programowaniu obiektowym i funkcyjnym oraz innych powiązanych tematach.
Na pewno poświęcę trochę maili umiejętnością miękkim.
Będziesz też informowany o nowościach Życia na kodach prosto na Twoją skrzynkę!