DDD проти реальності: поширені пастки та їх вирішення у NestJS
Domain-Driven Design (DDD) — потужний підхід до розробки програмного забезпечення, який допомагає побудувати системи, максимально наближені до реального бізнесу. Однак впровадження DDD на практиці може бути викликом, особливо в сучасних фреймворках, таких як NestJS. Незважаючи на гнучкість NestJS, розробники часто стикаються з проблемами під час впровадження DDD. У цій статті ми розглянемо поширені пастки під час використання DDD у NestJS і запропонуємо рішення для їх уникнення.
Що таке DDD і чому NestJS підходить для нього?
DDD — це підхід до розробки, в якому основний акцент робиться на бізнес-логіці та доменних моделях. Він включає:
- Ubiquitous Language (універсальна мова): Єдина термінологія, зрозуміла як розробникам, так і бізнесу.
- Доменні моделі: Фокус на бізнес-об’єктах і їх взаємодії.
- Розподіл обов’язків: Чітке розділення логіки на доменні об’єкти, сервіси, репозиторії тощо.
NestJS — це фреймворк на базі Node.js, який використовує модульну архітектуру і добре інтегрується з DDD завдяки підтримці:
- Декораторів і модулів для створення структурованого коду.
- IoC (інверсії контролю) через вбудований DI (Dependency Injection).
- Чіткої розділеності відповідальності, яка відповідає принципам DDD.
Поширені пастки DDD у NestJS і способи їх уникнення
1. Надмірна складність модулів
Проблема:
Розробники часто створюють надто багато модулів або зосереджують всю логіку в одному модулі, що суперечить принципам DDD. Наприклад, об’єднання доменної логіки, інфраструктури та API-контролерів в одному місці ускладнює підтримку.
Рішення:
Організуйте ваш проєкт за принципом “модулі навколо домену”:
- Розділіть логіку на три основні частини:
- Доменний рівень: Основні моделі, сервіси, доменні події.
- Інфраструктурний рівень: Репозиторії, інтеграція з базами даних.
- Інтерфейсний рівень: Контролери та DTO.
Приклад структури:
/src
/domain
/user
user.entity.ts
user.service.ts
user.domain-events.ts
/infrastructure
/user
user.repository.ts
/application
/user
user.controller.ts
user.dto.ts
2. Змішування доменних моделей та DTO
Проблема:
Часто DTO (Data Transfer Object) використовуються як доменні моделі, що суперечить принципам DDD. DTO створені для передачі даних, тоді як доменні моделі мають вміщувати бізнес-логіку.
Рішення:
Розділяйте DTO та доменні моделі. Використовуйте мапери для перетворення між ними.
Приклад:
// User DTO
export class CreateUserDto {
name: string;
email: string;
}
// User Entity
export class User {
constructor(private name: string, private email: string) {}
isValidEmail(): boolean {
return /\S+@\S+\.\S+/.test(this.email);
}
}
// Мапер
export class UserMapper {
static fromDto(dto: CreateUserDto): User {
return new User(dto.name, dto.email);
}
}
3. Використання анемічних моделей
Проблема:
Доменні моделі часто обмежуються лише властивостями, а вся бізнес-логіка розподіляється між сервісами, перетворюючи моделі на “анемічні”.
Рішення:
Переносьте логіку, що належить до моделі, у саму модель.
Поганий приклад:
export class User {
name: string;
email: string;
}
// Логіка у сервісі
export class UserService {
isEmailValid(user: User): boolean {
return /\S+@\S+\.\S+/.test(user.email);
}
}
Кращий приклад:
export class User {
constructor(private name: string, private email: string) {}
isValidEmail(): boolean {
return /\S+@\S+\.\S+/.test(this.email);
}
}
4. Відсутність доменних подій
Проблема:
Події, які є важливими для бізнес-логіки (наприклад, створення нового користувача), часто ігноруються або реалізуються на рівні сервісів.
Рішення:
Використовуйте патерн “доменні події” для забезпечення реактивності вашого домену.
Приклад:
export class UserCreatedEvent {
constructor(public readonly userId: string) {}
}
export class User {
private domainEvents: UserCreatedEvent[] = [];
constructor(private id: string, private name: string) {}
create() {
this.domainEvents.push(new UserCreatedEvent(this.id));
}
getDomainEvents(): UserCreatedEvent[] {
return this.domainEvents;
}
}
5. Неправильна робота з репозиторіями
Проблема:
Репозиторії часто перевантажуються логікою, яка не належить до їхньої відповідальності, або працюють безпосередньо з DTO.
Рішення:
Репозиторії повинні лише взаємодіяти з інфраструктурою (бази даних) та повертати доменні моделі.
Приклад:
// Репозиторій
export class UserRepository {
constructor(private readonly db: DatabaseService) {}
async findById(userId: string): Promise<User> {
const userData = await this.db.query('SELECT * FROM users WHERE id = $1', [userId]);
return new User(userData.id, userData.name);
}
}
6. Ігнорування IoC (інверсії контролю)
Проблема:
Розробники часто створюють об’єкти вручну, не використовуючи вбудований механізм Dependency Injection у NestJS.
Рішення:
Використовуйте DI для управління залежностями.
Приклад:
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async getUser(id: string): Promise<User> {
return this.userRepository.findById(id);
}
}
7. Відсутність модульної архітектури
Проблема:
Великий кодбейс може швидко перетворитися на “спагетті”, якщо модулі не використовуються належним чином.
Рішення:
Організовуйте додаток на рівні модулів, де кожен модуль відповідає за конкретний домен.
Приклад:
@Module({
controllers: [UserController],
providers: [UserService, UserRepository],
})
export class UserModule {}
Висновок
DDD — це потужний інструмент для створення складних, масштабованих додатків. Використовуючи можливості NestJS, ви можете впровадити DDD у ваш проект, але важливо уникати поширених помилок:
- Розділяйте логіку на доменні, інфраструктурні та інтерфейсні рівні.
- Використовуйте DTO для передачі даних, а не як доменні моделі.
- Реалізовуйте доменні події для реактивної роботи з бізнес-логікою.
Впровадження DDD у NestJS вимагає дисципліни, але з правильним підходом ви зможете створювати надійні додатки, які легко масштабуються та підтримуються. 🚀