Dependency injection і javascript

Це мої роздуми, які не претендують на правду в передостанній інстанції. Розраховано на людей які добре знають javascript, читали про solid, знають, що таке юніт тести і є нормальними штріхами.
Окей окей, це зовсім не нова тема. Ще ангуляр (той що angularJs) додав в javascript’ове комнюіті словосполучення dependecy injection. В чому ж суть цього допису? Суть в знаходженні цієї тоненької межі між “навіщо й так добре” і “не уявляю без цього”. Почнемо з уроку історії.
Давним-давно в одній далекій галактиці…

Все почалося не з сингулярності (раптово!), а з інфляції (гривні!) і рандомного утворення нашого всесвіту (не хочу вас розчаровувати, але ніякої мети вашого існування немає). Зразу після великого вибуху появився javascript. Очевидно, що це намагались максимально засекретити, але правду не сховаєш. Ще у 50ті історики знаходили описи як древні єгиптяни дебагали примхливий фараон експлорер (пращур іе) написаний на papirus.js.
На дворі 1995рік… Що ж, з самого початку ціль була зрозуміла — давайте зробимо мову, яка буде клеєм між інтерфейсними компонентами. Максимально просто і максимально ефективно (на той момент) реагувати на дії користувача.

Ніхто не очікував і, відповідно, не планував такого росту популярності. Мову з самого початку задумували як динамічну, щоб дизайнери не парились, яка різниця між float64 і uint32.
Час йшов, а веб-застосунки (“аплікації” були в садочку!) набирали популярності. Вже настільки складні веб-застосунки, що навіть jquery не справлявся.

Настав 2010 рік і на світ народився angular. Це було настільки круто, що навіть круто — не настільки круто. Нагадаю, es modules ще не було. Як же ми будемо використовувати код з інших файликів? На той момент існувало декілька варіантів: AMD, CommonJS і UMD. А ангулар девелопери ж не прості, це ж гуглъ мать вашу. І вони такі — давайте зробимо свій“імпорт” + прокачаєм його + ми ж шаримо як девелопати. І бац — dependency injection.
І грубо кажучи, ніхто толком, окрім ангулярщиків, DI не використовував (і не використовує). Так чому ж?
Що таке DI?
Почнемо з того що таке DI. Є така гарна, журналістська абревіатура SOLID. Була вона побудована на незкінченних граблях, на які наступили незкінченні девелопери. Так ось, літера I означає — Islam. Це релігія, суть якої в інверсії контролю. Це означає, що не ти керуєш своїм життям, а на все воля алаха, відповдіно — алах керує тобою.

Inversion of control — це такий принцип, при якому будучи на верхньому рівні, ти не маєш знати як реалізується нижчий рівень. Відповідно, якщо ти вирішив створити екзмепляр якогось дуже модного класу, ти не маєш паритись, що йому портібно для того, щоб створитись. Уяви, що ти розробляєш клас Scheduller і в середині хочеш створити екземпляр класу PaskaManager (взагалі старайтесь не робити “менеджерів”, просто це смішно звучить), але в конструкторі PaskaManager вказано — дай мені Logger (…і ще може бути RamadanPlanner, PerfomanceMeasurer і тд і тп). А кожний з цих компонентів (ганг оф фор казали “prefer composition over inheritance!”) має ще свої залежності. Як жеж нам створити цей сраний PaskaManager? Напряг.
class Logger {
constructor(somethingImportant: VeryImportant) {
...
}
}class PaskaManager {
private loggerService: Logger; constructor(loggerService: Logger) {
this.loggerService = loggerService;
}
}class Scheduller {
private paskaManager: PaskaManager;
constructor() {
const loggerDependecy: VeryImportant = new VeryImportant();
const logger: Logger = new Logger(loggerDependecy);
this.paskaManager = new PaskaManager(logger);
this.paskaManager.getJesusResurrected();
}
}
Ми звісно пішли як куля в лоб і усюди постворювати через конструктор усі залежності і сам PaskaManager. Але тепер клас Scheduller знає купу зайвої інфи. У чому ж проблема? В тому, що якщо ми поміняємо щось у процесі створення Logger, нам доведеться міняти код класу Scheduller. Абсолютне гівно.

Дідько, що ж робити? Правильно — делегувати. Давай заберемо з себе повноваження створювати залежності нашого компонента. Хай компонент сам вирішує, що йому треба, а саме створення буде деінде! Ого, чекай чекай, як це назвати… Інвер… інверсі… інверсія аккорда!

I for Inversion of Control
Отже, ми поміняли напрям контролю — тепер не батьківський елемент знає як створюється підопічний, а сам підопічний елемент знає все, що йому потрібно для створення. Прекрасно. Не даремно на це виділили цілу літеру з абревіатури!
Слова словами, а принципи принципами. Як це реалізувати? Одним з механізмів принципу інверсії контролю є — dependency injection. Батьківський клас далі не знає, що потрібно, щоб створити підопічний клас. Але, він його не може створити через конструктор new(). Для того, щоб додати новий компонент в наш клас Scheduller, ми його “впихуємо” (inject). “Впихнути” можна декількома шляхами, давайте зосередимось на впихання через конструктор, для простоти.
class Logger {
@inject()
constructor(somethingImportant: VeryImportant) {
...
}
}class PaskaManager {
private loggerService: Logger; @inject()
constructor(loggerService: Logger) {
this.loggerService = loggerService;
}
}class Scheduller {
private paskaManager: PaskaManager;
// Уявімо, що ми використовуємо якийсь класний IoC (про це згодом) за допомогою декораторів.@inject()
constructor(paskaManager: PaskaManager) {
this.paskaManager = paskaManager;
this.paskaManager.getJesusResurrected();
}
Як ми бачимо, все що ми вказуємо — це список компонент, які нам потрібно впихнути (за щоку) у конструктор і нам магічним чином прилітає вже створений компонент.
Так де ж вони створюються? І хто їх впихає? IoC container. Він взяв на себе обов’язки реєстрації усіх наших компонентів і впихання їх у потрібний момент. А чи не порушує IoC контейнер SRP (сінглє респонсібіліті прінсіпл) знаючи так багато про інші компоненти? Ні. Допоки його імплементація непрозора і ціль одна — менеджати залежності. Давайте приблизно накидаємо як би він міг виглядати.
class FancyIoC {
// ClassDefinition буде якась структура на основі Function
private bindings: Map<string, ClassDefinition>; // прив'язуємо назву з конструктора до конкретного класу
public bind = (name: string, class: ClassDefinition): void => {
bindings.set(name, class); // result ('PaskaManager',
PaskaManagerClassDefinition);
}// наш декоратор, який мав би спрацьовувати перед конструктором
public inject = (name: string): void => {
const classDef = bindings.get(name);
this.createComponent(classDef);
// магічна функція, яка створює "дерево залежностей" і починаючи
// знизу (з тих хто вже не має залежностей) створює їх по черзі
// догори. Нова компонента відповідно теж створить свої залежності
// через цей інджект.
// Це вже нас не дуже турбує, уявімо, що воно класно працює.
// https://github.com/inversify/InversifyJS/tree/master/src
// почитай сорси як тут пацани реалізували.
}
}class App {
public registerComponents = () => {
FancyIoC.bind('paskaManager', PaskaManager);
FancyIoC.bind('loggerService', Logger);
FancyIoC.bind('somethingImportant', VeryImportant);
}
}
Що ж, з DI розібрались. Так чому ж він такий непопулярний серед javascript’ерів? На це є декілька причин, але на мою думку найважливіша одна — розмір.
Весь цей час ми обходились без DI, бо розміри застосунків були не такими великими. З розміром росте складність, зі складністю потреба в кращому контролю якості, а кращий контроль якості потребує більше засобів для тестування. Саме розмір/складність і є цією межею коли DI починає приносити плоди.
Важлива частина
Динамічна природа js дає нам можливість використовувати такі штуки як rewire і sinon. І на перший погляд, DI виглядає зайвим. Але es6 import’и імпортують конкретну реалізацію, а не абстракцію. Відповідно mocking/stubbing можуть відкрити нам забагато реалізації конкретного об’єкта, яка зробить наші компоненти зав’язаними — coupled.
Якщо повернемось до абревіатури SOLID, де літера О означає open/closed principle, то імпортуванням реалізації, ми його тільки що порушили. Ми завжди маєм залежати від абстракцій, а не від реалізацій. Чому? Тому що нас не цікавить як реалізований той чи інший об’єкт, нас цікавить тільки його абстракція, з якою ми і хочемо працювати. Відповідно як би не змінювались внутрощі об’єкта, його “клієнтів” не потрібно змінювати. Це дозволяє нам бути гнучкими і не боятись змін.
Але навіщо мені цей SOLID, якщо і так все працює? І будете мати рацію задавши це питання.

Це просто набір принципів якими можна керуватись у об’єктно орієнтованому програмуванні. Це не срібна кулявлоб і більшість принципів потрібно підганяти під реалії javascript’а. Тут ще варто декілька разів подумати, що ви хочете від свого коду і як його зробити насамперед ефективним.
Межа
Що ж, для себе я так визначаю цю межу:
- величина/складність компонент/мікросервісів
- кількість компонент/мікросервісів в домейні
- важилвість компоненти/мікросервісу
- наскільки важливий кастомний контроль над стовренням (реалізація пулу)
- наскільки часто буде мінятись
- наскільки просто застабати/замокати
- рівень тесту: юніт, сервіс чи інтеграційний тест
- кількість тестів
Головне пам’ятайте — кіп іт стюпід, стюпід! Подякував.