Pracując z większymi systemami często spotykamy się z pojęciem obiektów domenowych, DTO, a okazyjnie natrafiamy nawet na DAO. Każda z tych nazw określa pewien typ obiektów, które mają określone zadanie.
Domeny i obiekty domenowe
Zacznijmy od najważniejszego z nich, czyli obiektu domenowego. Domena w programowaniu to, ogólnie mówiąc, pewien obszar odpowiedzialności naszego programu.
Tworząc system do rezerwacji pokoi hotelowych mamy na przykład trzy odpowiedzialności takiego systemu:
- zarządzanie pokojami
- zarządzanie listą gości
- zarządzanie samymi rezerwacjami
To są nasze trzy domeny: pokoje, goście i rezerwacje. Każda z tych domen jest odciętym od świata zewnętrznego (poza jedną parą „drzwi” – serwisem dostępowym) małym światem.
Domena zarządzania pokojami będzie odpowiadać za dodawanie nowych pokoi do systemu, oznaczanie, które są wolne, a które zarezerwowane, ustawianiem jaki typ łóżka, ile osób w danym pokoju może się znajdować etc. etc. Do tego oczywiście dochodzą wymagania niebiznesowe, jak persystencja danych, logowanie tego, co się dzieje i tak dalej.
Obiekt domenowy to serce całej domeny. Jest to główna klasa, której to obiekty reprezentują główną rzecz/idee obsługiwaną przez daną domenę.
W naszym wypadku wyznaczenie obiektów domenowych jest dość proste. Będą to klasy odpowiedzialne za przechowywanie danych o pokojach, gościach i rezerwacjach.
Przykład z pewnego momentu mojego kursu Kompletna aplikacja w języku Java – od zera do installera:
package pl.clockworkjava.domain.reservation; import pl.clockworkjava.domain.guest.Guest; import pl.clockworkjava.domain.reservation.dto.ReservationDTO; import pl.clockworkjava.domain.room.Room; import java.time.LocalDateTime; public class Reservation { private final long id; private final Room room; private final Guest guest; private final LocalDateTime from; private final LocalDateTime to; public Reservation(long id, Room room, Guest guest, LocalDateTime from, LocalDateTime to) { this.id = id; this.room = room; this.guest = guest; this.from = from; this.to = to; } String toCSV() { return String.format("%s,%s,%s,%s,%s%s", this.id, this.room.getId(), this.guest.getId(), this.from.toString(), this.to.toString(), System.getProperty("line.separator")); } public long getId() { return this.id; } public ReservationDTO getAsDTO() { return new ReservationDTO(this.id, this.from, this.to, this.room.getId(), this.room.getNumber(), this.guest.getId(), this.guest.getFirstName() + " " + this.guest.getLastName()); } public LocalDateTime getFrom() { return this.from; } public Room getRoom() { return this.room; } public LocalDateTime getTo() { return this.to; } }
Obiekty domenowe są zapisywane w bazie danych (albo persystowane w inny sposób) i zmiany w ich wartościach są audytowane (logowane, zapisywane by było wiadomo kiedy i co się zmieniło). Generalnie stanowią serce naszego programu.
DTO (Data Transfer Object)
Drugim typem obiektów jest DTO, czyli Data Transfer Object. Są to obiekty, które są tylko zbiorem danych (pól) i posiadają tylko zestaw getterów. Nie posiadają one żadnej logiki biznesowej.
Służą one do przesłania danych poza część aplikacji odpowiedzialną za logikę biznesową do na przykład front-endu. Jeśli ktoś chcę, by jego kod był bardzo „zamknięty” to posuwa się krok dalej i z domeny nie „wychodzą” obiekty domenowe, a okrojone DTO właśnie.
Jeśli nasza aplikacja udostępnia dane o rezerwacjach poprzez interfejs REST to zamiast wysyłać na świat cały obiekt domenowy Reservation, który może posiadać bardzo dużo informacji (np. dane adresowe osoby aktualnie rezerwującej dany pokój) i narażać się na przechwycenie tych danych lepiej stworzyć obiekt DTO. Posiada on część danych niezbędnych w danym kontekście i jest tworzony na podstawie obiektu domenowego. To właśnie DTO jest zwracane przez REST.
Przykład DTO z domeny rezerwacji:
package pl.clockworkjava.domain.reservation.dto; import java.time.LocalDateTime; public class ReservationDTO { private long id; private LocalDateTime from; private LocalDateTime to; private long roomId; private int roomNumber; private long guestId; private String guestName; public ReservationDTO(long id, LocalDateTime from, LocalDateTime to, long roomId, int roomNumber, long guestId, String guestName) { this.id = id; this.from = from; this.to = to; this.roomId = roomId; this.roomNumber = roomNumber; this.guestId = guestId; this.guestName = guestName; } public long getId() { return id; } public LocalDateTime getFrom() { return from; } public LocalDateTime getTo() { return to; } public long getRoomId() { return roomId; } public int getRoomNumber() { return roomNumber; } public long getGuestId() { return guestId; } public String getGuestName() { return guestName; } }
Obiekty typu DTO są dość „lekkie” i nie są w żaden sposób przechowywane w bazie danych oraz audytowane, więc możemy klas takich obiektów tworzyć wiele. Obiekt domenowy Reservation może mieć kilka DTO – każdy tworzony na potrzebny jednej metody, jednego „endpointa” w systemie.
Również DTO nie muszą posiadać danych tylko z jednego obiektu domenowego. Możemy tworzyć DTO, który łączy dane o rezerwacji (daty), pokoju (numer) oraz gościu (imię i nazwisko) i taki zestaw danych udostępniać.
Wszystko wedle wymagań, bo ponownie – DTO są „tanie”.
DAO (Data Access Object)
O ile obiekty domenowe czy DTO to „tradycyjne” obiekty, których w systemie istnieje wiele to DAO w danej domenie jest singletonem. Jest to klasa odpowiedzialna za zapisywanie i odczyt obiektów domenowy do bazy danych czy innego miejsca, gdzie zapisujemy trwale dane z obiektów domenowych. Obecnie o wiele częściej zamiast nazwy DAO funkcjonuje nazwa Repozytorium (z nomenklatury Domain Driven Design). Technicznie repozytoria, poza samą translacją z i do bazy danych, mają jeszcze dodatkowe odpowiedzialności jednak można spokojnie uznać DAO i Repozytoria pełnią te same funkcje w architekturze.
Przykładowy wycinek kodu Repozytorium:
package pl.clockworkjava.domain.reservation; //... public class ReservationDatabaseRepository implements ReservationRepository { private List<Reservation> reservations = new ArrayList<>(); //... @Override public Reservation createNewReservation(Room room, Guest guest, LocalDateTime from, LocalDateTime to) { try { String fromAsStr = from.format(DateTimeFormatter.ISO_DATE_TIME); // yyyy-MM-dd HH:mm:ss String toAsStr = to.format(DateTimeFormatter.ISO_DATE_TIME); String createTemplate = "INSERT INTO RESERVATIONS(ROOM_ID, GUEST_ID, RESERVATION_FROM, RESERVATION_TO) VALUES (:roomId, :guestId, :resFrom, :resTo)"; PreparedStatement statement = SystemUtils.connection.prepareStatement(createTemplate, Statement.RETURN_GENERATED_KEYS); statement.setLong("roomId", room.getId()); statement.setLong("guestId", guest.getId()); statement.setString("resFrom", fromAsStr); statement.setString("resTo", toAsStr); statement.executeQuery(); ResultSet rs = statement.getGeneratedKeys(); long id=-1; while(rs.next()) { id = rs.getLong(1); } Reservation newReservation = new Reservation(id, room, guest, from, to); this.reservations.add(newReservation); return newReservation; } catch (SQLException throwables) { System.out.println("Błąd przy tworzeniu rezerwacji"); throw new RuntimeException(throwables); } } //... }
I na tym zakończymy.
By być na bieżąco i mieć realny wpływ na tematykę tworzonych przeze mnie artykułów zapraszam do dołączenia do mojego newslettera.
W ostatnim przykładzie masz „prawie” SQL Injection. Przyzwoitość każe użyć w stylu:
statement =con.prepareStatement(„SELECT * from employee WHERE userID = :userId”);
statement.setString(userId, userID);
ResultSet rs = statement.executeQuery();
A więc przykazaywania parametrów za pomocą :name lub przez ’?’.
Użycie String.format to igranie z ogniem.
Dokładnie tak jest. akurat wziąłem losowy kawałek repozytorium z losowego momentu tworzenia aplikacji, zanim prepared statements zostały wprowadzone. Natomiast uwaga jak najbardziej słuszna i dla przyzwoitości poprawie, bo faktycznie przez nieuwagę moją ktoś może zły przykład brać 🙂
Oraz fakt aby nie uzywać „*” tylko podawać konkretne pola które nas interesują.
Cześć! Muszę przyznać, że bardzo ciekawie piszesz i zrozumiale. Ode mnie mała uwaga, aby zastanowić się bardziej nad klasą Reservation, ponieważ skoro jest to klasa domenowa to jej odpowiedzialnością nie powinno być formatowanie jej do CSV. Według mnie dodatkowo gettery również nie są dobrym rozwiązaniem, ponieważ uzewnętrzniamy jej szczegóły implementacyjne.
Życzę Ci samych sukcesów!
Hej, akurat tu się nie zgodzę 🙂 IMO skoro klasa ma wszystkie dane potrzebne do tego, by wypluć postać CSV czy w dowolnej innej formie tekstowej to niech to zrobi. Jeśli musiałby to robić dedykowany byt… to nasz obiekt domenowy musiały wystawić gettery do każdej danej, która ma być wystawiona na zewnątrz… i tu przechodzimy do drugiego punktu Twojego komentarza. Z którym w 100% się zgadzam – gettery, a zwłaszcza settery, tylko gdy są niezbędnie niezbędne 🙂
Dzięki i również pozdrawiam 🙂