← Назад к вопросам
Выносишь ли 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 должен иметь одну причину для изменения.
Вывод:
- Не выноси в общий класс просто так
- Используй интерфейсы/типы для семантики
- Каждый DTO может иметь свою логику (validate, transform, filter)
- Если 90% кода одинаков И логика одинакова — тогда выноси
- Иначе лучше иметь 5 явных, понятных DTO, чем 1 запутанный с множеством условий