DI - dependency injectionIOC - inversion of control
Само явление внедрения зависимостей избавляет нас от потребности инстанциирования объекта внутри самого класса. Таким образом, мы можем внедрять разный функционал по заранее определённому интерфейсу.
Такой подход так же позволяет отделить программный компонент, чтобы его отдельно протестировать
Совершить самое простое внедрение зависимостей можно:
Через конструктор
Либо через метод
Composition root - это одна точка, в которой собираются все зависимости с приложения
Это схема простого DI, который мы можем повторить в любом языке
А это схема с типизируемыми языками, в которой можно определить конкретный интерфейс той зависимости, которую мы будем внедрять
Интерфейс выступает в данном случае неким контрактом, который определяет то, что должно присутствовать в классе
И вот тут можно увидеть пример, что наш интерфейс представляет из себя контракт того, что должно присутствовать в классе
logger.interface.ts
А тут мы по контракту реализуем методы, которые должны будут присутствовать в классе обязательно
logger.service.ts
И теперь в главном классе можно поменять входной инстанс не на сам класс логгера, а на интерфейс, которому должен удовлетворять передаваемый внутрь app логгер
app.ts
Первый принцип говорит о том, что все наши компоненты должны быть максимально изолированны друг от друга. Связываем все компоненты через абстракции и интерфейсы, которые предоставляют определённый контракт на функциональность, которая должна присутствовать в другом компоненте:
Мы создали в классе приложения переменную, которая принимает в себя интерфейс логгера, в котором должны присутствовать методы: log, error и warn - сам логгер мы не принимаем, но в нём должны быть методы, которые мы описали в интерфейсе.
Второй принцип говорит нам о том, что нам нельзя прокидывать в более мелкие модули (те же сервисы) модули более высокого уровня:
Нам нельзя прокидывать инстанс app внутрь того же exception, logger или router.
Конкретно мы имеем:
Принципы, которым мы должны следовать
Паттерн, по которому эти принципы можно реализовать
И сама реализация
При регистрации приложения наш класс А регистрируется в контейнере: мы понимаем, что он заинтсанциирован и что он удовлетворяет какому-либо интерфейсу
То же самое происходит с классом Б
Контейнер - это центральная точка управления, которая за нас создаёт наши классы, при этом он понимает, какие зависимости ему нужны, создаёт эти зависимости и передаёт их внутрь.
Таким образом мы избавляемся от самостоятельного создания Agregation Root - теперь он создаётся автоматически и его реализация от нас скрыта, что упрощает наш DI, уменьшая строчки нужного кода
Так же в контейнере обычно реализуется сервис локатор, в котором мы регистрируем все наши сервисы, а потом уже можем вытащить конкретный инстанс этого сервиса, чтобы его в дальнейшем использовать
Такой паттерн не стоит использовать в реальной работе - это просто схожесть с контейнером, который будет использоваться в данной системе - в нашем случае это полезно для тестов и запуска самого приложения
069 Декораторы
Всю информацию о декораторах можно найти тут: Декораторы
Сам декоратор представляет из себя обёртку, которая получает тот объект, над которым будут проводиться манипуляции и далее модифицирует поведение этого объекта
Декоратор класса. Он принимает в себя таргет - сам класс и позволяет выполнить какие-либо манипуляции над классом или же просто выполнить что-то вместе с классом.
И чтобы мы могли изменять свойства нашего класса, мы можем получить функцию и вернуть её же обратно, но модифицированную, как показано ниже в примере.
Тут мы поменяли внутреннее свойство класса на переданное в декоратор. Декоратор так же сейчас записывается не через @Component, а через @Component(значение)
Так же мы можем оборачивать наши объекты сразу в несколько декораторов. Будет идти порядок выполнения снизу вверх в случае классов.
В остальных случаях - сверху вниз
Декоратор методов. Так же мы можем вовсе переопределить логику работы методов внутри классов. Декоратор позволяет сохранить изначальную работу метода и просто её дополнить, либо переопределить вовсе.
Тут уже представлена работа всех декораторов, включая декораторы свойства и параметра метода
полный листинг
Это декоратор для свойства класса
А тут работа декоратора для аргумента метода
070 Metadata Reflection
Мы можем осуществлять хранение метаданных наших объектов с помощью библиотеки Reflect.
В ней присутствует метод defineMetadata(), которая позволяет присвоить определённый ключ с определённым значением к определённому объекту.
Так же методом getMetadata() мы можем по этому ключу получить метаинформацию по объекту
Так мы подключаем библиотеку метаданных
Так выглядит синтаксис создания и получения метаданных для определённого объекта
Вкупе с этой настройкой компилятора ТС, у нас будет переноситься проверка типов и в наш рантайм (будет работать после компиляции)
tsconfig.json
Ну и сейчас нужно объяснить связь декораторов и метаданных. Дело в том, что мы можем вызвать декораторы под определённый инстанс класса и делать DI с помощью вызова декоратора, который инджектит инстанс класса при получении конструктором другого класса. То есть идёт связывание классов друг с другом через метаданные, которые сохраняются при вызове декоратора, вызываемого под параметры конструктора или под класс.
071 Внедряем InversifyJS
В первую очередь, нужно установить inversify в проект
Тут будут храниться символы, по которым будет происходить связывание наших компонентов внутри контейнера
types.ts
Далее в основном файле приложения мы можем наконец создать контейнер, внутри которого будет происходить связывание класса с его идентификатором (в качестве которого выступает символ TYPES)
main.ts
Так же для реализации связывания, мы можем подставить в качестве дженерика не интерфейс, а сам класс, если мы подразумеваем, что фильтры будут идти отдельными компонентами.
Далее нам нужно произвести инжек всех зависимостей от основного приложения, чтобы они попали в контейнер и до них можно было достучаться во время использования контейнера инверсифая
logger.service.ts
Так же нужно произвести инжект всех зависимостей параметров, которые должны быть доступны внутри контейнера
exception.filter.ts
Так же нужно упомянуть, что имена параметров и классов не стоит повторять, так как они могут конфликтовать в контейнере
users.controller.ts
base.controller.ts
И в основном классе приложения нужно так же заинжектить все параметры и сам класс
app.ts
common > base.controller.ts
Далее, чтобы всё заработало, нужно добавить данный импорт во все файлы, в которых был использован inject из библиотеки inversify
И теперь можно убедиться, что наше приложение работает.
Итог:
В итоге мы заменили наш DI на ручной биндинг контроллера.
Основным плюсом такого подходя является то, что мы можем в любой момент времени получить любой инстанс объекта через get.
Если у нас где-то появится новый нджектэбл, то нам не придётся его прокидывать вручную. Нам нужно будет только один раз сделать bind контейнера и в том месте, где нужно будет подставить тип, мы сможем его подставить через @inject(TYPES.тип)
Такой подход позволяет нам легко менять сервисы - достаточно просто поменять его в .to(Сервис) и у нас всё будет работать.
072 Упражнение - Улучшаем DI
Далее наше приложение может сильно разрастись и количество биндингов будет выходить за рамки 30 и 40 штук. Для решения проблемы с большим количеством биндингов, которая возникает во время разработки, был придуман модуль контейнера
main.ts
Вот так выглядит обновлённая версия проекта с собранными биндингами. Так же мы можем экспортировать эти биндинги, чтобы не хранить их в одном месте, а распределить по проекту и удобно разделять их, чтобы в будущем можно было удобно различать их и модифицировать
main.ts
И далее нужно немного улучшить наш DI и реализовать интерфейс для контроллера пользователей. Он будет иметь логин и регистрацию.
users.controller.interface.ts
В самом контроллере нам нужно вместе с экстендом от базового модуля ещё и заимплементироваться от интерфейса. Чтобы можно было одновременно сделать и то, и то, нужно сначала произвести extends, а уже потом implements