Java Developer? Przejdź na wyższy poziom wiedzy 🔥💪  Sprawdź

Team Leader? Podnieś efektywność swojego zespołu 👌 Sprawdź

Rodzaje obiektów w Java (encja, POJO, ValueObject, DTO)

utworzone przez 25 października 2021Hibernate, Java

Wstęp

Korzystając w aplikacji z Hibernate czy też Java Persistence API, aby móc użyć danych korzysta się z różnych obiektów. Pierwszym rodzajem obiektów jest encja. Ale aplikacja to nie tylko dane, ale także logika biznesowa wykorzystująca dane wejściowe i wyjściowe, obiekty domenowe. Definicją struktury danych poza tabelami może być REST API, event (Kafka, RabbitMQ), biblioteka, moduł wewnętrzny, główna domena. Sama klasa opakowująca dane może mieć jakąś logikę. W jaki sposób skorzystać i kiedy z poszczególnych rodzajów obiektów zawierających dane? Jakie jest zadanie poszczególnych rodzajów obiektów. Postaram się to przybliżyć w niniejszym artykule oraz wskazać, jakie są możliwości.

Immutable

Obiekt immutable reprezentuje obiekt, który jest niemodyfikowalny. Oznacza to, że aby uzyskać obiekt o zmienionych danych, należy stworzyć nowy, ze zmienionymi danymi. W Java istnieje wiele klas immutable String, BigDecimal, Long etc.

class  PersonId {
     
    private final String value;
      
    PersonId (String value) {
        this.value  = value;
    } 
      
    String getValue() {
        return this.value;      
    }
}

Obiekt może reprezentować zarówno pojedynczą wartość, jak i zbiór atrybutów.

Może zawierać złożoną logikę w środku.

Czy encja może być immutable? – nie może. Encja jest modyfikowalna (ustawianie wartości pól reprezentujących kolumny tabeli czy widoku). Mimo że istnieje anotacja @Immutable (Hibernate) to sama edycja encji jest możliwa – zmiany zostaną zignorowane lub zostanie rzucony wyjątek dla modyfikacji. Encja @Immutable może również zostać utworzona czy usunięta.

Plain Old Java Object

POJO jest rodzajem obiektu, który przechowuje atrybuty i pozwala na korzystanie z nich. Wg definicji nie powinien mieć odniesienia do żadnego frameworka – SIC!

public class Card {
    String cardId;

    String cardUuid;

    public Card() {
    }

    public String getCardId() {
        return this.cardId;
    }

    public String getCardUuid() {
        return this.cardUuid;
    }
    
    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public void setCardUuid(String cardUuid) {
        this.cardUuid = cardUuid;
    }
}

Jeśli podejść 'purystycznie’ do powyższego zapisu to użycie anotacji JPA czy Jackson może być uznane za złamanie definicji POJO.

@Entity
@Table(name = "CARDS")
@NoArgsConstructor
@Data
public class Card {
    @Id
    @Column(name = "CARD_ID")
    String cardId;

    @Column(name = "CARD_UUID")
    String cardUuid;

    @Column(name = "CARD_OWNER_ID")
    String cardOwnerId;

    @Column(name = "ENABLED")
    @Convert(converter = YesNoBooleanConverter.class)
    Boolean enabled;

    @Column(name = "COUNTRY")
    @Enumerated(EnumType.STRING)
    CardCountry cardCountry;
}

Jeśli tak to może pojawić się pytanie dlaczego w dokumentacji Hibernate jest odniesienie do POJO z anotacjami javax.persistence.Entity? Czy encja jest w takim razie klasą POJO?

Odpowiedź brzmi (jak zwykle w programowaniu) – to zależy. Jeżeli logika encji jest prosta, opiera się na danych z encji, nie ma skomplikowanej logiki (rich domain model) to można przymknąć oko na te anotacje. W takim modelu logika domenowa jest delegowana do innych obiektów domenowych, klas odpowiedzialnych za przetwarzanie danych.

Przykładem skomplikowanej logiki biznesowej może być np. transformacja danych z/do DTO, reagowanie na zdarzenia (event – zareaguj na to co się stało – np. konto zostało zablokowane z powodu przekroczenia limitu nieudanych prób logowania) lub rozkazy (command – zrób coś – np. blokada administracyjna konta)

W przypadku Hibernate czy JPA możliwe jest także skonfigurowanie mapowania w plikach XML. Dzięki temu anotacje nie będą występowały w klasie a obiekt może być traktowany jako podręcznikowy POJO.

Czy POJO musi być edytowalny? – nie. Ale niemodyfikowalnemu POJO jest bliżej do Value Object’a.

Value Object

Value Object reprezentuje zbiór danych, który jest niemodyfikowalny (immutable). Może zawierać logikę operującą na tych danych oraz opisywać pewien stan. Nie powinien zawierać skomplikowanej logiki ani identyfikować instancji obiektu (nie posiada tożsamości).

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.

.

final class Card {
    private final String cardOwner;

    private final BigDecimal cardLimit;

    public Card(String cardOwner, String cardLimit) {
        this.cardOwner= cardOwner;
        this.cardLimit= cardLimit;        
    }

    public String getCardLimit() {
        return this.cardLimit;
    }

    public String getCardOwner() {
        return this.cardOwner;
    }    
}

Powyższy przykład przedstawia Value Object dla karty (można taki kod wygenerować poprzez plugin Delombok do IDE dla klasy anotowanej @Value). Jak widać, nie ma on tożsamości (żadnego identyfikatora). Dodatkowo klasa jest final – tutaj pojawia się pewien aspekt traktowania dziedziczenia jako szczegółu implementacyjnego. Zatem kolejne ograniczenie dla Value Object – brak dziedziczenia.

Wczytując się w definicję to Value Object’em nie jest

  • obiekt identyfikowalny – np. encja (podstawowym atrybutem encji poza typem jest unikalny identyfikator)
  • obiekt o skompilowanej logice
  • obiekt, który można zmienić
  • obiekt, który dziedziczy

Czy Value Object może być POJO – tak.

Czy Value Object może być encją – nie, encja jest modyfikowalna oraz identyfikowalna. Można ukryć metody ustawiające dane, ale pola nie mogą być final, i sam framework JPA/Hibernate pod spodem modyfikuje stan obiektu podczas mapowania danych.

Data Transfer Object – DTO

Podstawowym zadaniem DTO jest agregowanie zbioru danych/atrybutów oraz umożliwienie/zapewnienie mechanizmów przesyłania/odbierania tych danych (np. serializacja/deserializacja). Nie powinien on mieć praktycznie żadnej logiki.

@Getter
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@JsonAutoDetect(fieldVisibility = ANY)
public class CardDto {
    String last4Digits;

    Boolean enabled;

    CardCountry cardCountry;

    UserDto owner;

    Instant expiresAt;

    BigDecimal balance;
}

Przesyłanie danych związane z DTO niekoniecznie musi być związane z wymianą przez sieć. DTO może być definicją danych we/wy pomiędzy modułami/subdomenami czy elementami systemu (jak np. backend i frontend).

Czy DTO może być edytowalny? – to zależy od frameworka dla serializacji/deserializacji (może on wymagać seterów i pustego konstruktora). Skoro zadaniem DTO jest wymiana danych, to edycji (setterów) nie musi być. Idealnie gdy DTO jest niemodyfikowalny.

Czy DTO może być Value Objectem? – tutaj jest pewne podobieństwo do sytuacji opisanej wcześniej – tzn. czy encja jest POJO. Możemy mieć informację o serializacji/deserializacji (np. poprzez anotacje Jackson). Ale gdy DTO nie ma żadnych odniesień do funkcjonalności związanych z frameworkami/(de)serialiazacją, nie jest edytowalny to jest pełnoprawnym Value Objectem.

Czy DTO jest POJO – podobnie jak Value Objectem – bez dodatkowych elementów związanych z transferem jest POJO.

Czy encja może być DTO – nie jest to rekomendowane.

Po pierwsze encja jest fragmentem domeny/dziedziny. Pod spodem jest jakiś framework (np. implementacja JPA), encje często są obiektami proxy, lub takie zawierają (mogą pojawić się problemy serializacją/deserializacją). Przy deserializacji pewnych danych może nie być (relacje) – czy to oznacza, że relacja ma być rozpięta a element podrzędny usunięty – niekoniecznie (a tak się może stać przy merge i nieodpowiednim ustawieniu kaskad w relacji).

Po drugie inna jest odpowiedzialność encji a inna DTO tzw. separation of concerns.

Po trzecie DTO może zawierać inne dane niż z encja, pewne agregaty, String zamiast relacji (np. nazwa województwa).

Po czwarte rolą DTO może być odseparowanie logiki dziedzinowej od funkcjonalności modułu i użycie DTO jako interfejsu wymiany danych. Pozwoli to w przyszłości na podmianę implementacji pod spodem, czy też wykrojenie do osobnego mikroserwisu (Loose Coupling and High Cohesion).

Projekcja encji

Projekcja dotyczy innej postaci danych z tabeli i/lub encji. W przypadku JPA czy Hibernate możesz zdefiniować kilka encji o różnej postaci dla tej samej tabeli.

@Entity
@Table(name = "Cards")
@Data
public class CardListViewItem {
    @Id
    @Column(name = "CARD_ID")
    String cardId;

    @Column(name = "CARD_UUID")
    String cardUuid;
    
    BigDecimal balance;    
}

Dodatkowo w Spring Data masz możliwość zdefiniowania mapowania danych pomiędzy encją (poprzez Repository) a interfejsem (mapowanie poprzez gettery o tej samej nazwę dla encji i interfejsu)

public interface CardListViewItem {
    String getCardId();

    String getCardUuid();
    
    BigDecimal getBalance();    
}

lub klasą DTO (mapowanie poprzez parametry konstruktora — składowe frazy select wstrzykiwane jako kolejne atrybuty konstruktora).

@Value
public class CardListViewItem {
    
    String cardId;

    String cardUuid;
    
    BigDecimal balance;       
}

W tym przypadku dla DTO nie ma transferu, ale jest pewna separacja/translacja danych. Rola projekcji jest inna niż encji – odczyt + ewentualna agregacja danych.

Czy projekcja może być Value Objectem – tak, w przypadku korzystania z interfejsów lub DTO (w Repository ze Spring Data).

Obiekt domenowy

Obiekt domenowy reprezentuje fragment dziedziny (modułu, obszaru), którego dotyczy, o określonym znaczeniu. Może on także zawierać logikę.

Przykłady obiektów domenowych

  • Użytkownik jako konto w systemie autoryzacji (identyfikator, hasło, data ważności konta)
  • Osoba (imię, nazwisko, identyfikator, data urodzenia) w strukturze organizacyjnej firmy
  • Limit karty w systemie autoryzacji operacji kartowych (rodzaj limitu, rodzaj ograniczenia np. kwota, liczba operacji dziennych, rozmiar ograniczenia np. 3000 lub 5).

Obiektem domenowym może być np. encja, ale może być to zupełnie inny obiekt, który nie ma persystencji.

Obiekt domenowy rozumiany jako domain entity wg. DDD powinien być identyfikowalny, przez co nie może być Value Objectem (nawet jeśli byłby niemodyfikowalny).

Przydatne linki

  • https://www.baeldung.com/java-pojo-class
  • https://martinfowler.com/bliki/ValueObject.html
  • https://en.wikipedia.org/wiki/Separation_of_concerns
  • https://www.baeldung.com/spring-data-jpa-projections

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