Проектування, орієнтоване на домен (DDD) з NestJS: Посібник для розробників

pic

Проектування, орієнтоване на домен

Як розробники, ми часто стикаємося з необхідністю балансувати між виконанням бізнес-вимог і написанням підтримуваного коду. Проектування, орієнтоване на домен (DDD), пропонує спосіб узгодження наших технічних рішень з основною бізнес-логікою. У поєднанні з NestJS, прогресивним фреймворком для Node.js, DDD може призвести до більш структурованого, масштабованого та зрозумілого коду. У цьому посібнику я поділюсь своїм досвідом застосування принципів DDD у NestJS з практичними прикладами для початківців.

Шукаєте надійний завантажувач відео з YouTube? Ознайомтесь із https://www.utilshub.com/youtube-downloader для швидкого та простого завантаження ваших улюблених відео!

Що таке проектування, орієнтоване на домен (DDD)?

DDD — це філософія, яка зосереджена на основному домені та його логіці. Головною метою є створення спільної мови між розробниками та бізнес-експертами, щоб гарантувати, що програмне забезпечення справді відповідає потребам бізнесу. Це моделювання вашого домену з точністю та використання цієї моделі для розробки вашого програмного забезпечення.

Чому варто використовувати DDD з NestJS?

Модульна архітектура NestJS, ін’єкція залежностей та декоратори роблять його чудовим кандидатом для DDD. Вона дозволяє чисто розділяти турботи та інкапсулювати бізнес-логіку таким чином, що це і підтримувано, і масштабовано. У своїх проєктах я помітив, що поєднання DDD з NestJS допомагає управляти складністю і покращує читабельність коду.

Основні принципи DDD

Давайте швидко повторимо основні концепції DDD:

  1. Універсальна мова: Спільна мова, яку використовують як команда розробників, так і експерти з домену, щоб всі були на одній хвилі.
  2. Сутності (Entities): Це об'єкти з унікальним ідентифікатором, які представляють такі речі, як Користувач або Замовлення у вашій системі.
  3. Об'єкти значень (Value Objects): Невідомі об'єкти, які описують якусь сторону домену, наприклад, Гроші або Адреса.
  4. Агрегати (Aggregates): Це групи об'єктів, які розглядаються як єдине ціле, при цьому один об'єкт є коренем.
  5. Репозиторії (Repositories): Інтерфейси для доступу до коренів агрегатів із сховища даних.
  6. Події домену (Domain Events): Події, які позначають важливі зміни в домені і можуть ініціювати інші процеси.

Налаштування NestJS для DDD

Встановлення NestJS

Спочатку потрібно встановити Node.js. Потім створіть новий проєкт NestJS:

npm i -g @nestjs/cli  
nest new my-ddd-project  
cd my-ddd-project

Це дає вам базовий додаток NestJS. Тепер давайте налаштуємо структуру, щоб вона відповідала принципам DDD.

Структура проєкту для DDD

Ось як я зазвичай організовую свої проєкти NestJS, застосовуючи DDD:

src/  
 cart/  
 domain/  
 entities/  
 value-objects/  
 events/  
 application/  
 services/  
 dto/  
 infrastructure/  
 repositories/  
 cart.module.ts

Ця структура дозволяє зберігати логіку домену окремо від прикладних та інфраструктурних шарів, що є важливим для DDD.

Реалізація DDD в NestJS

Визначення моделі домену

Давайте розглянемо визначення нашого домену. У цьому прикладі ми побудуємо простий додаток для електронної комерції.

Сутності (Entities)

// src/cart/domain/entities/product.entity.ts  
export class Product {  
 constructor(  
 public readonly id: string,  
 public name: string,  
 public price: Money,  
 ) {}  
}

Об'єкти значення (Value Objects)

// src/cart/domain/value-objects/money.vo.ts  
export class Money {  
 constructor(  
 public readonly amount: number,  
 public readonly currency: string,  
 ) {}  

 add(other: Money): Money {  
 if (this.currency !== other.currency) {  
 throw new Error('Currency mismatch');  
 }  
 return new Money(this.amount + other.amount, this.currency);  
 }  
}

Реалізація репозиторіїв

Визначення моделі домену

Давайте поглибимося у визначення нашого домену.

У цьому прикладі ми побудуємо простий додаток для електронної комерції.

Сутності

// src/cart/domain/entities/product.entity.ts  
export class Product {  
 constructor(  
 public readonly id: string,  
 public name: string,  
 public price: Money,  
 ) {}  
}

Об'єкти значення

// src/cart/domain/value-objects/money.vo.ts  
export class Money {  
 constructor(  
 public readonly amount: number,  
 public readonly currency: string,  
 ) {}  

 add(other: Money): Money {  
 if (this.currency !== other.currency) {  
 throw new Error('Несумісність валют');  
 }  
 return new Money(this.amount + other.amount, this.currency);  
 }  
}

Реалізація репозиторіїв

Репозиторії діють як міст між доменом і шаром даних. Ось простий приклад:

// src/cart/domain/repositories/cart.repository.ts  
import { Cart } from '../entities/cart.entity';  

export interface CartRepository {  
 save(cart: Cart): Promise;  
 findById(id: string): Promise;  
}

Створення сервісів застосунку

Сервіси застосунку оркеструють логіку домену. Ось базовий CartService:

// src/cart/application/services/cart.service.ts  
import { CartRepository } from '../../domain/repositories/cart.repository';  
import { Cart } from '../../domain/entities/cart.entity';  

export class CartService {  
 constructor(private readonly cartRepository: CartRepository) {}  

 async addProduct(cartId: string, product: Product): Promise {  
 const cart = await this.cartRepository.findById(cartId);  
 if (!cart) {  
 throw new Error('Кошик не знайдений');  
 }  
 cart.addProduct(product);  
 await this.cartRepository.save(cart);  
 }  
}

Обробка подій домену

Події домену — це чудовий спосіб розділити частини системи. Наприклад:

// src/cart/domain/events/product-added.event.ts  
export class ProductAddedEvent {  
 constructor(  
 public readonly cartId: string,  
 public readonly productId: string,  
 ) {}  
}

Створення додатку для електронної комерції

Для більш повного прикладу давайте описати функціональність Cart:

  • Сутності: Product, CartItem, Cart
  • Об'єкти значення: Money, ProductQuantity
  • Репозиторії: CartRepository
  • Сервіси: CartService
  • Події домену: ProductAddedToCartEvent, ProductRemovedFromCartEvent

Сутності та об'єкти значення

// src/cart/domain/entities/cart-item.entity.ts  
import { Product } from './product.entity';  
import { ProductQuantity } from '../value-objects/product-quantity.vo';  

export class CartItem {  
 constructor(  
 public readonly product: Product,  
 public readonly quantity: ProductQuantity,  
 ) {}  
}  

// src/cart/domain/entities/cart.entity.ts  
import { CartItem } from './cart-item.entity';  

export class Cart {  
 private items: CartItem[] = [];  

 addProduct(product: Product, quantity: ProductQuantity): void {  
 const existingItem = this.items.find(item => item.product.id === product.id);  
 if (existingItem) {  
 existingItem.quantity = existingItem.quantity.add(quantity);  
 } else {  
 this.items.push(new CartItem(product, quantity));  
 }  
 }  
}

Репозиторії

// src/cart/infrastructure/repositories/in-memory-cart.repository.ts  
import { CartRepository } from '../../domain/repositories/cart.repository';  
import { Cart } from '../../domain/entities/cart.entity';  

export class InMemoryCartRepository implements CartRepository {  
 private carts: Cart[] = [];  

 async save(cart: Cart): Promise {  
 const index = this.carts.findIndex(c => c.id === cart.id);  
 if (index !== -1) {  
 this.carts[index] = cart;  
 } else {  
 this.carts.push(cart);  
 }  
 }  

 async findById(id: string): Promise {  
 return this.carts.find(cart => cart.id === id) || null;  
 }  
}

Сервіси застосунку використовують репозиторії та публікують події домену.

Це дозволяє іншим частинам системи реагувати на зміни.

Тестування доменної логіки

Юніт-тести є важливими для того, щоб переконатися, що доменна логіка працює як очікується. Ось простий тест для сутності Cart:

import { Cart } from './cart.entity';  
import { Product } from './product.entity';  
import { Money } from '../value-objects/money.vo';  
import { ProductQuantity } from '../value-objects/product-quantity.vo';  

describe('Cart', () => {  
 it('should add a product to the cart', () => {  
 const cart = new Cart('cart1');  
 const product = new Product('product1', 'Product 1', new Money(100, 'USD'));  
 cart.addProduct(product, new ProductQuantity(1));  

 expect(cart.items.length).toBe(1);  
 });  
});

Інтеграція з модулями NestJS

Нарешті, ми повинні інтегрувати наші DDD шари в модулі NestJS:

// src/cart/cart.module.ts  
import { Module } from '@nestjs/common';  
import { CartService } from './application/services/cart.service';  
import { InMemoryCartRepository } from './infrastructure/repositories/in-memory-cart.repository';  

@Module({  
 providers: [  
 { provide: 'CartRepository', useClass: InMemoryCartRepository },  
 CartService,  
 ],  
})  
export class CartModule {}

Переваги DDD з NestJS

  • Відповідність бізнесу: Код точно відображає бізнес-домен.
  • Масштабованість: Чітке розмежування полегшує масштабування.
  • Підтримуваність: Розділення відповідальностей спрощує обслуговування та відлагодження.

Загальні пастки та поради

1. Надмірне ускладнення моделі домену

  • Пастка: Легко перенавантажити модель домену, намагаючись представити кожну деталь бізнес-логіки, що призводить до зайвої складності.
  • Порада: Починайте з простого та розвивайте вашу модель. Зосередьтеся на основних аспектах домену, які приносять найбільшу цінність. Використовуйте рефакторинг, коли з'являються нові уявлення про домен.

2. Ігнорування загальної мови

  • Пастка: Відсутність встановлення та використання загальної мови може створити розрив між командою розробників і експертами з домену, що призводить до непорозумінь і невідповідностей між кодом та бізнес-потребами.
  • Порада: Регулярно співпрацюйте з експертами з домену. Використовуйте ту саму мову у вашому коді, документації та обговореннях, щоб забезпечити узгодженість.

3. Погане розділення обов'язків

  • Пастка: Змішування доменної логіки з додатком або інфраструктурними обов'язками може призвести до заплутаного коду, який важко підтримувати та масштабувати.
  • Порада: Чітко розмежовуйте ваш домен, додаток та інфраструктуру. Використовуйте інтерфейси для абстракції репозиторіїв та сервісів, дозволяючи вашій доменній моделі залишатися незалежною від того, як зберігаються або отримуються дані.

4. Ігнорування подій домену

  • Пастка: Невикористання подій домену може призвести до тісно зв'язаних компонентів, що ускладнює адаптацію до змінних бізнес-потреб.
  • Порада: Використовуйте події домену для комунікації важливих змін у вашому домені. Це розв'язує частини системи і дозволяє додавати нові функції без зміни існуючого коду.

5. Недостатнє тестування доменної логіки

  • Пастка: Занадто велика залежність від інтеграційних тестів і недостатнє тестування юніт-тестів для доменної логіки може зробити тести повільними та крихкими.
  • Порада: Зосередьтеся на юніт-тестуванні ваших доменних сутностей та об'єктів значення. Переконайтеся, що кожна частина доменної логіки тестується в ізоляції, що допомагає виявити помилки на ранніх етапах і знижує складність інтеграційних тестів.

6. Ігнорування продуктивності

  • Пастка: Ігнорування впливу складних доменних моделей на продуктивність може призвести до неефективних запитів і операцій.
  • Порада: Знайдіть баланс між багатою моделлю домену та практичними міркуваннями продуктивності.

Використовуйте кешування, лінивий завантаження або інші техніки оптимізації продуктивності за необхідності, але завжди профілюйте і тестуйте, щоб переконатися, що ваші оптимізації ефективні.

Заключні думки

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

У моїй практиці прийняття DDD змінило мій підхід до розробки програмного забезпечення. Це дозволило мені створювати системи, які не лише технічно надійні, а й відповідають справжній суті домену. Якщо ви розглядаєте DDD для вашого наступного проекту, NestJS — це чудовий вибір, який допоможе вам ефективно втілити ці принципи в життя.

Перекладено з: Domain-Driven Design (DDD) with NestJS: A Developer’s Guide

Leave a Reply

Your email address will not be published. Required fields are marked *