[Разработка веб-сайтов, JavaScript, Node.JS, ООП, TypeScript] Внедрение зависимостей (dependency injection) через свойства-функции в JavaScript

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
19-Ноя-2020 16:32


Известный, но не очень популярный способ внедрения зависимостей. Попытка реализовать этот способ в популярных DI npm пакетах. Еще один свой DI.
Несколько слов об OOP и DI
Тему противопоставления ООП другим парадигмам хотел бы оставить в стороне. На мой взгляд в одном приложении вполне могут сочетаться разные парадигмы. Считаю ES классы большим шагом в сторону привлекательности js для использования ООП.
Небольшая история из личного опыта. В 2006 году был гораздо более популярен, чем сейчас — язык PERL. Он гибкий. Я в том году написал свою OO реализацию, и небольшое приложение, язык PERL это позволяет, пара мануалов 1, 2, и безграничные возможности.Потом вернулся к этой поделке через 2 месяца, понял что потратил приличное количество времени чтобы погрузиться в свою же ОО реализацию, да и она оказалась не такой уж хорошей, как я думал изначально. Еще через полгода снова пришлось вернуться, и понял что потратил еще больше времени чтобы понять свой же код. Плюс пришлось потратить время на поддержку этой ОО реализации. Для себя сделал вывод, что больше так делать не буду.
Программируя на JavaScript, я чувствовал, что будет такая же проблема. Вроде ООП хочется, но посмотришь вокруг сколько вариантов как это сделать с прототипной моделью, и все не стандарты.
TypeScript тогда не было, но и когда появился с первого раза у меня ничего не получилось, все на каком-то ровном месте пляски с бубном были (это конечно субъективно). Тогда не срослось.
У меня был внутренний настрой, чем меньше JS в проекте, тем лучше. Я использовал JQuery UI Widget Factory. Не идеально, но можно расширять и какой-никакой стандарт, и в целом достаточно быстро получалось. Сейчас ES6 classes после множества локальных реализаций классов на ES5 просто прорыв и возможность использовать ООП. И по появлению ES6 классов можно подумать и о новых реализациях DI.
Внедрение зависимостей (dependency injection) считаю важным инструментом парадигмы ООП. Все легко, когда мы хотим отнаследоваться от одного класса, и немножко изменить поведение под свой проект. Но если мы добавляем сложную библиотеку из нескольких классов, и в ней есть DI, то получаем гибкое приложение.
DI может избавить библиотеку от монструозности. Например, библиотека — календарик. Вариаций, как может быть нарисован календарь бесконечное количество (один/несколько месяцев, формат даты, времени, язык, стандарты...). Предусмотреть все возможные варианты как аргументы/параметры автору библиотеки просто невозможно. А если и захочет, то простенький календарик может превратиться в “монстрокалендарь”, который будут бояться использовать из-за его размеров. Но если будет возможность конечному клиенту легко чуточек допилить под себя или подключить плагин — календарик становится прекрасным! Вполне себе аргументы для использования DI.
В написании тестов DI может быть полностью самодостаточным инструментом — помощником.
По теме статьи
Один из вариантов реализации внедрения зависимостей — через свойство, причем в свойстве и передается зависимость. Javascript позволяет определять функции как переменные. А прототипная модель позволяет легко менять контексты у этих функций. В итоге можно реализовать внедрение зависимостей через функции, которые возвращают необходимые зависимости.
Проще всего пояснить примером.
Допустим есть класс App приложения,
класс Storage — какое то хранилище (один экземпляр на все приложение singleton/service),
и класс Date, для работы с датой (под каждую дату понадобится отдельный экземпляр).
Функции-свойству которая каждый след. вызов будет создавать новый объект (transient) добавим префикс “new”.
Функции-свойству всегда отдающую один и тот же объект (singleton) добавим префикс “one”.
class App {
    /** @type { function( int ):IDate } */
    newDate;
    /** @type { function(): IStorage } */
    oneStorage;
    construct( newDate, oneStorage ) {
        this.newDate = newDate;
        this.oneStorage = oneStorage;
    }
    main() {
        const oDate1 = this.newDate( 0 );
        const oDate2 = this.newDate( 1000 );
        const oStorage = this.oneStorage();
        oStorage.insert( oDate1.format() + ' - ' + oDate2.format() );
    }
}

Мне нравится такой подход, тем что он максимально универсален. Так можно внедрять все что угодно и по умолчанию отложено (lazy). Когда добавляется много вариантов из конкретных реализаций, как внедрять (например в inversify: to, toSelf, toConstantValue, toDynamicValue, toConstructor, toFactory, toFunction, toAutoFactory, toProvider, toService), вся концепция DI становится сложной на ровном месте. Поэтому если внедрять везде одинаково, то можно писать быстрее.
Часто конкретная реализация DI накладывает определенные требования на код самих компонент или сами компоненты становятся зависимы от системы внедрения зависимостей. Я попробовал этот пример оформить в популярных DI реализациях, найти максимально универсальный формат компоненты, заодно оформить их в некую сравнительную табличку. Ниже опишу мои впечатления от различных DI реализаций.
Разные трактовки назначения dependency injection
Прежде, чем привести табличку, хочу обратить внимание на то, что все библиотеки очень разные. И дополнительная разница появляется от разных трактовок назначения dependency injection. Я условно их разделил по своему видению:
  • Дать возможность писать тесты, не изменяя исходный код. Тестирование.
  • Уменьшить связность кода, оставляя в реализации компонента ключи/токены для обращения к другим компонентам. Удобство поддержки, повторное использование, тестирование.
  • Уменьшить связность кода, добавляя интерфейсы доступа к другим компонентам. Удобство поддержки, повторное использование, тестирование, безопасность, автокомплит/навигация IDE.

Мало где уделяют внимание на независимость компонент от DI. Но на мой взгляд у любой библиотеки появляется дополнительное преимущество, если ее компоненты могут работать с разными DI реализациями, а не тянут конкретную вместе с собой.
Во многих DI реализациях используются декораторы. Можно в библиотеке код компоненты оставить чистым, а декорировать в отдельном файле. Для клиента появляется вариант, либо импортировать чистый компонент, либо вместе с конкретным DI декоратором.
В целом, чем выше цифра, тем больше абстракций, больше гибкость, больше времени на разработку, ниже скорость исполнения кода. Поэтому говорить что, что-то лучше, или что-то хуже, неправильно. Есть разные инструменты для разных нужд. Выбор инструмента соответствующего задаче — настоящее “кунг-фу” )
Популярные dependency injection вспомогательные библиотеки javascript/typescript
Сделал небольшой парсер, разбирающий попадание сочетания “di” в npm. Пакетов по этой теме ~1400. Все рассмотреть невозможно. Рассмотрел в порядке уменьшения количества npm dependents.
repo
npm dependents
npm weekly downloads
github stars
возраст, лет
последняя правка, мес назад
lang
ES classes
interfaces
inject property
bundle size, KB
open github issues
github forks
inversify/Inversifyjs
1798
408k
6.6k
6
1
TS
+
+
+
63.3
204
458
typestack/typedi
353
62k
1.9k
5
3
TS
+
+
+
30.3
17
98
thlorenz/proxyquire
344
426k
2.6k
8
8
ES5
?
?
?
?
9
116
jeffijoe/awilix
244
42k
1.7k
5
1
TS
+
-
-
31.7
2
92
aurelia/dependency-injection
153
13k
156
6
2
TS
+
-
?
?
2
68
stampit-org/stampit
170
22k
3k
8
1
ES5
?
?
?
?
6
107
microsoft/tsyringe
149
80k
1.5k
3
1
TS
+
+
-
30.4
27
69
boblauer/mock-require
136
160k
441
6
1
ES5
?
?
?
?
4
29
mgechev/injection-js
105
236k
928
4
1
TS
+
-?
?
41.7
0
48
young-steveo/bottlejs
101
16k
1.2k
6
1
ES5 + D.TS
-?
-
-
13.3
2
63
jaredhanson/electrolyte
33
1k
569
7
1
ES5
-
-
-
?
25
65
zhang740/power-di
10
0.2k
65
4
1
TS
+
+
+
45.0
2
69
jpex-js/vue-inject
9
0.8k
174
4
12
ES5
-
-
?
?
3
14
zazoomauro/node-dependency-injection
5
1k
123
4
2
ES6 + D.TS
+
-?
+
291.0
3
17
justmoon/constitute
4
8k
132
5
60
ES6
+
-?
-
56.2
4
6
owja/ioc
1
2k
158
1
3
TS
+
+
+
11.3
4
5
kraut-dps/di-box
1
0k
0
0
1
ES6 + D.TS
+
+
+
11.1
0
0
Gitcompare ссылка
Codesandbox код реализации моего примера
https://github.com/inversify/InversifyJS
Наверное самый сложный, но и мощный пакет, возможно немного субъективно, потому что пример с ним делал самым первым. После него многие другие казались упрощенными версиями )).Наверное сложно придумать кейс, который бы не рассматривался авторами библиотеки. Монстр)
https://github.com/typestack/typedi
Чувствуется, что библиотека мощная, много разных возможностей. К сожалению, пока не смог разобраться, как я могу в App создать два разных экземпляра Date, с разными аргументами конструктора. Быть может здесь есть опытные его пользователи, которые подскажут?
https://github.com/thlorenz/proxyquire
Позволяет оставить код таким какой он есть, подменять содержимое файлов. В большей степени только для тестов. Сложно назвать DI, но для определенных задач может быть очень подходящим.
https://github.com/jeffijoe/awilix
Не получилось реализовать, возникает ошибка “Symbol(Symbol.toPrimitive)”, как я понял, из-за того что в основе библиотеки Proxy, а у меня один из сервисов наследник от нативного Date класса. Не увидел в примерах использования интерфейсов.
https://github.com/aurelia/dependency-injection
Судя по документации и примерам создана именно с основной целью целью иметь возможность разбивать классы на более мелкие. Является частью фреймворка Aurelia.
https://github.com/stampit-org/stampit
Необычная ОО реализация. Множественное наследование. Не пытался что-то делать.
https://github.com/microsoft/tsyringe
Я не фанат Microsoft, но объективно написать реализацию в их библиотеке у меня получилось быстрее всех остальных. Все умеет, специально выделили что инъекция свойства не реализована и никогда не будет реализована.
https://github.com/boblauer/mock-require
По задумке очень похожа на proxyquire.
https://github.com/mgechev/injection-js
Использовалась в Angular 4. Обширные возможности, конкретно мой пример реализовать не получилось, непонятно как в useFactory передать аргумент.
https://github.com/young-steveo/bottlejs
Мой пример сделать не получилось. Вроде подходит метод .instanceFactory, но как туда передать аргумент не понятно.
https://github.com/jaredhanson/electrolyte
Не пытался реализовать. Варианты с ES6 классами пока не реализованы автором.
https://github.com/zhang740/power-di
Много возможностей. Есть специальный код для использования вместе с React. Чрезвычайно маленькая документация. Чтобы разобраться как что-либо сделать приходится смотреть тесты пакета. Не без костылей, но реализовал свой пример.
https://github.com/jpex-js/vue-inject
Специфичный для vue без ES6 классов инструмент. Не рассматривал. В этом фреймворке есть и возможность ипспользовать ES6 classes, и есть функционал provide inject через который можно использовать DI. Библиотека кажется устаревшей.
https://github.com/zazoomauro/node-dependency-injection
Конфигурация зависимостей определяется отдельным YAML/JS/JSON файлом. Для сервера. Основана на концепции фреймворка на php symfony Мой пример сделать не получилось, думал через костыли и передачу класса в setParameter, но и там ограничение, невозможно использовать конструктор класса как параметр.
https://github.com/justmoon/constitute
Реализовал, но костылями, которые аннулируют все DI преимущества.
https://github.com/owja/ioc
Сначала показался новой версией inversify, облегченной и удобной. Возможно это авторешение циклических зависимостей, но кажется неочевидным: чтобы прописать зависимости у компонента, уже на входе надо иметь ссылку на инстанс контейнера, определяющего эти зависимости. По моему мнению с таким подходом связность увеличивается, а не уменьшается.
https://github.com/kraut-dps/di-box
Мой велосипед, подробнее ниже.
Я понимаю что мой пример получился замороченный, и есть много проектов без таких требований, но если DI реализация может больше, это большой плюс на будущее, ведь проекты развиваются.
Свой велосипед
Основан на прототипной “магии”, пример совсем без каких либо библиотек:
class Service {
    work () {
        console.log('work');
    }
}
class App {
    oneService;
    main () {
        this.oneService().work();
    }
}
// специальный es6 класс, выполняющий функции DI
class AppBox {
    Service;
    App;
    _oService;
    newApp () {
        const oApp = new this.App();
        // тут прототипная магия
        oApp.oneService = this.oneService.bind(this);
        return oApp;
    }
    oneService () {
        if (!this._oService) {
            this._oService = new this.Service();
        }
        return this._oService;
    }
}
const oBox = new AppBox();
oBox.Service = Service;
oBox.App = App;
const oApp = oBox.newApp();
oApp.main();

Класс Box можно представить как набор декораторов конструкторов со своим состоянием, хранящим конструкторы и синглтоны, .Непосредственно в библиотеке несколько инструментов чтобы создавать синглтоны (.one()), не писать bind(this), контролировать заполненность обязательных свойств. С библиотекой этот же пример выглядит так:
import {Box} from "di-box";
class Service {
    work() {
        console.log( 'work' );
    }
}
class App {
    oneService;
    main() {
        this.oneService().work();
    }
}
class AppBox extends Box {
    App;
    Service;
    newService() {
        return new this.Service();
    }
    oneService() {
        return this.one( this.newService );
    }
    newApp() {
        const oApp = new this.App();
        oApp.oneService = this.oneService;
        return oApp;
    }
}
const oBox = new AppBox();
oBox.Service = Service;
oBox.App = App;
const oApp = oBox.newApp();
oApp.main();

Пример в codesandbox
Контроль обязательных свойств такой:
const oBox = new AppBox();
// пропущено oBox.Service = Service;
oBox.App = App;
const oApp = oBox.newApp(); // то будет ошибка: свойство Service is undefined
oApp.main();

Конструкторы...
При написании компонентов для DI реализаций частенько приходится писать много аргументов в конструктор. И через какое то время, приходит мысль, что передача одного объекта со всеми зависимостями удобнее. Передача по ключу, удобнее чем по порядковому номеру.Сравните:
constructor( arg1, arg2, arg3 ) {}
// и
constructor( { arg1key: arg1, arg2key: arg2, arg3key: arg3 } ) {}

Но можно пойти еще дальше и попробовать отказаться от конструкторов, не во вред функциональности. Какие задачи у конструктора?
  • Выполнить какие-то операции инициализации.
  • Определить обязательные для работы компонента входные аргументы.

Первый пункт в ES таки подразумевает создание отдельного метода инициализации. Если этого не сделать, то достаточно сложно переопределить конструктор в наследнике из-за этой особенности. А DI изначально задуман для того чтобы сделать компонент более гибким.
Второй пункт можно решить организационным соглашениям. Например все публичные свойства не должны содержать undefined. Можно провести аналогию с абстрактными свойствами и методами из других языков. Как будто все публичные свойства абстрактны.
Сравните:
class A {
    _arg1;
    _arg2;
    constructor( arg1, arg2 = null ) {
        this._arg1 = arg1;
        this._arg2 = arg2;
    }
}
const instance = new A( 1, 2 );
// и
class A {
    arg1; // будет ошибка, если не установлено
    arg2 = null; // ошибки не будет null !== undefined
}
const instance = new A();
instance.arg1 = 1;
instance.arg2 = 2;

Если компонент создается в dependency injection реализации, то можно дополнительной проверкой это реализовать. Это поведение по умолчанию библиотеки внедрения зависимостей di-box.Но для классического подхода или для typescript с удобным синтаксисом типа constructor( public arg1: type, public arg2: type ) это поведение можно убрать опциями при создании Box:
new AppBox( { bNeedSelfCheck: false, sNeedCheckPrefix: null } );

В примере на codesandbox.
Итого с di-box получаем возможность писать в ООП стиле, с минимальным, но достаточным дополнительным кодом, реализующим DI. С одной стороны в реализации присутствует прототипная “магия”, но с другой она только на мета уровне, и сами компоненты могут быть чистыми, и ничего не знать об окружении.
Буду рад обсуждению, возможно упустил из виду какие-то другие библиотеки. Или может вы знаете как лучше реализовать мой пример в какой-то из реализаций DI. Напишите в комментариях.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_node.js, #_oop (ООП), #_typescript, #_dependency_injection, #_vnedrenie_zavisimostej (внедрение зависимостей), #_di, #_oop (ООП), #_javascript, #_ecmascript, #_es6, #_typescript, #_dibox, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_node.js, #_oop (
ООП
)
, #_typescript
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 26-Ноя 11:21
Часовой пояс: UTC + 5