← Назад к вопросам

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

2.0 Middle🔥 81 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Это классический вопрос про рефакторинг и применение DRY принципа. Ответ зависит от контекста, но есть практические рекомендации.

Проблема

// Bad: дублирование
class CreateUserDTO {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
}

class UpdateUserDTO {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
}

class UserResponseDTO {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
}

class SearchUserDTO {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
}

class AdminUserDTO {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
}

Это нарушает DRY (Don't Repeat Yourself) и усложняет поддержку.

Подходы решения

1. Наивный подход — создать базовый DTO:

// Base DTO
class UserBaseDTO {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
}

// Наследование
class CreateUserDTO extends UserBaseDTO {}
class UpdateUserDTO extends UserBaseDTO {}
class UserResponseDTO extends UserBaseDTO {}

Минусы:

  • Нарушает принцип Liskov Substitution (LSP) — наследование должно для переиспользования, не всегда подходит
  • Если позже UpdateUserDTO получит дополнительные валидации, всё сломается
  • Классы не выражают intent (намерение) использования

2. Composition approach — используй интерфейсы:

// Базовый интерфейс
interface UserData {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
}

// Специфичные DTO
class CreateUserDTO implements UserData {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
  
  // Специфичная логика CreateUserDTO
  async validate() {
    // проверка email уникальности
  }
}

class UserResponseDTO implements UserData {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
  
  // Специфичная логика для response
  hide() {
    // скрыть sensitive данные
  }
}

class UpdateUserDTO implements UserData {
  firstName?: string;      // Опциональные для update
  lastName?: string;
  email?: string;
  phone?: string;
  age?: number;
}

Плюсы:

  • Явно выражает intent каждого DTO
  • Разные валидации и логика для разных операций
  • Flexibility — легко добавлять специфичные методы

3. Type-driven approach — используй типы:

// Единый тип для данных
type UserData = {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
};

// DTO классы с типизацией
class CreateUserDTO {
  constructor(public data: UserData) {}
  
  validate() {
    if (!this.data.email.includes('@')) {
      throw new Error('Invalid email');
    }
  }
}

class UpdateUserDTO {
  constructor(public data: Partial<UserData>) {}  // Опциональные поля
  
  toEntity() {
    return new User(this.data);
  }
}

class UserResponseDTO {
  constructor(private user: User) {}
  
  toJSON() {
    return {
      firstName: this.user.firstName,
      lastName: this.user.lastName,
      email: this.user.email,
      phone: this.user.phone,
      age: this.user.age
    };
  }
}

Рекомендация: когда выносить, а когда нет

Выноси в базовый интерфейс/тип ЕСЛИ:

  • Поля действительно идентичны во всех DTO
  • Нет разных валидаций для разных операций
  • DTO используются в разных контекстах одинаково
// Хорошо
interface UserProfile {
  firstName: string;
  lastName: string;
  email: string;
}

class CreateUserDTO implements UserProfile { ... }
class UserResponseDTO implements UserProfile { ... }

НЕ выноси ЕСЛИ:

  • У DTO разные валидации
  • Разные поля опциональны/обязательны
  • Разная бизнес-логика преобразования
  • Разные требования к безопасности (что показывать)
// Плохо — слишком много разности
class CreateUserDTO extends UserBaseDTO {
  @IsEmail()
  email: string;  // Must be valid email
}

class UpdateUserDTO extends UserBaseDTO {
  email?: string;  // Optional
}

class UserResponseDTO extends UserBaseDTO {
  password?: undefined;  // Never show
}

Лучший подход для Node.js

// 1. Определи базовый тип/интерфейс
type UserFields = {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  age: number;
};

// 2. Создавай специфичные DTO классы
class CreateUserDTO {
  constructor(
    public firstName: string,
    public lastName: string,
    public email: string,
    public phone: string,
    public age: number
  ) {}
  
  validate() {
    if (!this.email.match(/@/)) throw new Error('Invalid email');
    if (this.age < 18) throw new Error('Too young');
  }
}

class UpdateUserDTO {
  constructor(public data: Partial<UserFields>) {}
  
  hasChanges(): boolean {
    return Object.keys(this.data).length > 0;
  }
}

class UserResponseDTO {
  constructor(private user: User) {}
  
  toJSON() {
    return {
      firstName: this.user.firstName,
      lastName: this.user.lastName,
      email: this.user.email,
      phone: this.user.phone,
      age: this.user.age,
      createdAt: this.user.createdAt  // Additional fields in response
    };
  }
}

// 3. Используй в контроллере
class UserController {
  async create(req: Request) {
    const dto = new CreateUserDTO(
      req.body.firstName,
      req.body.lastName,
      req.body.email,
      req.body.phone,
      req.body.age
    );
    
    dto.validate();
    const user = await this.userService.create(dto);
    return new UserResponseDTO(user).toJSON();
  }
  
  async update(req: Request) {
    const dto = new UpdateUserDTO(req.body);
    if (!dto.hasChanges()) {
      return {error: 'No changes'};
    }
    const user = await this.userService.update(dto);
    return new UserResponseDTO(user).toJSON();
  }
}

Правило SOLID

Single Responsibility Principle:

  • CreateUserDTO — отвечает за валидацию входящих данных
  • UserResponseDTO — отвечает за форматирование ответа
  • UpdateUserDTO — отвечает за частичное обновление

Каждый DTO должен иметь одну причину для изменения.

Вывод:

  1. Не выноси в общий класс просто так
  2. Используй интерфейсы/типы для семантики
  3. Каждый DTO может иметь свою логику (validate, transform, filter)
  4. Если 90% кода одинаков И логика одинакова — тогда выноси
  5. Иначе лучше иметь 5 явных, понятных DTO, чем 1 запутанный с множеством условий