Друкарня від WE.UA

Чому ви насправді не використовуєте 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)

Статті про вітчизняний бізнес та цікавих людей:

  • Як побудована програма Meest China Academy

    Курс про товарний бізнес охоплює різні етапи роботи: від пошуку ідеї до перевірки товару, логістики та масштабування. Програма Meest China Academy містить 17 модулів, які послідовно розкривають ці теми без зведення всього навчання до однієї універсальної поради.

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

    Meest
  • Які технології використані в Айфон 17

    Айфон 17 поєднує OLED-дисплей із частотою до 120 Гц, чип A19, дві камери Fusion 48 Мп і швидке заряджання через USB-C. У COMFY можна купити Айфон 17 з накопичувачем на 256 або 512 ГБ, вибравши конфігурацію відповідно до обсягу фото, відео та застосунків

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

    Iphone 17
  • Як вибрати вживаний iPhone: коротке керівництво для розумної покупки

    Вторинний ринок смартфонів Apple активно зростає, і питання, як вибрати вживаний iPhone, стає актуальним для дедалі більшої кількості користувачів. Правильний підхід дозволяє отримати бу iPhone з актуальною iOS і доброю камерою за помітно нижчу ціну, ніж у нових пристроїв.

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

    Iphone
  • Як подолати жіночу безплідність за допомогою донорських яйцеклітин

    Діагноз "безпліддя" рідко звучить як вирок одразу. Для багатьох жінок саме в цей момент вперше звучить словосполучення "донорські яйцеклітини" — і це часто не кінець шляху до материнства, а якраз новий, реальний шанс.

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

    Донорство
  • Як заспокоїти зубний біль і чому самолікування може призвести до операції?

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

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

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

5Довгочити
796Перегляди
17Підписники
На Друкарні з 15 квітня 2023

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

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

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

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

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

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

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

    Js

Це також може зацікавити:

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

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

Це також може зацікавити: