Idea DTO

1

Cześć,

załóżmy, że mamy takie encję:

Pisane pseudokodem, nie zwracajcie uwagi na szczegóły. Liczy się idea.

public class Person
{
	public int Id { get; set; }
	public string Name { get; set; }
	public string Surname { get; set; }
	public List<Address> Addresses { get; set; }
}

public class Address
{
	public int Id { get; set; }
	public string Street { get; set; }
}

Załóżmy, że dla przykładu dane w bazie wyglądają następująco:

// Już po zjoinowaniu tabeli Person i Address
[Name], [Surname], [Street]
Grzesiek, Grześkowski, Wrocławska
Grzesiek, Grześkowski, Krakowska
Kamil, Kamilowski, Poznańska
Ryszard, Kwiatkowski, NULL

Jak widać jest tutaj Grzesiek Grześkowski, który posiada dwa adresy Kamil, który posiada takowy jeden oraz Ryszard, który nie posiada wpisów w tabeli adresów, co przekłada się automatycznie na zawartość listy w klasie Person.

No i gdzieś tam jest sobie apka, do której podłączam model z tymi encajmi, serwisami itd. W jednym z widoków potrzebuję wyświetlić sobie dane w taki sposób:

[Name], [Surname], [HasAddress]
Grzesiek, Grześkowski, true
Kamil, Kamilowski, true
Ryszard, Kwiatkowski, false

Teraz kwestia DTO i moje pytanie:

  • Czy powinienem wystawić DTO, które tak naprawdę będzie wynikiem wyspecjalizowanego selecta z bazy, czyli:
public class PersonDTO
{
		public int Id { get; set; }
		public string Name { get; set; }
		public bool HasAddress { get; set }
}
  • Jeżeli tak to czy do wstawiania danych powinienem stworzyć osobne DTO posiadające jawnie kolekcję adresów (wstawić kompletnych danych przy pomocy pierwszego DTO nie można, bo nie mamy jak przekazać do serwisu informacji o adresach) np. tak:
public class PersonAlteringDTO
{
		public int Id { get; set; }
		public string Name { get; set; }
		public List<AddressDTO> Addresses { get; set; }
}
  • A może zawsze wystawiać uniwersalne DTO, takie jak PersonAlteringDTO i niech dopiero wyższe warstwy zajmą się odpowiednią obróbką danych? Jeżeli tak to czy kolekcja innego DTO wewnątrz nadrzędnego DTO to dobry pomysł? Może powinienem przekazać listę idków powiązanych adresów? Jest to trochę mniej problematyczne niż lista DTO reprezentująca adresy, ponieważ używając Automappera do zmapowania takiego DTO można się zapętlić i dostać StackOverflowException. Można oczywiście określić MaxDepth ale czy to jest dobra droga? Jak wiadomo DTO powinno być najprostsze.

Podpowiedzcie, bo zaczynam się gubić w tym jakie dane przekazywać wyższym warstwom i jak to zrobić żeby było wygodnie również przy zmianie/wstawianiu danych.

3

Na początku, chciałbym zauważyć że idea DTO jest upośledzona od samego początku. W teorii jest to obiekt który posiada tylko publiczne właściwości (zwykle pole + get + set) i nie posiada logiki. Ktoś przekorny mógłby pomyśleć że to zwykła struktura skoro gwałcimy zasady obiektowości, no ale przecież jest get i jest set, a do pola nie da się dostać bo jest prywatne więc jest spoko. Więc nazwijmy to obiektem, bo mamy prawie XXI wiek i teraz piszę się obiektowo.

Ja zakładam jednak że DTO czy JSON to jedynie struktura i tak jest dużo łatwiej załapać o co chodzi. Oczywiście get/set musi być, bo frameworki tego wymagają, ale to myślenie na odwrót.

Wracając do Twoich pytań. Projektując DTO, czyli strukturę odpowiedzi z jakiejś usługi, nie patrzysz w ogóle na to co znajduje się pod spodem. Nie możesz nawet założyć co jest w innej warstwie i jak to wygląda od strony technicznej. Musisz spojrzeć na konkretny przypadek który chcesz obsłużyć i pod ten konkretny przypadek projektować. Jeżeli serwis ma zwracać usługa ma zwracać danego człowieka z adresem, to tak projektujesz DTO. Jeżeli oprócz tego ma dodać kod pocztowy którego nie masz w bazie i liczbę przejechanych kilometrów na rowerze z endomondo, to też to dodajesz. Dopiero potem w serwisie pobierasz z bazy to co masz, dociągasz z endomondo liczbę kilometrów i zipcode z innej usługi na podstawie ulicy i miasta. Ktoś kto skorzysta z usługi, otrzyma konkretne dane, ale nie ma pojęcia skąd i w jaki sposób. W tym wypadku nie jest też istotne czy jest to usługa REST zwracająca json albo soap, czy po prostu spakujesz to jako libkę i będzie to jedna z warstw Twojej aplikacji.

0

Wracając do Twoich pytań. Projektując DTO, czyli strukturę odpowiedzi z jakiejś usługi, nie patrzysz w ogóle na to co znajduje się pod spodem. Nie możesz nawet założyć co jest w innej warstwie i jak to wygląda od strony technicznej. Musisz spojrzeć na konkretny przypadek który chcesz obsłużyć i pod ten konkretny przypadek projektować.

Czyli jedno DTO dla widoku, tylko do prezentacji danych, a drugie z kolekcją adresów do wstawiania? Tylko czy tym sposobem nie uzależniam swoich serwisów od głównej aplikacji? Idąc tym tropem to, patrząc na razie na SQL'a i nie bawiąc się w jakieś endomondo etc., wyciągam dane wyspecjalizowanym selectem. Teraz gdy przyjdzie obsłużyć podobny widok to będę musiał zrobić kolejne, wyspecjalizowane DTO, a do tego kolejną metodę, które opakuje dane w odpowiedni sposób dla tegoż DTO. Nie lepiej zrobić jednak uniwersala i niech warstwa wyżej to obrabia? Wedle zasady, że: "masz tutaj wszystkie dane z serwisu, przerób na viewmodel"

5

@grzesiek51114: na przykladzie:
jezeli masz zwrocic liste osob do tabelki a tam w jednej z kolumn dla kazdej osoby beda wypisane ich adresy to robisz PersonWithAddressDTO i po tym sie iterujesz na widoku.
jezeli masz zwrocic liste osob do tabelki a tam w jednej z kolumn bedzie przycisk: pokaz adresy uzytkownika -> wtedy mozesz zwrocic samo PersonDTO i jak uzytkownik kliknie "pokaz adresy uzytkownika" to wywolac wtedy javascriptem serwis ktory zwroci Ci List<PersonAddressDTO>. chodzi o to zeby zrobic to optymalnie w kontekscie wydajnosci, wygody pisania w przekroju wszystkich warstw. to ze bedziesz miec PersonDTO , PersonAddressDTO i PersonWithAddressDTO nic nie szkodzi. jak bedziesz chcial na sile uniwersalnie robic i za kazdym razem wywolywac i dociagac dla kazdej osoby osobno AddressDTO to pomysl ile razy zawolasz usluge, ile selectow wygenerujesz i jak beznajdziejnie bedzie wygladac to po stronie widoku - w przypadku listingu osob (PersonDTO) wraz z ich adresami (AddressDTO) bedziesz musial opakowac to chcoaizby przez jakiegos Tuple. wiec wiesz. EASY CODE EASY GO. a w backendzie to juz mozesz sobie ladnie zrobic i zoptymalizowac.

PS ^- ta porada jest odplatna i jak skorzystasz to musisz zaplacic 100,00 zl

0

...na sile uniwersalnie robic i za kazdym razem wywolywac i dociagac dla kazdej osoby osobno AddressDTO to pomysl ile razy zawolasz usluge, ile selectow wygenerujesz

No dokładnie. W zasadzie główne pytanie tego wątku powinno brzmieć: Czy dzielić DTO na powiedzmy: InsertionDTO i SelectionDTO, tak jak napisałaś, rzecz jasna bez tych nazw, bo chodzi o ideę..? Czy zastosować uniwersala?

Widzę, że Feng shui mówi jednak, że silnie wyspecjalisowane DTO to nic złego. I w sumie zgadzam się i mnożenie metod, które generują poszczególne DTO da się przeżyć, a korzyść z tego taka, że maleje ilość zapytań. To prawda i tez za każdym razem w górę nie są transportowane ciężkie obiekty tylko rzeczy złożone ładnie z prymitywów. Do wstawiania można użyć odpowiedniego InsertionDTO i też chyba będzie to w porządku.

3

Popatrz na to od strony użytkownika serwisu.
Sytuacja #1
Pobierasz osobę z serwisu getPersonWithAddress(Integer personId) i dostajesz osobę z adresem i listą ulubionych dań. Coś jest nie tak nie?
Sytuacja #2
Masz serwis do zapisu adresu osoby savePersonAddress(Person personWithAddress) i do środka możesz włożyć listę ulubionych dań. Po co? Co się stanie gdy je podasz? Co się stanie gdy ich nie podasz?

W obu przykładach wyspecjalizowane DTO usunęło by te problemy. Ale tak naprawdę chodzi o coś innego. Jak zaczniesz dorzucać nowe rzeczy do mniej więcej wspólnego DTO, to w aplikacji która ma być rozwijana latami tego nie da się opanować. Potem pojawiają się jakieś dane, a ludzie mają dziwną tendencję do dopychania nowych danych gdzieś gdzie mniej więcej pasuje, bo po co robić nową klasę. I w efekcie masz mega spuchnięte struktury a usługa korzysta z kilku z nich. Mało tego. Jak masz duże obiekty to zwykle masz pojedyncze duże mappery, które zmieniają jedną klasę w drugą. I tak serwis z przypadku #2 może działać na początku dobrze, a po jakimś czasie ktoś rozszerzy Person o listę posiadanych butów, zaktualizuje mapper który zamieni DTO na encję ORM i tadam, masz usługę do aktualizacji adresu która może uaktualnić listę butów. Oczywiście taką sytuację mogłyby wykryć testy integracyjne, ale życie nie jest tak piękne i nie spotkałem się z dobrymi testami integracyjnymi z bazą. Jeżeli ktoś jeszcze pisze projekt na hurrra, ładując wszystko do 1 obiektu to jestem pewny że albo testów integracyjnych nie ma, albo są, tylko ssą pałę a istnieją żeby "być na czasie". Przykład z mapperami zadziała też w przypadku #1 gdzie oprócz adresu dostaniesz listę butów, dociągniętych w mapperze oczywiście metodą n+1 selektów, bo nikt tego nie przewidział.

0

Czyli DTO służące do wstawiania czy zmiany danych niekoniecznie musi być odwzorowaniem encji? Kurcze tak też myślałem, bo niekoniecznie trzeba wypełniać wszystkie zależności, które są w encji. Ich transportowanie byłoby kompletnie bez sensu, bo to przecież strasznie ciężkie obiekty.

1

DTO nigdy nie powinno być odwzorowaniem encji 1:1. Czy to biznesowej czy bazodanowej. DTO ma służyć do transportu danych pomiędzy usługami, więc powinien zawierać to co jest niezbędne do ich pełnej obsługi.

0

Tak tez myślałem tylko zastanawiałem się nad rozgrupowaniem DTO na np. Insertion i Selection i przejmowałem się tym, że może namnożyć się trochę metod przy większym projekcie. Powoli rozwiewacie wątpliwości.

2

To też nie chodzi o to żeby robić sztuczne rozgraniczenie na CRUDa. Taka struktura jak Address pasuje przecież do każdego typu metody, bo zawsze będzie zawierać ten sam zestaw pól.

Metod się nie namnoży, a co najwyżej klas, ale uwierz mi że lepiej mieć 20 klas, przy zmianie poprawić 1 i zapomnieć, niż mieć 1 i po zmianie zachowania 1 metody zastanawiać się czy pozostałe 19 się nie rozjechało.

0

To też nie chodzi o to żeby robić sztuczne rozgraniczenie na CRUDa.

Tak, tak - jeżeli jedno DTO pasuje co całego CRUD'a to także uważam, że rozbijanie jest bez sensu. IMHO rozbić trzeba kiedy w SelectionDTO są pola składane dynamicznie albo brakowałoby danych do pełnego uzupełnienia encji^, chcąc wykorzystać to samo DTO do modyfikacji.

^ czy jakichkolwiek danych po stronie backendu.

1

Polecam przestać skupiać się na używaniu SelectionDTO/InsertionDTO. Po prostu operacja coś robi przy użyciu jakichś danych. Zwykle metod pobierających może być mnóstwo, a każda zwraca inne wyniki lub przy użyciu innych parametrów.

2

Kod z pierwszego posta to nie encje tylko również "DTO". Zresztą, w moim odczuciu wątek w ogóle nie jest o DTO, bo de facto chodzi tu o coś trochę innego niż przesyłanie danych między warstwami/aplikacjami.
I to nie jest tak, że idea DTO jest upośledzona. To Java i C# są upośledzone, bo zarówno DTO jak i pełnoprawne klasy zgodne z OOP definiuje się w tych językach przy użyciu tego samego słowa kluczowego class. (Ok, w C# jest też struct, ale nie służy ono do DTO tylko do czegoś zupełnie innego.)

Co do tematu:
Po pierwsze SRP. Klasa powinna mieć jeden powód do zmiany. Zmiana danych wyświetlanych użytkownikowi oraz zmiana danych zapisywanych do bazy, to dwa różne powody.
Po drugie, gratuluję. Dzięki oddzieleniu od siebie read i write modeli prawie odkryliście CQRS. :)

0

@somekind: rozwiń trochę to co napisałeś. Jak byś rozwiązał przekazywanie danych pomiędzy serwisem, a warstwą wyższą? Wiadomo, że nie przekazujemy encji wyżej. Encje siedzą sobie pod spodem i warstwy wyższe nie powinny ich widzieć wobec tego przychodzi od razu do głowy pomysł zastosowania DTO. IMHO jest to dobre rozwiązanie albo jestem jeszcze czegoś nieświadom. :-)

Zmiana danych wyświetlanych użytkownikowi oraz zmiana danych zapisywanych do bazy, to dwa różne powody.

Więc również dwie różne klasy DTO. Jedna do przesyłu danych służących do wyświetlania, druga do przekazywania danych do zapisu. Czyli odnośnie klas, które przedstawiłem w pierwszym poście:

  • Jedno DTO przenoszące wyniki "spłaszczonego selecta" do warstwy wyżej, bez żadnych kolekcji. Tylko to co jest, powiedzmy potrzebne w gridzie w widoku;
  • Drugie DTO służące do wstawiania / zmiany danych, mające w sobie pola Name i Surname oraz powiedzmy IList<AddressDTO> do trzymania informacji o adresach. Załóżmy, że klasa / "encja" / Address posiada bardzo wiele pól więc idea rozbijania jej na wartości typu: AddressName itp. mija się z celem.
  • Można by pójść jeszcze o krok dalej i utworzyć różne DTO do manipulowania danymi, w zależności do tego jakie dane chcemy zmieniać. Jedno DTO przenosiłoby powiedzmy Name i Surname, bo tylko to w danym miejscu apki można edytować, a drugie PersonName i StreetName do wykorzystania gdzie indziej itd. Wedle tego co napisał @krzysiek050 Polecam przestać skupiać się na używaniu SelectionDTO/InsertionDTO

Dobrze rozumiem?

Po drugie, gratuluję. Dzięki oddzieleniu od siebie read i write modeli prawie odkryliście CQRS.

Tak czytam o co chodzi i to chyba nie sarkazm tylko... komplement. Dzię-ku-je-my ;)

1
grzesiek51114 napisał(a):

@somekind: rozwiń trochę to co napisałeś. Jak byś rozwiązał przekazywanie danych pomiędzy serwisem, a warstwą wyższą? Wiadomo, że nie przekazujemy encji wyżej. Encje siedzą sobie pod spodem i warstwy wyższe nie powinny ich widzieć wobec tego przychodzi od razu do głowy pomysł zastosowania DTO. IMHO jest to dobre rozwiązanie albo jestem jeszcze czegoś nieświadom. :-)

Wydaje mi się, że jedyne sensowne.

  • Jedno DTO przenoszące wyniki "spłaszczonego selecta" do warstwy wyżej, bez żadnych kolekcji. Tylko to co jest, powiedzmy potrzebne w gridzie w widoku;

Ten "spłaszczony select" nazywa się projekcją. :) No i takie DTO jest de facto od razu viewmodelem, bo służy o wyświetlania w widoku.

  • Drugie DTO służące do wstawiania / zmiany danych, mające w sobie pola Name i Surname oraz powiedzmy IList<AddressDTO> do trzymania informacji o adresach. Załóżmy, że klasa / "encja" / Address posiada bardzo wiele pól więc idea rozbijania jej na wartości typu: AddressName itp. mija się z celem.

Ja bym pewnie nawet oddzielił DTO od nowego rekordu i od aktualizacji istniejącego. Często np. pewnych danych nie można zmieniać po utworzeniu rekordu.

  • Można by pójść jeszcze o krok dalej i utworzyć różne DTO do manipulowania danymi, w zależności do tego jakie dane chcemy zmieniać. Jedno DTO przenosiłoby powiedzmy Name i Surname, bo tylko to w danym miejscu apki można edytować, a drugie PersonName i StreetName do wykorzystania gdzie indziej itd. Wedle tego co napisał @krzysiek050 Polecam przestać skupiać się na używaniu SelectionDTO/InsertionDTO

Też można, wszystko zależy od konkretnego przypadku.

P.S. Sorry za późną odpowiedź, wątek zaginął mi gdzieś w otwartych kartach. ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1