DDD проти реальності: поширені пастки та їх вирішення у NestJS

NestJS

Domain-Driven Design (DDD) — потужний підхід до розробки програмного забезпечення, який допомагає побудувати системи, максимально наближені до реального бізнесу. Однак впровадження DDD на практиці може бути викликом, особливо в сучасних фреймворках, таких як NestJS. Незважаючи на гнучкість NestJS, розробники часто стикаються з проблемами під час впровадження DDD. У цій статті ми розглянемо поширені пастки під час використання DDD у NestJS і запропонуємо рішення для їх уникнення.

Що таке DDD і чому NestJS підходить для нього?

DDD — це підхід до розробки, в якому основний акцент робиться на бізнес-логіці та доменних моделях. Він включає:

  • Ubiquitous Language (універсальна мова): Єдина термінологія, зрозуміла як розробникам, так і бізнесу.
  • Доменні моделі: Фокус на бізнес-об’єктах і їх взаємодії.
  • Розподіл обов’язків: Чітке розділення логіки на доменні об’єкти, сервіси, репозиторії тощо.

NestJS — це фреймворк на базі Node.js, який використовує модульну архітектуру і добре інтегрується з DDD завдяки підтримці:

  1. Декораторів і модулів для створення структурованого коду.
  2. IoC (інверсії контролю) через вбудований DI (Dependency Injection).
  3. Чіткої розділеності відповідальності, яка відповідає принципам DDD.

Поширені пастки DDD у NestJS і способи їх уникнення

1. Надмірна складність модулів

Проблема:

Розробники часто створюють надто багато модулів або зосереджують всю логіку в одному модулі, що суперечить принципам DDD. Наприклад, об’єднання доменної логіки, інфраструктури та API-контролерів в одному місці ускладнює підтримку.

Рішення:

Організуйте ваш проєкт за принципом “модулі навколо домену”:

  • Розділіть логіку на три основні частини:
    1. Доменний рівень: Основні моделі, сервіси, доменні події.
    2. Інфраструктурний рівень: Репозиторії, інтеграція з базами даних.
    3. Інтерфейсний рівень: Контролери та 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 вимагає дисципліни, але з правильним підходом ви зможете створювати надійні додатки, які легко масштабуються та підтримуються. 🚀