Чому ви насправді не використовуєте DI в Nest.js

DI достатньо потужний патерн для розробки гнучких сервісів. І багато хто хто починає читати документацію Nest.JS або довго працює з цим фреймоврком знає що в Nest.JS є DI. Але чому насправді ми частіше за все не використовуємо його, або робимо це не до кінця. Подивимось і статті


Почнемо з визначень. В нас є два терміна, скороченя яких є DI

  1. Dependency injection

  2. Dependency inversion

Перший позначає спосіб розв’язання залежностей за допомогою зовнішнього об'єкт, а от другий це доволі сильний патерн, один з принципив SOLID, який дозволяє будувати гнучкі сервіси.

І якщо по використанню першого питань немає. Його використовують всі і завжди як тільки починають писати на фреймворках по типу Nest.js або Angular, то про другий варто поговорити детальніше.

Що таке залежність одного модуля від іншого?

Якщо у вашому коді зустрічається шось подібне

class A {

  constructor() {
    const b = new B()
  }

}

То це означає що класс А залежить від класу В. Чому так? Бо для того щоб створити класс А нам потрібно мати все для того щоб створити класс Б. Щоб зрозуміти це подивимось на більш складний приклад

class Book {
  constructor(name: string) {
    this.name = name
  }
}

class Bookshelf {
  constructor(name: string) {
    const book = new Book(data)
  }
}

Цей приклад показує що для того щоб створити клас Bookshlef ми повинні передати в нього зайві данні які він не використовує тільки для того щоб він зміг створити класс Вook.

Як виглядає інверсія залежності?

Для того щоб інверсувати залежность ми створемо контракт. Який буде описувати який саме класс нам потрібне. І перекладемо відповідальність на того хто буде створювати наш клас. Він повинен буде розв’язати цю залежність. А ми в той час можемо продовжити працювати з інтерфесом

interface IBook {
  name: string;
  getName(): string;
}

class Book implements IBook {
  name: string;

  constructor(name: string) {
    this.name = name
  }

  getName(): string {
   return this.name; 
  }
}

class Bookshelf {
  
  constructor(book: IBook) {
    this.book = book;
  }
  
}

const book = new Book('Title')
const bookshelf = new Bookshelf(book)

Що ми бачемо у цьому прикладі? Ми створили інтерфейс який описує приклад об’єкта який буде використовувати наш клас. Інший клас імплементує цей інтерфейс, а потім вже передає готовий об’єкт нам у конструктор.

Чому це добре? Тому що ми не отримуємо зайві данні а також не залежемо від інших класів. А використовуємо тільки реалізації прийнятного для нас інтерфейсу. Зазвичай в компільованих язиках на додачу до цього ми зменшуємо частоту перезбору нашого модуля так як інтерфейси міняються набагато рідше, навідміну від їх реалізації. Але в JS нам це не страшно :)

Так а що не так з DI в Nest JS?

Давайте подивимось як виглядає звичайний інжект залежностей в якомусь з модулів Nest.js

class BookService {
  constructor(private readonly dataService: BookDataService){}
}

Що тут не так? Для того щоб отримати необхідний клас, ми кажемо несту названня конкретного класу. І він за допомогою механізма Dependency Injection інжектує нам конкретний клас який ми запросили. Але ми все ще залежимо від конкретного класу. Знов. Так ми не маємо передавати зайві данні для його створення(Цю проблему вирішив Dependency Injection). Але ми залежимо від конкретного класу. Замість того щоб залежати він інтерфейса. Ми залежемо від реального(класа), а не від абстрактного(інтерфейса). І якщо клас змінется то ми відчуємо проблеми. Також ми позбавляємо себе можливості використуовати інший корисний патерн з числа SOLID. Принцип підстановки Барбари Лісков. Тобто якщо ми захочемо замінити цей сервіс на такий самий за контрактом, але інший за реалізацією нам доведется імпортуівати сервіс у модуль, а потім міняти у всіх сервісах де викорустовувася старий.

Як ми можемо це виправити?

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

book.service.ts

@Injectable()
class BookService {
  constructor(@Inject('DataService') private readonly dataService: IDataService){}  
}

book.module.ts

@Module({
  imports: [{
    provide: 'DataService',
    useClass: BookDataService  
  }],
  provide: [],
  exports: []
})
class BookModule {}

За допомогою такого прийому ми можемо використовувати загально прийнятий інтерфейс у якості референса на функціонал. Але при цьому не прив’язуватись до певного класу і в будь який момент замінити його на інший.

Післямова

Частіше за все немає необхідності використовувати цей прийом. В невеликих проектах або в проектах з нескладною логікою варто використовувати прицип KISS і не переускладнювати собі роботу. Але це корисно знати якщо ви цікаветесь темою архітектури. Бо такий принцип часто можно зустріти в такий собі Clear Architecture яка розповсюджена зараз на C# або в Androin проектах. А от про те як натягнути цю архітектуру на JS backend або fronntend чекайте у наступних довгочитах(Можете навіть обрати про що б хотілось почути раніше про back чи front)

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Владислав Лєсной
Владислав Лєсной@unnsin

564Прочитань
4Автори
17Читачі
На Друкарні з 15 квітня

Більше від автора

  • Clear Architecture для бекенда на JS

    Кожен хто займається розробкою певний час приходить до слова архітектура. Сьогодні ми подивимось на одну з парадигм проектування архітектури через призму типового JS бекенду.

    Теми цього довгочиту:

    Clean Architecture
  • Паттерн “Репозиторій“ в JS

    В попередній статті ми розібрались з паттерном Специфікація Сьогодні ми поговоримо про логічне продовження паттерн репозиторій.

    Теми цього довгочиту:

    Js

Вам також сподобається

Коментарі (7)

через те що в nodejs немає інтерфейсів - це основний біль DI. Коли я пробував написати свій DI з декораторами, то зрозумів що найкраще рішення - використовувати абстрактні класи замість інтерфейсів. Я адепт того, щоб DI при реєстрації обʼєкту в свому контейнері - звіряв чи клас імплементує…

Вам також сподобається