Już dostępny

Program Szkoleniowy Java Developer dostępny 🔥💪 tylko TERAZ za 999 zł  Sprawdź szczegóły i agendę

Zakres

Monitoring • Apache Kafka • Clean Code Testowanie • Hibernate • Systemy kolejkowe Sprawdź szczegóły i agendę

Zakres

14 modułów  /  ponad 40h nagrań  /  230 lekcji  /  dożywotni dostęp  /  Sprawdź szczegóły i agendę

Jak zamapować agregat DDD w Hibernate

utworzone przez Hibernate, Java

[Szybkie info]: Startujemy z II edycją Programu Szkoleniowego Java Developera 🚀. To MEGA piguła wiedzy o Java 🔥💪

  • 14 tygodniowy program szkoleniowy online,
  • 230 lekcji w formie video (40 godzin materiału)
  • z dożywotnim dostępem
  • Case Studies, masz dostęp do kodu i obrazów Dockerowych
  • zamknięta grupa mentorzy + uczestnicy i webinary na żywo

W agendzie znajdziesz: Mikroserwisy, Systemy kolejnowe, Apache Kafka, Caching, Hibernate/MyBatis/Spring Data, techniki efektywnych Testów kodu, Clean Code i Maven.

Tylko teraz dołączysz z 63% rabatem to 2699 zł 999 zł (+VAT). I nigdy już nie będzie taniej. Poniżej dowiesz się więcej:

Zobacz więcej

A teraz przechodzimy do artykułu:

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ą. Jeżeli 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)).

@Entity
@Data
class Card {

    @OneToOne....
    BankAccount bankAccount;

    @OneToMany....
    List<CardLimit> cardLimits;
    
    DebitStatus debit(DebitValueObject debit) {
        var validationStatus = validate(debit);
        if(!validationStatus.isOK()){
            return DebitStatus.forError(validationStatus);
        }
        bankAccount.debit(debit);
        cardLimits.forEach(limit -> limit.debit(debit.getAmount()));
        return DebitStatus.ok();
    }

    private ValidationStatus validate(BigDecimal debitAmount) {
        var accoubtCanBeDebited = bankAccount.canBeDebited(debitAmount);
        if(!accountCanBeDebited) {
            return ValidationStatus.insufficientBalance();
        }
        var limitValidationErrors = cardLimits.stream()
                .map(limit -> limit.validateDebit(debitAmount))
                .filter(status -> status != OK)
                .collect(Collectors.toList());
        if(!limitValidationErrors.isEmpty()) {
            return ValidationStatus.limitError(limitValidationErrors);
        }
        return ValidationStatus.ok();
    }
}

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.

.

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, że 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śli 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, jeżeli użytkownik jest w pełni funkcjonalny.

@Entity
class UserAccount {

    private Boolean activated;
    private Instant expiresAt;
    private Boolean blocked;

    Boolean isOperable() {
        return activated && Instant.now().isBefore(expiresAt) && !blocked;
    }
}

Inny przykład to sieć obiektów i transformacja tej sieci do innego modelu (np. do DTO) – jeżeli 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).

@Entity
class Person {
    
    private String firstName;
    
    @OneToOne...
    private IdentityDocument document;
    
    PersonDto toDto() {
        return PersonDto.builder()
                .firstName(firstName)
                .document(toDocumentDto(document))
                .build();
    }
    
    private static IdentityDocumentDto toDocumentDto(IdentityDocument document) {
        return IdentityDocumentDto.builder()
                .id(document.getId())
                .build();
    }
}

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.

@Entity
class Person {

    private String firstName;

    @OneToOne...
    private IdentityDocument document;    
}

class PersonMapper {

    static PersonDto toDto(final Person person) {
        return PersonDto.builder()
                .firstName(person.getFirstName())
                .document( DocumentMapper.toDocumentDto(person.getDocument()))
                .build();
    }    
}

class DocumentMapper {
    
    static IdentityDocumentDto toDocumentDto(final IdentityDocument document) {
        return IdentityDocumentDto.builder()
                .id(document.getId())
                .build();
    }    
}

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.

.

Wadą tego rozwiązania jest to, że konieczne będzie udostępnienie getterów dla mapowanych pól. Zaletą mappera jest to, że w przypadku rozbudowanych encji nie będzie w niej długiego fragmentu kodu odpowiedzialnego za mapowanie danych (może okazać się np. że 70%-80% ciała klasy to mapowanie danych). Kolejną zaletą mapperów może być to, że jeśli 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.

@Slf4j
class DebitCommandHandler {
    
    private CardRepository repository;
        
    void handleDebitAccountCommand(final DebitAccountCommand command) {
        repository.getByCardUuid(command.getCardUuid())
            .ifPresentOrElse(card -> handleCardDebit(card,command), 
                    () -> log.error("Card with Uuid={} not found", command.getCardUuid()));        
    }
    
    private void handleCardDebit(final Card card, final DebitAccountCommand command) {
        var debitStatus = card.debit(command);
        if(debitStatus.isError()) {
            log.error("Error od debit error={}", debitStatus.toErrorString());
        } else {
            log.info("Card with uuid={} debited successfully with amount={}", command.getCardUuid(), command.getAmount());
        }        
    }
}

@Entity
@Data
class Card {

    @OneToOne....
    BankAccount bankAccount;

    @OneToMany....
    List<CardLimit> cardLimits;

    DebitStatus debit(final DebitAccountCommand debit) {
        var validationStatus = validate(debit);
        if(!validationStatus.isOK()){
            return DebitStatus.forError(validationStatus);
        }
        bankAccount.debit(debit);
        cardLimits.forEach(limit -> limit.debit(debit.getAmount()));
        return DebitStatus.ok();
    }    
}

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). Jeżeli 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.

@Slf4j
class DebitCommandHandler {

    private CardRepository repository;
    private ExternalExchangeRegistry registry;

    void handleDebitAccountCommand(final org.foreign.ForeignExchangeEvent event) {

        log.info("Foreign event received type={}, id={}", event.getType(), event.getId());
    
        final ExternalExchangeVO externalExchangeVO = ExternalExchangeVO.of(event);
        var exchangeId = registry.register(externalExchangeVO);
    
        final DebitAccountCommand command = DebitAccountCommandMappef.of(event);        
        repository.getByCardUuid(command.getCardUuid())
            .ifPresentOrElse(card -> handleCardDebit(card,command),
                    () -> handleProcessingError(exchangeId, command));
    }
    
    private void handleProcessingError(final ExternalExchangeId externalExchangeId, DebitAccountCommand command) {
        log.error("Card with Uuid={} not found", command.getCardUuid());
        registry.registerProcessingFailure(externalExchangeId);
    } 
}

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.

.

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 👌

Dyskusja