Postaramy się wykorzystać DDD, architektury/wzorce do zaprojektowania struktury projektu
oraz pisać kod wspomagając się TDD/BDD.
Seria z tworzenia aplikacji do zarządzania projektami
- #0 Prawie trywialna aplikacja do zarządzania projektami (ten wpis).
Dodatki, aby lepiej zrozumieć co tutaj robimy
- Wstęp do Domain-Driven Design – strategiczne narzędzia (ENG)
- Początki w testowaniu kodu. Testy jednostkowe, czy może integracyjne?
Ostatnimi czasy bardzo zainteresowały mnie tematy DDD, TDD/BDD oraz architektur. Jest to takie uzupełnienie układanki jaką próbowałem sobie ułożyć w głowie. Dają one kilka fajnych rzeczy dzięki, którym jest łatwiej zrozumieć czyiś kod oraz samemu pisać lepszy. Oczywiście nie jestem jakimś guru w tym temacie. Prawdopodobnie popełnię kilka gaf podczas tworzenia projektu, ale właśnie o to chodzi w tej serii. Tak, żeby stworzyć/odtworzyć cały ten proces. Aż do efektu końcowego. W tym wpisie znajdziesz co chcemy zrobić, a w kolejnych wpisach omówię krok po kroku implementację.
Parę słów na wstęp
Najczęściej powtarzaną radą na początku jaką słyszałem było, aby pisać jak najwięcej. Faktycznie jest to najlepsza metoda na naukę programowania. W ten sposób nauczysz się najwięcej. To jest, aż tak łatwe. Przez podstawy jest dość łatwo przejść, ale co potem? Co w momencie kiedy trzeba coś zrobić dobrze, a nie tylko, iż działa? Zamiast tworzyć potwora spaghetti wystarczy usiąść chwilkę i przemyśleć kilka koncepcji. Z pomocą przychodzi tu cała masa wzorców, idei, paradygmatów, metodyk, praktyk. Jest tego po prostu cała masa. Łatwo jest się w tym pogubić. Dlatego postanowiłem stworzyć ten projekt, w którym będę inkrementalnie dodawać/refaktorować nowe rzeczy. Poruszymy takie tematy jak DDD (Domain-Driven-Design), TDD/BDD (Test/Behavior Driven Development) oraz czasami wrzucimy tu i ówdzie jakiś wzorzec – jeżeli będzie akurat pasował. Takie tematy to lata doświadczenia. Postaramy się trochę zhakować ten czas wzorując się na istniejących ideach/przykładach oraz wiedzy ludzi, którzy przeszli już tą ścieżkę. W ten sposób uda nam się szybciej dojść do celu poznając po drodze wszystkie fajne koncepty. Myślę, iż jest to spory krok, aby zostać lepszym programistą. W miarę poszerzania wiedzy z tych zakresów mam nadzieję, iż projekt będzie ewoluować co w końcowym efekcie może dać całkiem fajny efekt. Obecny plan na aplikację znajdziesz poniżej – mam nadzieję, iż w miarę upływu czasu zacznie on jeszcze bardziej ewoluować. Wszelkie konstruktywne uwagi mile widziane. Bez zbędnego przedłużania bierzemy się do roboty!
Co zrobimy?
Znasz Trello? Zrobimy coś podobnego. Opiszę krok po kroku tworzenie tej aplikacji. jeżeli już potrafisz trochę programować to pewnie masz za sobą pierwszą to-do-listę. Ta będzie trochę bardziej zaawansowana, a przynajmniej mam taką nadzieję. Oprócz podstawowych funkcjonalności dodamy również kilka mniej potrzebnych jak chociażby pokazywanie pogody.
? Zakładam, że…
- ?? używasz IntelliJ Idea Ultimate.
- znasz podstawy jakiegoś języka obiektowego.
- ? znasz jakieś podstawowe paradygmaty/zasady/OOP – tak intuicyjnie przynajmniej.
- ? nie tylko chcesz, żeby działało, ale też, żeby było fajnie napisane – choć to w sumie to subiektywna rzecz.
W czym robimy?
- /src – kod źródłowy będzie w Kotlinie.
- /test, /integration-test – testy w Groovym (Spock).
- A wszystko to budowane dzięki Gradle.
- W dużo późniejszym etapie będzie również potrzebny Docker (ale do tego jeszcze daleka droga).
Niektóre rzeczy jakie tutaj poznamy nie są zorientowane językowo.
Co oznacza, iż łatwo jest wynieść te idee do innego języka, frameworka.
- ? TDD/BDD – w mojej ocenie w TDD chodzi o pisanie testów przed logiką domenową. Zaś w BDD chodzi, żeby te testy jak najlepiej oddawały problem biznesowy. Ładnie opisane testy w takiej formie, iż choćby osoba nie-techniczna mogłaby z łatwością dowiedzieć się co robi przypadek testowy. Pozwalając tym samym na zrozumienie działania aplikacji. Świat idealny. Co nie?
- ? DDD – czyli takie modelowanie aplikacji na podstawie domeny w jakiej pracujemy (np. e-commerce). Skupiamy się tutaj na domenie, czyli rdzeniu naszej aplikacji. Można powiedzieć, iż mapujemy wymagania od biznesu na kod. Jest to część strategiczna DDD. Druga część to taktyczna, czyli to w jaki sposób implementujemy różne rzeczy. Bierze odpowiedzialność za rzeczy z bounded-contextu. Wszystkie te rzeczy takie jak services, factories, repositories, entities – wszystkie te nazwy wywodzą się właśnie z DDD.
1⃣ Pierwsza wersja będzie…
W architekturze warstwowej (aka. layered architecture) – prawdopodobnie najbardziej popularna. Często domyślny wybór podczas pisania aplikacji. Po jej poznaniu łatwiej jest się uczyć kolejnych.
2⃣ Druga wersja będzie…
W heksagonalnej z prostym CQRSem, którą to osobiście bardziej lubię. Pozwala według mnie w łatwiejszy sposób zamknąć odpowiedzialność wewnątrz modułu (bounded contexu) oraz wystawić na świat tylko to co nas interesuje. Wyobraź sobie miasto z dużą ilością budynków. W całym tym tłoku jest tylko jeden wielki wieżowiec, który jest widoczny choćby z kosmosu. Chcielibyśmy właśnie, żeby ten bounded contex miał takie skyscraperFacade co wystawia wszystkie metody potrzebne do modyfikacji obiektów. Nie można po prostu wjechać do miasta. Trzeba polecieć balonem, albo helikopterem do wieżowca i tam dopiero przez odpowiednie tunele przejść do innych części miasta. Do tego łatwiej jest powiedzieć co jest tutaj unitem w testach jednostkowych.
3⃣ Trzecia wersja będzie…
Dodana do mikroserwisowego ZOO. Stworzymy to wszystko od podstaw. Na szczęście spring ma te wszystkie funkcjonalności w podstawowej formie zaimplementowane. Dzięki czemu wiele z tych rzeczy ogranicza się do stworzenia adnotacji. Myślę, iż jest to w miarę prosta architektura na początek. Te mikroserwisy nazywają się backing-services. Takie wspomagacze do działania całego zoo.
? Nasze zoo można ponazywać w następujący sposób:
user-autorization-service – furtka do zasobów dostępnych tylko dla zalogowanych użytkowników. Zasoby dzielą się na protected oraz unprotected. Lista produktów jest przykładem zasobu jaki możemy pobrać bez logowania się do strony. Dostępne zwykle w formie read-only. Z drugiej strony są zasoby, do których mamy dostęp tylko po zalogowaniu. Chociażby informacje o użytkowniku/ach.
? Tutaj guide prosto ze strony springa spring-boot-oauth2.
edge-service – łączy requesty z frontu do backendu poprzez strasznie brzmiącą rzecz – reverse-proxy. w uproszczeniu unifikuje zasoby z różnych backendów do jednego wspólnego REST API. zwykle na całą architekturę mikroserwisów składa się wiele, wiele backendów. Każdy z nich może wystawiać różne rzeczy. Czasami publiczne, czasami nie. Chodzi tutaj o to, aby powiedzieć co dokładnie chcemy, aby było publiczne.
? Więcej w dokumentacji – spring-cloud-netflix.
discovery-service – w całym mikroserwisowym zoo jest bardzo duża ilość aplikacji. U nas będzie jedna, dwie niemniej w prawdziwym świecie jest tego kilkadziesiąt, kilkaset, a choćby więcej. Trzeba jakoś trzymać pieczę nad tym wszystkim. Z pomocą przychodzi bardzo prosty koncept.
? Guide prosto ze strony springa service-registration-and-discovery.
centralized-configuration-server – globalne konfiguracje dla docelowego środowiska. Jest tutaj config-server oraz jakiś client. Załóżmy, iż client jest to prosta Springowa aplikacja typu HelloWorld. W momencie kiedy jej serwer HTTP startuje zasysa ona domyślną konfigurację z config-servera. Oczywiście tylko i wyłącznie jeżeli ten serwer jest uruchomiony. Można też dynamicznie wstrzykiwać/zmieniać configi podczas runtime.
? Więcej znajdziesz w dokumentacji spring-cloud-config-server oraz guide ze strony springa.
Do tego kilka innych rzeczy, które omówimy bardziej szczegółowo w innych wpisach.
load-balancer– rozdziela ruch na instancje naszej aplikacji. Dodatkowo może pingować do tych instancji, czy aby na pewno przez cały czas żyją (health check).
Memcached,Redis, BigCache – słowem coś do cache’owania (najlepiej in-memory, bo RAM jest szybszy niż najlepszy SSD). Chodzi generalnie o to, aby requesty dostawały zasoby jeszcze szybciej. Dlatego pomijamy bazę danych i serwujemy niektóre dane prosto z RAMu – bardzo upraszczając, bo nie wszystkie rozwiązania działają w ten sposób.
#⃣ Czwarta piąta, szósta, siódma wersja…
Jakie jeszcze funkcjonalności można wymyślić?
- jakaś integracja z zewnętrznym API – pogoda (np. Darksky).
- billing, subskrypcje (integracja z Payu i pobawienie się na ich sandboxie – do sprawdzenia, czy się da).
- opis jakiś prostych monitoringów, kibana, grafana etc.
- profil użytkownika, dziennik aktywności, wysyłka maili.
- elasticsearch – do przeszukiwania tasków.
- logowanie z wykorzystaniem OAuth2.
- jakiś prosty event-sourcing
- oraz jakiś prosty CQRS (niekoniecznie powiązane z tym powyższym)
? Początkowe założenia na bounded contexty.
Czym jest bounded-context? Znajdziesz to w tym wpisie ».
Team
- POST: /teams – stwórz team.
- POST: /teams/:teamName/members – dodaj nowego człowieka do teamu.
- GET: /teams – na początek pobierz wszystkie teamy (można też dodać paginację).
- PUT: /teams/:id – zmień nazwę teamu.
- DELETE: /teams/:id – usuń team.
Projects
- POST: /projects/drafts – stwórz wersję roboczą projektu – wymagana tylko nazwa projektu.
- POST: /projects – stwórz projekt – wymagana nazwa projektu wraz z listą ficzerów?.
- GET: /projects – pobierz wszystkie wersje robocze.
- POST: /projects/:id – pobierz projekt.
- PUT: /projects – zmień/zaktualizuj projekt.
- PATCH: /projects/:id/started – wystartuj z projektem jeżeli jest przypisany do teamu.
- PATCH: /projects/:id/ended – zakończ projekt jeżeli wszystkie ficzery zostały zrobione.
W obecnej znajdziesz takie branche step-1-teams oraz step-2-projects.
Projekt (github) będzie podzielony na branche według kontekstów.
Master będzie miał wszystko, a zmiany dodawane będą poprzez pull-requesty.
? Kilka wskazówek do tworzenia dobrego REST API
? Do opisu zasobów używamy rzeczowników, a nie czasowników.
? Ponadto NIE używamy akcji do opisu zasobu createNew, getAll.
Używamy liczby mnogiej team, teams.
- ? NIE GET: /getAllTeams, POST: /createNewTeam
- TAK GET: /teams, POST: /teams
? Nie mieszamy metod HTTP.
? Nie używamy GET do zmiany stanu.
? Po to istnieją inne metody POST, PUT, DELETE, PATCH.
- ? NIE GET: /projects/:id/ended
- TAK PATCH: /projects/:id/ended
? Używamy różnych kodów statusu.
? Kilka takich co użyjemy w naszej aplikacji.
- 200: OK – wszystko działa!
- 201: CREATED – pomyślnie utworzyłeś nowy zasób.
- 204: NO_CONTENT – pomyślnie zmieniłeś zasób.
- 304 NOT_MODIFIED – mamy to w pamięci podręcznej i nie ma potrzeby wrzucania tego ponownie.
- 404: NOT_FOUND – nic nie znalazłem. Nie ma zasobu za tym URI.
- 422: UNPROCESSABLE_ENTITY – może brakuje jakiś wymaganych pól w encji? Albo encja już istnieje?
- ? 500: INTERNAL_SERVER_ERROR – request może być dobry, ale serwer nie wie co zrobić.
? Generalnie kody mają takie funkcje.
- 1xx (informacyjnie) – może przekroczony czas na połączenie? Albo serwer odrzucił połączenie?
- 2xx (sukces) – wszystko działa zgodnie z zamiarami. Czasami to się zdarza.
- 3xx (przekierowanie)– może zasób jest dostępny chwilowo/trwale pod innym adresem?
- 4xx (klient) – przesłałeś nieprawidłowego JSONa? A może zasób jest niedostępny dla niezalogowanych?
- ? 5xx (serwer) – wszyscy zginiemy.???
? Czego unikać?
Zagnieżdżone zasoby – generalnie powinno się tego unikać. Nie jest to co prawda zła praktyka, ale znacznie utrudnia potem pracę z API. Co może podchodzić pod złą praktykę. Zależy od sytuacji, także sam oceń. Na pewno są przypadki gdzie takie coś może mieć miejsce. Generalnie nie powinno się tworzyć większej relacji niż 2-poziomu.
- POST: /teams/:id/members/:sub-id
?Czego nie zrobimy, a mogłoby się pojawić?
Wersjonowanie API – jest to po prostu furtka dająca możliwość dalszego rozwoju naszego API.
- POST: /api/v1/teams
- GET: /api/v1/teams
1⃣ Warstwy pierwszej wersji:
? API
To co wchodzi do naszej aplikacji. Dane wejściowe. Brak tu jakiejkolwiek logiki – czysty HTTP.
? APPLICATION
Taki mediator pomiędzy domeną, a infrastrukturą. Broni modele z domeny przed zewnętrznymi rzeczami. Tylko ta warstwa oddziaływuje bezpośrednio na Domenę. Przykładowy ApplicationService nie powinien mieć żadnej logiki biznesowej. Wie tylko jaki model użyć, ale nie wie jak model działa. Wie jaki agregat wywołać oraz potencjalnie wie troszczkę o domenowych serwisach – tylko tyle.
? DOMAIN
Praktycznie najważniejsza część naszej aplikacji. Tutaj znajduje się rozwiązanie real-life problemu, który dał nam biznes. Zaimplementowane różne zachowania modelu oraz walidacje. Po prostu serce, rdzeń naszej aplikacji. Mamy tutaj nasze agregaty oraz serwisy domenowe.
? PERSISTANCE
Ostatni przystanek. Mapujemy modele z domeny do tej warstwy. Tutaj jest powiedziane dokładnie jak nasze dane będą przechowywane. Abstrakcyjnym przykładem mogłoby być, iż lista ficzerów z domeny (słowo klucz: lista) byłyby przechowywane w formie zwykłego stringa, który oddziela je przecinkiem. Takie zachowanie raczej nie ma to większego sensu niemniej to tylko przykład.
? INFRASTRUCTURE
Tutaj można wrzucić klienta do zewnętrznego API (np. pogody).
#⃣ Najważniejsze na koniec. Projekt znajdziesz tutaj Githubie »
#⃣ Oraz nowszy projekt, który jest lepszą wersją powyższego
Dodatkowo: Co warto czytać? (nie tylko programistyczne)
? Moja lista książek (systematycznie będę dodawać tam więcej pozycji) »
Inne materiały do przejrzenia
- spring-testing (github project)
- ddd-leaven-v2 (github project)
- Building microservices with Go (blog series)
- Microservices (blog post – Martin Fowler)
- Event-Sourcing in Microservices