DI достатньо потужний патерн для розробки гнучких сервісів. І багато хто хто починає читати документацію Nest.JS або довго працює з цим фреймоврком знає що в Nest.JS є DI. Але чому насправді ми частіше за все не використовуємо його, або робимо це не до кінця. Подивимось і статті
Почнемо з визначень. В нас є два терміна, скороченя яких є DI
Dependency injection
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)