Використовуйте склад та успадкування для ДТО


13

У нас є веб-API ASP.NET, який надає API REST для нашої програми на одній сторінці. Ми використовуємо DTO / POCO для передачі даних через цей API.

Проблема полягає в тому, що ці DTO з часом збільшуються, тому тепер ми хочемо відновити DTO.

Я шукаю "найкращі практики", як створити DTO: В даний час у нас є невеликі DTO, які складаються лише з полів типу значень, наприклад:

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

    public string Name { get; set; }
}

Інші DTO використовують це UserDto за складом, наприклад:

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

    public UserDto AssignedTo { get; set; }
}

Також є деякі розширені DTO, які визначаються по спадку від інших, наприклад:

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

Оскільки деякі DTO використовуються для декількох кінцевих точок / методів (наприклад, GET та PUT), вони поступово збільшуються деякими полями. А завдяки спадкоємності та складу інші ЗНО також збільшилися.

Моє запитання зараз - це спадкування та склад, а не хороша практика? Але коли ми не використовуємо їх повторно, відчувається, що ми пишемо один і той же код кілька разів. Чи погана практика використовувати DTO для кількох кінцевих точок / методів, чи повинні бути різні DTO, які відрізняються лише деякими нюансами?


6
Я не можу сказати вам, що вам робити, але, на мій досвід роботи з DTO, спадщиною та складом швидше пізніше полюю, як на погану каву. Я більше ніколи не використовую DTO. Колись. Плюс до того, я не вважаю подібні ЗНО порушенням DRY. Дві кінцеві точки, які повертають одне і те ж представлення, можуть повторно використовувати ці самі DTO. Дві кінцеві точки, які повертають подібні зображення, не повертають однакові DTO, тому я роблю конкретні DTO для кожного . Якби мене змусили обрати, склад з часом є менш проблематичним.
Лаїв

@Laiv це правильна відповідь на питання, просто не варто. Не впевнений, чому ви поставили це як коментар
TheCatWhisperer

2
@Laiv: Що ти використовуєш замість цього? На мій досвід, люди, які борються з цим, просто переосмислюють це. DTO - це просто контейнер для даних, і це все.
Роберт Харві

TheCatWhisperer, тому що мої аргументи будуть в основному засновані на думці. Я все ще намагаюся вирішити подібні проблеми у своїх проектах. @RobertHarvey правда, не знаю, чому я схильний бачити речі складніше, ніж вони є насправді. Я все ще працюю над рішенням. Я був цілком переконаний, що HAL - це модель, призначена для вирішення цих проблем, але прочитавши вашу відповідь, я зрозуміла, що я теж роблю свій DTO занадто грануляторів. Тому я спочатку застосую ваш підхід. Зміни будуть менш драматичними, ніж повністю перейти до HATEOAS.
Лаїв

Спробуйте це для хорошої дискусії, яка може вам допомогти: stackoverflow.com/questions/6297322/…
johnny

Відповіді:


10

Як найкраща практика, спробуйте зробити свої DTO максимально стислими. Поверніть лише те, що вам потрібно повернути. Користуйтеся лише тим, що вам потрібно використовувати. Якщо це означає кілька додаткових DTO, так і буде.

У вашому прикладі завдання містить користувача. Там, мабуть, не потрібен повний об’єкт користувача, можливо, лише ім'я користувача, якому призначено завдання. Інші властивості користувача нам не потрібні.

Скажімо, ви хочете перепризначити завдання іншому користувачеві, під час публікації про повторне призначення може виникнути спокуса передати повний об’єкт користувача та повний об'єкт завдання. Але, що насправді потрібно, це лише завдання Id та ідентифікатор користувача. Це єдині два фрагменти інформації, необхідні для повторного призначення завдання, тому моделюйте DTO як такий. Зазвичай це означає, що для кожного виклику відпочинку є окремий DTO, якщо ви прагнете до худорлявої моделі DTO.

Також іноді може знадобитися спадщина / склад. Скажімо, у одного є робота. У роботі є кілька завдань. У такому випадку отримання роботи також може повернути список завдань. Отже, немає правила проти складу. Це залежить від того, що моделюється.


Як можна коротше, це означає, що я повинен відтворити DTO, якщо вони просто відрізняються в деяких деталях - чи не так?
офіцер

1
@officer - Загалом, так.
Джон Рейнор

2

Якщо ваша система не базується виключно на CRUD-операціях, ваші DTO надто детальні. Спробуйте створити кінцеві точки, що втілюють бізнес-процеси або артефакти. Цей підхід чудово відображає рівень бізнес-логіки та проект "Ерік Еванс", орієнтований на домен ".

Наприклад, скажімо, у вас є кінцева точка, яка повертає дані для рахунків-фактур, щоб вони могли відображатися на екрані чи формі кінцевому користувачеві. У моделі CRUD вам буде потрібно кілька дзвінків до ваших кінцевих точок, щоб зібрати необхідну інформацію: ім'я, адресу виставлення рахунку, адресу доставки, позиції. У контексті бізнес-транзакцій один DTO з однієї кінцевої точки може повернути всю цю інформацію одразу.


Роберте, ти маєш на увазі під цим агрегатним коренем?
johnny

В даний час наші DTO розроблені для надання цілих даних для форми, наприклад, під час відображення списку todo, TodoListDTO має перелік TaskDTO. Але оскільки ми повторно використовуємо TaskDTO, кожен з них також має UserDTO. Отже, проблема полягає у великій кількості даних, яка запитується у бекенді та надсилається по дроту.
офіцер

Чи потрібні всі дані?
Роберт Харві

1

Важко встановити кращі практики для чогось такого "гнучкого" або абстрактного, як DTO. По суті, DTO - це лише об'єкти для передачі даних, але залежно від місця призначення або причини передачі, ви можете застосувати різні "найкращі практики".

Я рекомендую прочитати " Шаблони архітектури корпоративних додатків" Мартіна Фаулера . Існує ціла глава, присвячена моделям, де ДТО отримує дійсно докладний розділ.

Спочатку вони були "розроблені" для використання у дорогих віддалених дзвінках, де вам, швидше за все, знадобиться багато даних з різних частин вашої логіки; DTO здійснюватимуть передачу даних за один виклик.

За словами автора, DTO не передбачалося використовувати в місцевих середовищах, але деякі люди знайшли для них застосування. Зазвичай вони використовуються для збору інформації з різних POCO в єдине ціле для GUI, API або різних рівнів.

Тепер при успадкуванні повторне використання коду більше схоже на побічний ефект успадкування, а не на його основну мету; композиція, з іншого боку, реалізується з повторним використанням коду як основної мети.

Деякі рекомендують спільно використовувати склад і спадщину, використовуючи сильні сторони обох і намагаючись пом’якшити їх слабкі сторони. Далі є частиною мого розумового процесу при виборі або створенні нових DTO або будь-якого нового класу / об'єкта з цього приводу:

  • Я використовую успадкування з DTO всередині того ж шару або того ж контексту. DTO ніколи не успадковує POCO, BLL DTO ніколи не успадковує від DAL DTO тощо.
  • Якщо я виявлю, що намагаюся приховати поле від DTO, я рефактор і, можливо, використаю композицію замість цього.
  • Якщо дуже мало різних полів з базового DTO - це все, що мені потрібно, я вкладу їх у загальний DTO. Універсальні DTO використовуються лише всередині.
  • Базова POCO / DTO майже ніколи не буде використовуватися для будь-якої логіки, таким чином база відповідає лише потребам своїх дітей. Якщо мені коли-небудь потрібно використовувати базу, я уникаю додавати будь-яке нове поле, яке його діти ніколи не використовуватимуть.

Деякі з них можуть бути не «найкращими» методами, вони досить добре працюють над проектами, над якими я працював, але потрібно пам’ятати, що жоден розмір не підходить усім. У випадку з універсальним DTO ви повинні бути обережними, мої методи підписів виглядають так:

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

Якщо будь-який з методів коли-небудь потребує власного DTO, я роблю спадщину, і зазвичай єдине зміна, яке мені потрібно внести, це параметр, хоча іноді мені потрібно копати глибше для конкретних випадків.

З ваших коментарів я розумію, що ви використовуєте вкладені DTO. Якщо ваші вкладені DTO складаються лише зі списку інших DTO, я вважаю, що найкраще зробити це розгортати список.

Залежно від кількості даних, які потрібно відобразити або працювати з ними, може бути хорошою ідеєю створити нові DTO, які обмежують дані; Наприклад, якщо ваш UserDTO має багато полів, і вам потрібно лише 1 або 2, може бути кращим мати DTO лише з цими полями. Визначення шару, контексту, використання та корисності DTO дуже допоможе при його розробці.


Я не міг додати більше 2 посилань у своїй відповіді, ось додаткова інформація про місцеві DTO. Я знаю, що це дуже стара інформація, але я думаю, що деякі з них, можливо, все ще актуальні.
IvanGrasp

1

Використання композиції в DTO є прекрасною практикою.

Спадщина серед конкретних типів DTO є поганою практикою.

По-перше, у такій мові, як C #, властивість, що реалізується автоматично, має дуже невеликі витрати на технічне обслуговування, тому їх дублювання (я справді заперечую дублювання) не є таким шкідливим, як це часто.

Однією з причин не використовувати конкретні спадщини серед DTO є те, що певні інструменти з радістю відображають їх у неправильні типи.

Наприклад, якщо ви використовуєте утиліту бази даних на зразок Dapper (я не рекомендую її, але вона популярна), яка виконує class name -> table nameвисновок, ви можете легко зберегти похідний тип як конкретний базовий тип десь в ієрархії і тим самим втратити дані або гірше .

Більш глибока причина не використовувати успадкування серед DTO полягає в тому, що він не повинен використовуватися для обміну реалізаціями між типами, які не мають очевидних відносин "є". На мій погляд, TaskDetailце не звучить як підтип Task. Це так само легко може бути властивістю Taskабо, що ще гірше, може бути супертипом Task.

Тепер одне, що може вас турбувати, - це підтримка узгодженості між назвами та типами властивостей різних пов'язаних DTO.

Хоча успадкування конкретних типів, як наслідок, допоможе забезпечити таку узгодженість, набагато краще використовувати інтерфейси (або чисті віртуальні базові класи в C ++) для підтримання такої послідовності.

Розглянемо наступні ЗНО

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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.