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ć 🙂