JavaRush /Курсы /C# SELF /Проблема с классами для передачи данных (DTO)

Проблема с классами для передачи данных (DTO)

C# SELF
19 уровень , 0 лекция
Открыта

1. Знакомство с DTO

DTO (Data Transfer Object, «объект передачи данных») — термин родом из профессионального программирования, особенно из мира распределённых приложений и сервисов (например, когда один сервис вызывает другой через сеть). По сути, это просто объект — в большинстве случаев обычный класс C#, — чья единственная задача: содержать набор данных и безопасно передаваться между слоями программы или между приложениями.

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

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Классический DTO: только свойства, никакой логики

Всё! Никакой бизнес-логики, никаких сложных методов — только свойства.

Классы только для передачи данных

Представьте, что вы занялись доставкой пиццы. Ваш курьер (DTO) не должен уметь готовить пиццу, принимать заказы или чинить велосипед. Всё, что от него требуется — донести коробку из точки А в точку Б. Вот и класс-DTO: его задача — хранить простые данные. Он не должен уметь себя валидировать, он не должен знать, как правильно отображать себя в UI — только хранить.

  • DTO — это «люди без профессии» или «объекты без логики», просто носят данные.
  • Их используют, чтобы не засорять бизнес-логику деталями передачи данных: чем меньше чемодан, тем проще его нести.

2. Проблемы обычных классов-DTO на практике

Казалось бы, вот оно счастье: объявил класс с автосвойствами, и живи припеваючи! Но всё не так гладко.

Мутируемость данных: что может пойти не так

public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}
DTO с публичными set — легко случайно изменить данные

А теперь представим, что где-то в коде случайно поменяли свойство:

ProductDto dto = new ProductDto { Id = 1, Name = "Молоко" };
SomeApiProcess(dto);
dto.Name = "Кефир"; // Ой!

Из-за того, что все свойства имеют публичный set, легко случайно испортить объект или, хуже того, изменить его в одном месте, что неожиданно "поплывёт" по всей системе. Это становится настоящей проблемой, когда объекты используются многими модулями и потоками.

Копирование и сравнение — нескончаемая рутина

Допустим, вы хотите создать копию ProductDto, но поменять только имя.

var otherDto = new ProductDto { Id = dto.Id, Name = "Творог" };

Если свойств много, это превращается в скучнейший и потенциально баговый процесс копирования каждого поля вручную.

А если вам ещё и сравнивать надо объекты? Придётся переопределить Equals и GetHashCode, что часто делают кое-как или вовсе забывают.

Неизменяемость — не для "простых" классов

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

3. Как на практике это выглядит в большом проекте

Давайте рассмотрим пример из жизни.

Вы — разработчик в огромном интернет-магазине. Вам нужно передать информацию о заказе по сети. Для этого вы создаёте такой класс:

public class OrderDto
{
    public int Id { get; set; }
    public string Customer { get; set; }
    public double Total { get; set; }
}

Всё работает, пока не появляется пара сотен тысяч заказов в неделю. Куча микросервисов начинает гонять DTO-шки туда-сюда, кто-то где-то забывает, что Total должен считаться, а не задаваться руками, кто-то случайно меняет Customer после отправки… И вот у вас появляются баги, которые очень сложно отловить.

Как можно улучшить?

  • Использовать только get-свойства (без set).
  • Придумать специальный конструктор, чтобы установить значения только при создании.
  • Переопределить Equals и GetHashCode

Понимаете, к чему это ведёт? То, что должно было быть простым «чемоданом», внезапно превращается в мутного монстра.

4. Краткий разбор типичных проблем DTO-классов

В небольших проектах баги, вызванные изменением DTO-шки, можно "словить руками". В больших — это катастрофа. DTO используют для авторизации, передачи денег, обработки заказов, — любое нелегальное изменение данных может превратиться в денежные потери или (чёрт подери!) уголовное дело.

Ниже приведена "шпаргалка" проблем, с которыми сталкиваются почти все, кто пишет DTO на классах:

Проблема Описание
Мутируемость Данные легко менять где угодно — появляется риск багов (особенно в многопоточности)
Клонирование Копировать объекты неудобно, часто пишут руками
Сравнение По умолчанию сравниваются по ссылке, а не по значению
Поддержка with-копий Хотелось бы копировать с изменением части данных без ручного копирования
Неочевидная вложенность Вложенные DTO приходится копировать полностью, а это муторно

5. Пример: передача данных между слоями в нашем приложении

Допустим, у нас есть приложение-ежедневник, где мы можем хранить задачи. Раньше у нас был бы такой класс:

public class TaskDto
{
    public int Id { get; set; }
    public string Description { get; set; }
    public DateTime DueDate { get; set; }
}

Мы читали TaskDto из файла, потом добавляли его в коллекцию, потом выводили его на экран. Всё было хорошо... до тех пор, пока кто-то из другого модуля случайно не поменял DueDate прямо перед сериализацией — и у пользователя возникла путаница.

7. А можно ли лучше?

Да! И не только можно, но и нужно! Для решения этих проблем в C# появилась специальная конструкция — record. Она решает большую часть головной боли, связанной с классами-DTO, почти магическим образом.

Что делает record:

  • По умолчанию значение-ориентирован: сравнение объектов по содержимому, а не по ссылке.
  • Неизменяемость: можно прописать только get-свойства, значения задаются через конструктор.
  • Лёгкое клонирование: появилась конструкция with для копирования с изменением.
  • Автоматическая генерация методов для сравнения и вывода.
public record TaskDto(int Id, string Description, DateTime DueDate);
DTO на основе record: компактно, безопасно, удобно!

Круто? Очень! Но об этом — в следующей лекции.

2
Задача
C# SELF, 19 уровень, 0 лекция
Недоступна
Создание простейшего класса-DTO
Создание простейшего класса-DTO
2
Задача
C# SELF, 19 уровень, 0 лекция
Недоступна
Использование DTO для передачи данных между методами
Использование DTO для передачи данных между методами
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Александр Уровень 34
14 декабря 2025
а куда п.6 делся?)
Aleksey Trofimov Уровень 39
19 января 2026
мутировал в 7п.