Используйте состав и наследование для DTO

13

У нас есть веб-API ASP.NET, который предоставляет REST API для нашего одностраничного приложения. Мы используем 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 для нескольких конечных точек / методов, или должны быть разные DTO, которые отличаются только некоторыми нюансами?

сотрудник
источник
6
Я не могу сказать вам, что вы должны делать, но по моему опыту работы с DTO, наследование и составление раньше, чем позже охота на вас, как на плохой кофе. Я никогда не буду снова использовать DTO. Когда-либо. Плюс я не считаю подобные DTO сухим нарушением. Две конечные точки, которые возвращают одно и то же представление, могут повторно использовать одни и те же DTO. Две конечные точки, которые возвращают похожие представления, не возвращают одинаковые DTO, поэтому я создаю конкретные DTO для каждого . Если бы я был вынужден выбирать, композиция в долгосрочной перспективе будет менее проблематичной.
Laiv
@Laiv, это правильный ответ на вопрос, просто не надо. Не уверен, почему вы положили это в качестве комментария
TheCatWhisperer
2
@Laiv: Что вы используете вместо этого? По моему опыту, люди, которые борются с этим, просто переосмысливают это. DTO - это просто контейнер для данных, и это все.
Роберт Харви
TheCatWhisperer, потому что мои аргументы будут в основном основаны на мнении. Я все еще пытаюсь решить подобные проблемы в своих проектах. @RobertHarvey правда, не знаю, почему я склонен видеть вещи сильнее, чем они есть на самом деле. Я все еще работаю над решением. Я был совершенно уверен, что HAL - это модель, предназначенная для решения этих проблем, но, прочитав ваш ответ, я понял, что я тоже делаю DTO слишком детально. Поэтому я сначала применю ваш подход на практике. Изменения будут менее значительными, чем полный переход на HATEOAS.
Laiv
Попробуйте это для хорошей дискуссии , которые могли бы помочь вам: stackoverflow.com/questions/6297322/...
джонни

Ответы:

10

В качестве лучшей практики постарайтесь сделать свои DTO максимально лаконичными. Верните только то, что вам нужно вернуть. Используйте только то, что вам нужно. Если это означает несколько дополнительных DTO, пусть будет так.

В вашем примере задача содержит пользователя. Вероятно, там не нужен полный пользовательский объект, может быть, просто имя пользователя, которому назначена задача. Нам не нужны остальные пользовательские свойства.

Допустим, вы хотите переназначить задачу другому пользователю, у вас может возникнуть желание передать полный объект пользователя и полный объект задачи во время повторного назначения. Но что действительно нужно, так это просто идентификатор задачи и идентификатор пользователя. Это только две части информации, необходимые для переназначения задачи, поэтому смоделируйте DTO как таковое. Обычно это означает, что есть отдельный DTO для каждого повторного вызова, если вы стремитесь к худой модели DTO.

Также иногда может понадобиться наследование / композиция. Допустим, у кого-то есть работа. Работа имеет несколько задач. В этом случае получение работы также может вернуть список заданий для работы. Таким образом, нет никаких правил против композиции. Это зависит от того, что моделируется.

Джон Рейнор
источник
Как можно более кратко также означает, что я должен воссоздать DTO, если они просто отличаются по некоторым деталям - не так ли?
офицер
1
@officer - В общем, да.
Джон Рейнор
2

Если ваша система не основана исключительно на операциях CRUD, ваши DTO слишком детализированы. Попробуйте создать конечные точки, которые воплощают бизнес-процессы или артефакты. Этот подход хорошо соотносится со слоем бизнес-логики и с «доменным дизайном» Эрика Эванса.

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

Роберт Харви
источник
Роберт, ты имеешь в виду совокупный корень здесь?
джонни
В настоящее время наши DTO предназначены для предоставления полных данных для формы, например, при отображении списка задач, TodoListDTO имеет список TaskDTO. Но поскольку мы повторно используем TaskDTO, у каждого из них также есть UserDTO. Таким образом, проблема заключается в большом количестве данных, которые запрашиваются в бэкэнде и отправляются по проводам.
офицер
Требуются ли все данные?
Роберт Харви
1

Трудно найти лучшие практики для чего-то столь же «гибкого» или абстрактного, как DTO. По сути, 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 очень поможет при его разработке.

IvanGrasp
источник
Я не мог добавить более 2 ссылок в своем ответе, вот еще немного информации о местных DTO. Я знаю, что это очень старая информация, но я думаю, что некоторые из них, возможно, все еще актуальны.
ИванГрасп
1

Использование композиции в DTO - прекрасная практика.

Наследование среди конкретных типов DTO - плохая практика.

С одной стороны, в языке, подобном C #, автоматически реализуемое свойство имеет очень мало накладных расходов на сопровождение, поэтому их дублирование (я действительно ненавижу дублирование) не так вредно, как это часто бывает.

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

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

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

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

В то время как наследование конкретных типов, как следствие, поможет обеспечить такую ​​согласованность, гораздо лучше использовать интерфейсы (или чисто виртуальные базовые классы в C ++) для поддержания такой согласованности.

Рассмотрим следующие 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
}
Алуан Хаддад
источник