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?
Odpowiedzi:
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.
źródło
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.
źródło
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:
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:
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.
źródło
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 name
wnioskowanie, 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
TaskDetail
nie brzmi to jak podtypTask
. Równie łatwo może być własnościąTask
lub, co gorsza, może być nadtypemTask
.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
źródło