Выносишь ли 5 DTO с одинаковыми полями в одно?

Ответ

Да, если DTO (Data Transfer Object) имеют полностью идентичную структуру и семантику, я выношу их в общий базовый класс или интерфейс. Это следует принципу DRY (Don't Repeat Yourself) и упрощает поддержку.

Пример на TypeScript для Node.js/Express:

// 1. Общий базовый интерфейс
interface BaseUserDto {
    email: string;
    firstName: string;
    lastName: string;
}

// 2. Специфичные DTO наследуют или расширяют базовый
interface UserCreateDto extends BaseUserDto {
    password: string; // Дополнительное поле только для создания
}

interface UserUpdateDto extends BaseUserDto {
    // Пока полей нет, но структура общая
}

interface UserResponseDto extends BaseUserDto {
    id: string;
    createdAt: Date;
}

// 3. Использование в контроллере Express
app.post('/users', (req: Request<{}, {}, UserCreateDto>, res: Response) => {
    // Валидация и обработка req.body
});

app.patch('/users/:id', (req: Request<{ id: string }, {}, UserUpdateDto>, res: Response) => {
    // Обновление пользователя
});

Когда НЕ стоит объединять:

  • Если DTO могут эволюционировать по-разному (например, в UserUpdateDto поле email станет опциональным, а в UserCreateDto — обязательным).
  • Если они используются в разных bounded context'ах (доменная модель разная). В таких случаях дублирование может быть менее вредным, чем неправильная абстракция.

Ответ 18+ 🔞

Да ты посмотри, какие у нас тут DTOшки развелись, как тараканы после ремонта! Ситуация, в общем-то, классическая: пишешь один, пишешь второй, а на третий уже подозрение ёбать чувствую — да это же одно и то же, ёпта!

Если у них структура и смысл полностью идентичные, то держать кучу копий — это ж чистой воды распиздяйство. Тут даже принцип есть такой, DRY называется — Don't Repeat Yourself, или, как я это понимаю, «не выёбывайся, не повторяй одно и то же».

Вот смотри, как это по-человечески делается на TypeScript'е. Просто, без наворотов.

// 1. Делаем общую основу, как фундамент для избушки
interface BaseUserDto {
    email: string;
    firstName: string;
    lastName: string;
}

// 2. А теперь все остальные DTO от этой основы пляшут
interface UserCreateDto extends BaseUserDto {
    password: string; // Вот, например, при создании пароль нужен, а так всё то же самое
}

interface UserUpdateDto extends BaseUserDto {
    // Пока тут пусто, но структура-то общая! И если что-то добавим — будет видно.
}

interface UserResponseDto extends BaseUserDto {
    id: string;
    createdAt: Date; // И клиенту отдаём немного больше инфы
}

// 3. Используем там, где надо
app.post('/users', (req: Request<{}, {}, UserCreateDto>, res: Response) => {
    // Работаем с req.body, там всё по контракту
});

app.patch('/users/:id', (req: Request<{ id: string }, {}, UserUpdateDto>, res: Response) => {
    // Апдейтим юзера
});

Всё, красота. Одна правка в BaseUserDto — и во всех наследниках автоматом. Удивление пиздец, как удобно!

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

  • Они могут меняться по-разному. Вот представь: завтра в UserUpdateDto поле email станет необязательным (типа, хочешь — поменяй почту, не хочешь — и так сойдёт), а в UserCreateDto оно обязательным и останется. Если они от одного предка — начинается пиздец, придётся костыли пилить. Лучше уж пусть будут отдельными, но честными.
  • Они из разных «вселенных» (bounded context). Если один DTO для админки, а другой для публичного API, и даже поля с одним названием email несут разный смысл — это манда с ушами получится, если их объединить. Тут дублирование — меньшее из зол, честно.

Короче, чувак, правило простое: если реально одно и то же — выноси. Если есть хоть тень сомнения, что они разойдутся — оставь как есть, терпения ноль ебать на будущие костыли. Голова должна думать, а не только клавиатуру обивать.