🦁 Domain-Driven Design | Bounded Context | Być albo nie być? + zadanie praktyczne!

zycienakodach.pl 1 rok temu

Użyto kadr z filmu "Król Lew" (c) Disney

Pytanie brzmi: kim Ty jesteś? To najważniejsze zdanie, jakie pada w znanej bajce “Król Lew”. Odpowiedź na nie totalnie zmienia bieg wydarzeń. Simba jest w stanie efektywnie działać, dopiero kiedy sobie uświadomi, jakie ma korzenie i jaka jest jego (pojedyncza) odpowiedzialność.

Oryginał po angielsku:

Nowa wersja z 2020 po polsku: Dużo osób krytykuje, ale mi się bardzo podobała :)

Dlatego po poznaniu podstawowych zasad podziału na moduły, będziesz dla swojego projektu, niczym dobry Rafiki (w suahili znaczy przyjaciel) dla zagubionego w świecie Simby. Uchronisz go od sięgnięcia samego dna! Twój kod nie raz potrzebuje pomocy, w zrozumieniu kim adekwatnie jest. Kolejne instrukcje if-else i rozgałęzienia procesów biznesowych pokazują, iż nie jest to takie oczywiste. A niewłaściwy podział na moduły, doprawiony mikroserwisami prowadzi do nieuchronnej katastrofy w projekcie.

Teoria z życia codziennego

Foto z wizyty w Japonii-tutaj akurat patrzę na zamek w Osace :)
(Kliknij obrazek, aby powiększyć)

Jedną z pierwszych rzeczy, jakie zrobiłem, kiedy przyleciałem do Japonii, było zaopatrzenie się w mapę tokijskiego metra. Dzięki temu mogłem dotrzeć do wszystkich atrakcji, które chciałem zobaczyć. Co by się stało, gdybym zamiast tego wziął mapę samochodową? Zakładając oczywiście, iż nie miałem tam samochodu. Zapewne na kilka by mi się zdała.

Wchodząc w bardziej programistyczny żargon, mapa jest abstrakcją na rzeczywistość. Jest to model, przydatny do rozwiązania określonych problemów / zautomatyzowania procesów. Takich jak:

  • znalezienie swojego położenia;
  • wyznaczenia najkrótszej drogi między punktem A i B;
  • dojście do najbliższej stacji.

Sama mapa, jak i każdy model NIE JEST dokładnym odzwierciedleniem rzeczywistości. Miasto nie jest naprawdę czarną kropką, a rzeka nie przypomina niebieskiej linii. Pomijamy to, co w danym kontekście jest nieistotne, a skupiamy się na określonych cechach. To jest właśnie najważniejsza nauka Domain-Driven Design! Tutaj liczy się kontekst. I na tym będzie bazowało Twoje dzisiejsze zadanie. Powyższą analogię zastosujemy do wytwarzanego oprogramowania, które powinno modelować rzeczywistości we właściwym kontekście i rozwiązywać problemy klientów. Jeśli chcemy posługiwać się nazwami wzorców z DDD, to będziemy mówić, iż dany model (i jego język, zwany Ubiquitous Language) jest adekwatny we właściwym Bounded Contexcie. Na razie możesz myśleć o tym, jak o podziale systemu na odpowiednie moduły, a w ramach modułu modelujesz wycinek rzeczywistości, używając klas i funkcji.

Najczęstsze błędy przy dzieleniu na moduły

Dzisiaj zajmiemy się jednym z najprostszych do zauważanie i bardzo często popełnianym. Moje pierwsze programy aż roją się od takich rozwiązań. 😊
Skupimy się na poziomie pojedynczej klasy i jej pól. W miarę postępów będziemy wypływać na głębsze wody. Poprzez modularny monolit, aż do zapisu danych i komunikacji między mikroserwisami.

BŁĄD #1: Moduły (w DDD powiesz Bounded Contexty) schizofreniczne

Popularne powiedzenie mówi, iż jeżeli coś jest do wszystkiego, to jest do niczego. Wyobraź sobie, jak nieczytelna byłaby mapa, która próbowałaby jednocześnie pokazać metro / poziomice / atrakcje turystyczne i wskazówki dla kierowców. Dokładnie tak samo jest z modułami, w których połączyliśmy to, co człowiek powinien rozdzielić.

Zobaczmy prosty przykład z aplikacji, którą robiłem ostatnio z mentorowanym przeze mnie zespołem w ramach kursu CodersCamp (kod akurat w TypeScript).

class User { userId: string; email: string; password: string | undefined; googleId: string; }

Klasa User to prawie zawsze znak, iż w naszym modelowaniu coś poszło nie tak. Zazwyczaj towarzyszą jej takie byty jak UserController, UserService, UserRepository i tak dalej, a wszystko w jednym wielkim module USERS. Niestety jest to oznaką przypadłości, która prowadzi do katastrofy wielu projektów. Sam tworzyłem tak aplikacje na początku mojej kariery-wszędzie serwis wywołujący serwis i jeden wielki kod spaghetti, który nie wiadomo jak testować. Oczywiście najważniejsze, żeby być świadomym, tego, co robisz i tę świadomość będziemy tutaj rozwijać. Nie ma czegoś takiego jak obiektywny wzorzec czy antywzorzec. Wszystko zastosowane we właściwym kontekście ma swoje miejsce.

Kiedy stawiamy pierwsze kroki w programowaniu, nasze projekty są zwykle odbiciem tego, co pokazywał jakiś pan w tutorialu. Jednak trzeba pamiętać, iż szkolenia, które skupiają się na jakiejś technologii (np. Spring Framework, Express, .NET Core), nie nauczą Cię wszystkiego. I nie to jest ich zadaniem. Zobaczysz, co jest możliwe, ale nie jak połączyć tę wiedzę np. z architekturą odpowiednio dobraną do problemu.

W przypadku nieszczęsnego użytkownika zwykle pod prostą nazwą User, kryje się wiele pojęć z różnych kontekstów. Może to być np. Customer (jeśli coś kupuje) / Payer (za coś płaci) / Student (bierze udział w szkoleniu) / Subscriber (zapisany na ten mailing) / Participant (bierze w czymś udział - np. w rozmowie) / Responder (odpowiada na pytania ankiety) etc. Takie odseparowanie pozwala Ci np. nie wymuszać tego, iż płatnikiem będzie koniecznie ktoś posiadający konto w Twoim systemie (jakiś User), a nie np. jakaś firma sponsorująca.

Rozpatrzmy prosty przypadek, który właśnie niedawno implementowałem, więc jestem w temacie. W aplikacji mamy funkcjonalność, która pozwoli na podstawie emaila i hasła wygenerować token JWT. Od razu rodzi się pytanie – czy może istnieć użytkownik bez hasła? Odpowiedź brzmi: NIE. Następnie implementujemy dodatkowy feature, który pozwala nam na uwierzytelnienie także przez Google. Teraz odpowiedź na to samo pytanie brzmi: TAK, jeżeli użytkownik loguje się jedynie przez Google, to hasło jest zbędne. W powyższym modelu zmienna password jest typu string | undefined, tylko dlatego, iż pomieszaliśmy dwa różne konteksty. Idąc tą drogą przy dodawaniu nowych sposób uwierzytelniania, ten model ciągle będzie musiał być modyfikowany. Z pewnością coś takiego przeczy zasadom Open-Closed i Single Responsibility z SOLID. Dodatkowo robimy mniejszy użytek z dobrodziejstwa, jaki dają języki typowane.

The question is: who are YOU?

Teraz wyobraź sobie korzystanie z takiej implementacji i związane z tym rozterki developera (w komentarzach na listingu poniżej). Niestety choćby sam kod nie zna odpowiedzi na powyższe pytanie, zadane Simbie przez Rafiki. To nazywamy właśnie schizofrenią kontekstu. Ponieważ nie został jasno wydzielony kontekst, za każdym razem musimy pytać siebie, w jakim kontekście działamy poprzez używanie instrukcji warunkowych.

if (user.password !== undefined) { // Już wcześniej to sprawdzałem... // Ale czy ja na pewno mam hasło? // Co to oznacza, iż nie ma hasła, skoro musi ono być? // Dobra, dla Google nie mam hasła, ale czy konto z GitHuba będzie miał hasło? } else { // Tutaj powinna być logika dla autoryzacji z Google? // A co z Facebookiem jeżeli go dodam? }

Można wręcz powiedzieć, iż taki kod próbuje okłamywać developera. O oszustwach, jakich się dopuszczamy podczas programowania, więcej przeczytasz na blogu TUTAJ. Testowanie takiego modułu, który łączy ze sobą wszystkie te sposoby uwierzytelnienia, też będzie problematyczne, bo oczywiście idą za tym kolejne zależności do dostarczanych np. przez Google bibliotek. Dlatego coś takiego z pewnością wymaga rozdzielenia. Wprowadzamy wtedy 3 klasy z pojedynczą odpowiedzialnością, w 3 osobnych modułach, zamiast jednej (mogą być choćby o takiej samej nazwie, bo w różnych kontekstach będą oznaczać co innego). I przenosimy je do różnych pakietów / modułów. Nie jest to złamanie zasady DRY — Don’t Repeat Yourself. Dla mnie swego czasu było to wielkim odkryciem 😊
Może to wyglądać następująco:

// Moduł kont użytkowników. // Tutaj będziemy wyznaczać id i pilnować unikalności emaila. // W kolejnych postach i mailach zobaczysz jak zapewniać niepowtarzalność w systemach tradycyjnych i tych opartych o Event Sourcing. class User { userId: string; email: string; } ----------------------------------------- //Moduł uwierzytelnienia przez Google. // Odpowiada za powiązanie konta Google z odpowiednim użytkownikiem. class GoogleUser { userId: string; email: string; googleId: string; } ----------------------------------------- //Moduł uwierzytelnienia przez hasło i generowania JWT po stronie naszej aplikacji. // Tylko tutaj potrzebne nam jest hasło. // Google nie potrzebuje wiedzieć jakie hasło użytkownik ustawił w naszej aplikacji. class User { userId: string; email: string; password: string; }

Czy masz gdzieś klasę, którą przydałoby się tak wydzielić i na jej podstawie utworzyć nowy moduł? Czy już tutaj nasuwają Ci się jakieś pytania? Śmiało dodaj komentarz pod tym postem albo zapisz się na mój mailing i odpisz do mnie w odpowiedzi.

Niedługo na blogu ZycieNaKodach.pl zajmiemy się kolejnym nieprawidłowym podziałem. Jest on szczególnie często popełniany przy wydzielaniu mikroserwisów w systemie i praktycznie niweluje ich zalety do 0. Strzeż się podziału na konteksty anemiczne! Czyli takie, które są skupione wokół jednej encji (Entity-Based) w stylu Zamówienie / Rezerwacja / User itp.

Teraz Twoja kolej!

Jestem pewien, iż znajdziesz gdzieś w Twoich projektach podobną przypadłość jak opisana powyżej. Podziel się Twoimi spostrzeżeniami na ten temat. Zaproponuj jaki podział chcesz wprowadzić i czym się kierujesz, sugerując właśnie takie rozwiązanie. Każda odpowiedź jest dobra, a głupie pytania nie istnieją 😊 Jeśli nie chcesz wystawiać Twojego projektu na światło dzienne, to chociaż zasiej wątpliwość wśród Twojego zespołu.

Trzymam kciuki i wiem, iż Ci się uda 😊


Ten post jest częścią Mailingu Domain-Driven Design. Jeśli chcesz dostać więcej tego typu treści i mieć ze mną ułatwiony kontakt, to koniecznie zapisz się TUTAJ. Czytam i odpowiadam na wszystkie maile. To czasochłonne zajęcie, ale karma wraca z prędkością światła. Wszelkie luźne myśli też mile widziane albo cokolwiek innego, co Twoje paluszki chcą właśnie wystukać na klawiaturze.

Idź do oryginalnego materiału