Wstęp
Na pewno spotkałeś się z pojęciem bogatej domeny (rich domain) oraz jej przeciwieństwa czyli ADM (Anemic Domain Model). Korzystasz z encji, klas serwisowych, mapujesz dane, a także chcesz zaimplementować logikę biznesową. W jaki sposób można do tego podejść, jaka powinna lub jaka może być odpowiedzialność poszczególnych elementów aplikacji.
Domain entity
Pojęcie znane z DDD, w dużym uproszczeniu reprezentuje logiczny fragment domeny/dziedziny/obszaru, który jest identyfikowalny (posiada jakąś tożsamość – np. identyfikator), zawiera dane oraz odpowiada za zachowanie i logikę w ramach tego fragmentu. I tutaj pojawia się właśnie element związany z logiką. o ile jest logika to takiemu obiektowi bliżej jest do rich domain model niż do anemic domain model.
Operacje w systemach
W ramach aplikacji może być realizowanych wiele rodzajów operacji, mogą to być na przykład elementy z dość szerokiej listy (jak poniżej):
- Mapowanie/transformacja danych, np.:
- Encja do DTO.
- Serializacja/deserializacja JSON/XML.
- Przemapowanie z jednego modelu do drugiego (np. translacja danych związana z komunikacją z innym systemem po REST).
- Stworzenie raportu/wydruku (HTML, CSV, Excel, PDF).
- Walidacja
- Walidacja wymaganych parametrów REST API (path param, request param).
- Walidacja struktury danych dla żądania REST(JSON, swagger, OpenAPI).
- Walidacja krzyżowa danych (np. czy PESEL i płeć pasuje).
- Walidacja biznesowa (sprawdzenie, czy stan danych w bazie pozwala na wykonanie operacji, czy identyfikator istnieje, czy saldo konta pozwala na obciążenie w określonej kwocie).
- Operacje CRUD (zapis danych w bazie, cache, wyszukiwanie).
- Przetwarzanie zdarzeń/obsługa żądań.
- Logika biznesowa
- Wykorzystywane wszystkie powyższe elementy.
- Ciąg logicznych kroków bazujący na danych wejściowych i wcześniej zebranych i zapisanych informacji.
- Komunikacja z innymi systemami i/lub zasobami zewnętrznymi.
Na pewno istnieje jeszcze wiele innych rodzajów operacji – weryfikacja uprawnień, szyfrowanie, logowanie, historia zmian/audyt operacji etc.
Jeżeli chodzi o encję i logikę jaką może realizować w ramach rich domain model to kandydatami do tego są wytłuszczone elementy powyżej – o ile logika ta opierać się będzie na danych encji i bliskich powiązań tej encji – np. operacja obciążenia karty kwotą amount – encja Card powiązana relacją z BankAccount (rachunkiem) i sprawdzenie, czy saldo rachunku umożliwia obciążenie (canBeDebited(amount)).
Psst… Interesujący artykuł?
Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.
Dziękujemy!
Wysłaliśmy Ci mail powitalny, w którym znajdziesz link do aktywacji newslettera. Do usłyszenia!
Błędy i pułapki
Realizując jakąś funkcjonalność, należy zwrócić szczególną uwagę na kilka elementów, które mogą ułatwić (albo utrudnić) określenie tego, co i w jaki sposób encja powinna realizować. Elementy te to w szczególności:
- Nieprawidłowa definicja dziedziny/obszaru – np. ogólna nazwa karty, która zawiera dane karty (dane właściciela, numer karty, saldo karty), limity karty, historię karty. Już taki podział jest nieprawidłowy, to co najmniej cztery domeny – użytkownicy, dane karty (wskazanie na id właściciela), limity karty i historia transakcji karty (obydwie wskazują na identyfikator karty). Reprezentacją mogłyby być przykładowe serwisy/fasady/dziedziny
- UserInventory – dodawanie użytkownika, edycja danych użytkownika, aktywacja/deaktywacja
- CardInventory – dodawanie karty, przypisywanie karty do użytkownika, ustawianie limitów, blokowanie/odblokowanie karty
- CardTransactionExecutor – wykonywanie operacji na karcie
- CardTransactionsHistory – dostęp do historii operacji kartowych
- UserCardLimitVerifier – weryfikacja operacji pod kątem zgodności z ustawieniami limitów dla danej karty użytkownika
- Zbyt późna walidacja danych – niepotrzebnie wykonywana jest dalsza logika (patrz Fail Fast opisany tutaj)
- Exception Driven Development – sterowanie logiką aplikacji poprzez rzucanie wyjątków jest antywzorcem. Jak sama nazwa wskazuje, exception to jakiś wyjątek.
- Jeśli rzucasz wyjątek typu catchable to musisz obsługiwać te wyjątki, przykładowo metoda nie może być użyta w przetwarzaniu strumieniowym bez łapania wyjątku
- Jeśli rzucasz wyjątek dziedziczący z RuntimeException, tracisz z oczu fragment logiki. Lepiej jest zwracać status wykonania operacji i ew kod błędu lub prawidłowy wynik.
- Zbieranie informacji o stosie wywołania, w którym wystąpił wyjątek, jest bardzo kosztowne. Podobnie wydruk takiego stosu, możesz dodać do tego przechwytywanie i ponowne opakowywanie wyjątku źródłowego.
- Kod trudny w testowaniu – zbyt wiele zależności i pól w klasie, ciężko wstrzyknąć mocki, nie można napisać testów jednostkowych bez korzystania z bazy danych czy jakiegoś frameworka, skomplikowane mockowanie, zbyt duże zagnieżdżenia (nie stosowanie się do fail fast).
Apache Kafka – wydajność vs. gwarancja dostarczenia wiadomości
Jak stworzyć piekielnie szybką albo maksymalnie bezpieczną wersję producenta oraz konsumenta.
W końcu trzeba zacząć programować – encja
Przychodzi w końcu ten moment, w którym zaczynasz pisać fragment kodu. Chcesz lub musisz oprogramować encję, zastanawiasz się jaką logikę może ona realizować i w jaki sposób można ją zapisać. Chcesz skorzystać z rich domain model zamiast encji typu POJO (czytaj więcej o tutaj) – musisz więc sobie zdać sprawę z tego, iż klasy będą większe niż w przypadku POJO. Natomiast logika będzie realizowana w ramach logicznego obszaru, co jest olbrzymią zaletą.
Pierwszą zasadą jest unikanie setterów/getterów dla pól, jeżeli to możliwe (a najlepiej, gdyby ich w ogóle nie było) – np. metody activate/deactivate/isActive zamiast setEnabled/isEnabled, czy credit/debit dla zmiany salda konta o określoną wartość.
Korzystaj z enkapsulacji – ograniczaj dostęp do danych do niezbędnego minimum. Nie wszystkie kolumny z tabeli zmapowane na pola klasy encji muszą być widoczne na zewnątrz. Logika encji może zmieniać stan takich pól a na zewnątrz udostępniać tylko fragment – np. konto użytkownika ma datę ważności, stan aktywacji oraz informację o zablokowaniu – może istnieć metoda isOperable, która sprawdza te trzy warunki i zwraca true, o ile użytkownik jest w pełni funkcjonalny.
Inny przykład to sieć obiektów i transformacja tej sieci do innego modelu (np. do DTO) – o ile jest jakiś nadrzędny element (encja), to tylko on powinien mieć metodę toDTO, ponieważ inne podrzędne same nie mogą istnieć (np. osoba i dokument tożsamości).
Ale mapowanie można również wykonać w inny sposób – możesz stworzyć mapper, który zajmie się transformacją danych, lub też sam obiekt będzie miał logikę tworzenia samego siebie na podstawie danych innego obiektu.
Psst… Interesujący artykuł?
Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.
Dziękujemy!
Wysłaliśmy Ci mail powitalny, w którym znajdziesz link do aktywacji newslettera. Do usłyszenia!
Wadą tego rozwiązania jest to, iż konieczne będzie udostępnienie getterów dla mapowanych pól. Zaletą mappera jest to, iż w przypadku rozbudowanych encji nie będzie w niej długiego fragmentu kodu odpowiedzialnego za mapowanie danych (może okazać się np. iż 70%-80% ciała klasy to mapowanie danych). Kolejną zaletą mapperów może być to, iż jeżeli potrzebujemy dociągnąć jakieś dane z zewnętrznego systemu, to nie zrobimy tego w encji.
Kolejny element walidacja – tutaj w zależności od zakresu walidacji, może być ona realizowana w ramach encji dla prostszych przypadków, lub w przypadku gdy walidacja sięga dalej poza encję, lepszym rozwiązaniem może być walidowanie poza encją w osobnym walidatorze. Zachęcam również do tego, aby walidacja nie rzucała wyjątku (unikaj exception driven developement).
Przetwarzanie zdarzeń czy rozkazów wygląda podobnie – możesz stworzyć klasy handlerów, które obsłużą całość logiki, a możesz też w ramach encji realizować fragment logiki.
Dodatkowo w przypadku gdy zdarzenia pochodzą z systemów zewnętrznych, dobrze jest odseparować się od modelu dostawcy i stworzyć osobne obiekty reprezentujące te dane (mapowanie modelu zewnętrznego na wewnętrzny). o ile natomiast zdarzenie czy rozkaz wykracza znaczeniem poza zakres encji, to funkcjonalność obsługi powinna być delegowana do osobnej klasy serwisowej, która ewentualnie wywoła odpowiednie metody na encjach.
Przydatne linki
Podoba Ci się ten artykuł? Weź więcej.
Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.
Dziękujemy!
Wysłaliśmy Ci mail powitalny, w którym znajdziesz link do aktywacji newslettera. Do usłyszenia!
Gdybyś potrzebował jeszcze więcej:
Jesteś Java Developerem?
Przejdź na wyższy poziom wiedzy
„Droga do Seniora” 🔥💪
Jesteś Team Leaderem? Masz zespół?
Podnieś efektywność i wiedzę swojego zespołu 👌
Gdybyś potrzebował jeszcze więcej:
Jesteś Java Developerem?
Przejdź na wyższy poziom wiedzy
„Droga do Seniora” 🔥💪
Jesteś Team Leaderem? Masz zespół?
Podnieś efektywność i wiedzę swojego zespołu 👌