Użyj kompozycji i dziedziczenia dla DTO

13

Mamy interfejs API sieci Web ASP.NET, który zapewnia interfejs API REST dla naszej aplikacji jednostronicowej. Używamy DTO / POCO do przesyłania danych przez ten interfejs API.

Problem polega na tym, że z czasem te DTO stają się coraz większe, więc teraz chcemy zmienić DTO.

Szukam „najlepszych praktyk”, jak zaprojektować DTO: Obecnie mamy małe DTO, które składają się tylko z pól typu wartości, np .:

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Inne DTO używają tego UserDto według składu, np .:

public class TaskDto
{
    public int Id { get; set; }

    public UserDto AssignedTo { get; set; }
}

Ponadto istnieją pewne rozszerzone DTO, które są definiowane przez dziedziczenie od innych, np .:

public class TaskDetailDto : TaskDto
{
    // some more fields
}

Ponieważ niektóre DTO były używane w kilku punktach końcowych / metodach (np. GET i PUT), z czasem zostały one stopniowo zwiększane o niektóre pola. A dzięki dziedziczeniu i składowi inne DTO również się powiększyły.

Moje pytanie brzmi: czy dziedziczenie i skład nie są dobrymi praktykami? Ale kiedy ich nie wykorzystamy, to przypomina pisanie tego samego kodu wiele razy. Czy stosowanie DTO dla wielu punktów końcowych / metod jest złą praktyką, czy też powinny istnieć różne DTO, które różnią się tylko niektórymi niuansami?

oficer
źródło
6
Nie mogę ci powiedzieć, co powinieneś zrobić, ale według mojego doświadczenia w pracy z DTO dziedzictwo i kompozycja wcześniej niż później polują na ciebie jak zła kawa. Nigdy więcej nie użyję DTO. Zawsze. Ponadto nie uważam podobnych DTO za naruszenie SUCHEJ. Dwa punkty końcowe, które zwracają tę samą reprezentację, mogą ponownie wykorzystywać te same DTO. Dwa punkty końcowe, które zwracają podobne reprezentacje, nie zwracają tych samych DTO, więc dla każdego robię określone DTO . Gdybym był zmuszony dokonać wyboru, na dłuższą metę kompozycja jest mniej problematyczna.
Laiv
@Laiv to poprawna odpowiedź na pytanie, po prostu nie rób tego. Nie jestem pewien, dlaczego umieściłeś to jako komentarz
TheCatWhisperer
2
@Laiv: Czego zamiast tego używasz? Z mojego doświadczenia wynika, że ​​ludzie, którzy się z tym borykają, po prostu przeceniają to. DTO to tylko pojemnik na dane i to wszystko.
Robert Harvey
TheCatWhisperer, ponieważ moje argumenty opierałyby się głównie na opiniach. Wciąż staram się rozwiązywać tego rodzaju problemy w moich projektach. @RobertHarvey prawda, nie wiem, dlaczego widzę rzeczy trudniejsze niż w rzeczywistości. Nadal pracuję nad rozwiązaniem. Byłem całkiem przekonany, że HAL był modelem służącym do rozwiązania tych problemów, ale czytając twoją odpowiedź, zdałem sobie sprawę, że robię też moje DTO zbyt szczegółowe. Dlatego w pierwszej kolejności zastosuję twoje podejście. Zmiany będą mniej dramatyczne niż całkowite przejście do HATEOAS.
Laiv
Spróbuj tego, aby uzyskać dobrą dyskusję, która może ci pomóc: stackoverflow.com/questions/6297322/…
johnny

Odpowiedzi:

10

Najlepszą praktyką jest, aby Twoje DTO były jak najbardziej zwięzłe. Zwróć tylko to, co musisz zwrócić. Używaj tylko tego, czego potrzebujesz. Jeśli to oznacza kilka dodatkowych DTO, niech tak będzie.

W twoim przykładzie zadanie zawiera użytkownika. Prawdopodobnie nie jest tam potrzebny pełny obiekt użytkownika, może tylko nazwa użytkownika, któremu przypisano zadanie. Nie potrzebujemy reszty właściwości użytkownika.

Powiedzmy, że chcesz ponownie przypisać zadanie do innego użytkownika, może pojawić się pokusa, aby przekazać obiekt pełnego użytkownika i obiekt pełnego zadania podczas ogłoszenia o ponownym przypisaniu. Ale tak naprawdę potrzebne jest tylko identyfikator zadania i identyfikator użytkownika. To jedyne dwie informacje potrzebne do ponownego przypisania zadania, więc modeluj DTO jako takie. Zazwyczaj oznacza to, że istnieje oddzielne DTO dla każdego połączenia odpoczynku, jeśli dążysz do modelu lean DTO.

Czasem też może być potrzebne dziedziczenie / kompozycja. Powiedzmy, że ktoś ma pracę. Zadanie ma wiele zadań. W takim przypadku uzyskanie pracy może również zwrócić listę zadań dla pracy. Nie ma więc żadnej reguły przeciwko kompozycji. To zależy od tego, co jest modelowane.

Jon Raynor
źródło
Tak zwięzłe, jak to możliwe, oznacza również, że powinienem odtworzyć DTO, jeśli różnią się tylko szczegółami - prawda?
oficer
1
@officer - Ogólnie tak.
Jon Raynor
2

O ile twój system nie opiera się wyłącznie na operacjach CRUD, twoje DTO są zbyt szczegółowe. Spróbuj utworzyć punkty końcowe, które ucieleśnią procesy biznesowe lub artefakty. Takie podejście ładnie odwzorowuje się na warstwę logiki biznesowej i „projekt oparty na domenie” Erica Evansa.

Załóżmy na przykład, że masz punkt końcowy, który zwraca dane do faktury, dzięki czemu może być wyświetlana na ekranie lub formularzu użytkownikowi końcowemu. W modelu CRUD potrzeba kilku wywołań do punktów końcowych, aby zebrać niezbędne informacje: imię i nazwisko, adres rozliczeniowy, adres wysyłki, elementy zamówienia. W kontekście transakcji biznesowych pojedyncze DTO z jednego punktu końcowego może zwrócić wszystkie te informacje naraz.

Robert Harvey
źródło
Robert, masz na myśli tutaj Korzeń Kruszywa?
Johnny
Obecnie nasze DTO są zaprojektowane tak, aby dostarczać całe dane do formularza, np. Podczas wyświetlania listy czynności do wykonania, TodoListDTO ma listę zadań do wykonania. Ale ponieważ ponownie używamy TaskDTO, każdy z nich ma również UserDTO. Problemem jest więc duża ilość danych, które są wyszukiwane w wewnętrznej bazie danych i przesyłane przewodowo.
oficer
Czy wszystkie dane są wymagane?
Robert Harvey
1

Trudno jest ustalić najlepsze praktyki dla czegoś tak „elastycznego” lub abstrakcyjnego jak DTO. Zasadniczo DTO są jedynie obiektami do przesyłania danych, ale w zależności od miejsca docelowego lub przyczyny transferu możesz chcieć zastosować różne „najlepsze praktyki”.

Polecam przeczytać Wzory architektury aplikacji korporacyjnych autorstwa Martina Fowlera . Cały rozdział poświęcony jest wzorcom, w których DTO otrzymują naprawdę szczegółowy rozdział.

Początkowo zostały one „zaprojektowane” do użycia w kosztownych zdalnych połączeniach, w których prawdopodobnie potrzebna byłaby duża ilość danych z różnych części logiki; DTO przesyłałyby dane w jednym połączeniu.

Według autora, DTO nie były przeznaczone do stosowania w lokalnych środowiskach, ale niektóre osoby znalazły dla nich zastosowanie. Zwykle służą do gromadzenia informacji z różnych POCO w jednym obiekcie dla GUI, interfejsów API lub różnych warstw.

Teraz, w przypadku dziedziczenia, ponowne użycie kodu jest bardziej efektem ubocznym dziedziczenia niż jego głównym celem; natomiast kompozycja jest implementowana z ponownym użyciem kodu jako głównym celem.

Niektóre osoby zalecają stosowanie kompozycji i dziedziczenia razem, wykorzystując mocne strony obu i próbując złagodzić swoje słabości. Następujące czynności są częścią mojego procesu mentalnego podczas wybierania lub tworzenia nowych DTO lub jakiejkolwiek nowej klasy / obiektu w tym zakresie:

  • Używam dziedziczenia z DTO w tej samej warstwie lub w tym samym kontekście. DTO nigdy nie odziedziczy po POCO, BLL DTO nigdy nie odziedziczy po DAL DTO itp.
  • Jeśli próbuję ukryć pole przed DTO, dokonam refaktoryzacji i być może użyję kompozycji.
  • Jeśli potrzebuję bardzo niewielu różnych pól z bazowego DTO, umieszczę je w uniwersalnym DTO. Uniwersalne DTO są używane tylko wewnętrznie.
  • Podstawowy POCO / DTO prawie nigdy nie będzie używany do żadnej logiki, w ten sposób baza odpowiada tylko potrzebom swoich dzieci. Jeśli kiedykolwiek będę potrzebować skorzystać z bazy, unikam dodawania nowego pola, którego jego dzieci nigdy nie użyją.

Niektóre z nich mogą nie być „najlepszymi” praktykami, działają całkiem dobrze w projektach, nad którymi pracuję, ale musisz pamiętać, że żaden rozmiar nie pasuje do wszystkich. W przypadku uniwersalnego DTO należy zachować ostrożność, moje podpisy metod wyglądają tak:

public void DoSomething(BaseDTO base) {
    //Some code 
}

Jeśli którakolwiek z metod kiedykolwiek potrzebuje własnego DTO, dziedziczę i zwykle jedyną zmianą, którą muszę wprowadzić, jest parametr, chociaż czasem muszę głębiej szukać konkretnych przypadków.

Z twoich komentarzy wynika, że ​​używasz zagnieżdżonych DTO. Jeśli twoje zagnieżdżone DTO składają się tylko z listy innych DTO, myślę, że najlepszą rzeczą do zrobienia jest rozpakowanie listy.

W zależności od ilości danych, które należy wyświetlić lub z którymi pracować, dobrym pomysłem może być utworzenie nowych DTO, które ograniczają dane; na przykład, jeśli twoje UserDTO ma wiele pól i potrzebujesz tylko 1 lub 2, lepiej mieć DTO tylko z tymi polami. Zdefiniowanie warstwy, kontekstu, użycia i użyteczności DTO bardzo pomoże w jego projektowaniu.

IvanGrasp
źródło
Nie mogłem dodać więcej niż 2 linków w mojej odpowiedzi, oto kilka informacji o lokalnych DTO. Wiem, że to bardzo stara informacja, ale myślę, że niektóre z nich mogą być nadal aktualne.
IvanGrasp
1

Używanie kompozycji w DTO jest doskonałą praktyką.

Dziedziczenie wśród konkretnych rodzajów DTO jest złą praktyką.

Po pierwsze, w języku takim jak C # właściwość zaimplementowana automatycznie ma bardzo mały narzut związany z utrzymywalnością, więc powielanie ich (naprawdę brzydzę się powielaniem) nie jest tak szkodliwe, jak to często bywa.

Jednym z powodów, dla których nie należy stosować konkretnego dziedziczenia wśród DTO, jest to, że niektóre narzędzia chętnie mapują je na niewłaściwe typy.

Na przykład, jeśli korzystasz z narzędzia bazy danych, takiego jak Dapper (nie polecam, ale jest popularne), które wykonuje class name -> table namewnioskowanie, możesz łatwo zapisać typ pochodny jako konkretny typ podstawowy gdzieś w hierarchii, a tym samym stracić dane lub gorzej .

Głębszym powodem, dla którego nie należy używać dziedziczenia między DTO, jest to, że nie należy go używać do udostępniania implementacji między typami, które nie mają oczywistej relacji „to”. Moim zdaniem TaskDetailnie brzmi to jak podtyp Task. Równie łatwo może być własnością Tasklub, co gorsza, może być nadtypem Task.

Teraz możesz się martwić o zachowanie spójności między nazwami i typami właściwości różnych powiązanych DTO.

Podczas gdy dziedziczenie konkretnych typów pomógłoby w konsekwencji zapewnić taką spójność, o wiele lepiej jest używać interfejsów (lub czystych wirtualnych klas bazowych w C ++) do utrzymania tego rodzaju spójności.

Rozważ następujące DTO

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
Aluan Haddad
źródło