Зарегистрируйтесь, чтобы продолжить обучение

Dependency Injection Container JS: Предметно-ориентированное проектирование

const repository = new UserRepository()
repository.save(user)

Каждый раз, когда в коде встречается подобная запись, мы уже можем сделать вывод, что полиморфизм включения обошёл этот код стороной. Подменить реализацию UserRepository не представляется возможным, ведь он прямо жестко закодирован в месте своего использования. Такая ситуация не всегда является проблемой, но, всё же, хотелось бы иметь возможность лёгкой подмены компонентов системы. Да и в тестах часто бывает нужно подменять реализации и использовать стабы.

В этой ситуации мы можем воспользоваться DIP (dependency inversion principle), то есть принципом инверсии зависимостей:

  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Хотя он и звучит страшно, на практике, особенно в динамических языках, применять его проще простого. Грубо говоря, всё сводится к тому, что мы передаём зависимости снаружи, а клиентский код ими пользуется. Например так:

const createUser = (userData, userRepository) => {
  const user = new User(userData)
  userRepository.save(user)
}

Как видите, теперь код не зависит от конкретной реализации репозитория. Так мы получаем преимущества от ООП. Только всегда будьте прагматиками. Инверсия ради инверсии, это так себе обоснование для усложнения. В реальности не так часто бывает нужна подмена, как об этом кричат в некоторых книжках, но всё же это важная тема.

Рядом с DIP всегда появляется словосочетание Dependency Injection или Внедрение Зависимостей — это набор способов, с помощью которых можно доставить зависимости. Кроме внедрения через параметры функции, выделяют следующие способы:

  • Через конструктор
  • Через сеттер

Кроме, собственно, полиморфизма, многим компонентам часто нужны готовые объекты, представляющие различные подсистемы программы. К таким компонентам могут относиться соединения с базой данных, доступы к кешам, любые компоненты с состоянием. Единственный способ получать к ним доступ без внедрения зависимостей — это использование глобальных переменных.

Многие действительно так и делают, более того, экосистемы некоторых языков подталкивают к таким подходам. Например, в Ruby очень часто объекты делаются глобальными переменными.

Из этой ситуации есть несколько хорошо изученных выходов.

Service Locator

Сервис-локатор (service locator) - это чуть более продвинутая альтернатива глобальным переменным. Этот паттерн подразумевает наличие одного глобального объекта, который и является сервис-локатором. В начале программы он инициализируется всеми нужными сервисами. В процессе жизни программы каждый компонент сам запрашивает у локатора нужные зависимости. Честно скажем, что этот подход так себе, но за неимением лучшего, может стать неплохим подспорьем.

import locator from './locator.js'

const sendEmail = (subject, body) => {
  const email = new Email(subject, body)
  locator.emailSender.send(email)
}

DI Container

Самый продвинутый вариант называется Dependency Injection Container. При таком подходе контейнер становится центральной частью системы. С одной стороны он предоставляет интерфейс для описания всех сервисов и их зависимостей, с другой стороны сам занимается созданием графа объектов, попутно внедряя зависимости в те места, где они нужны. Такой подход особенно распространён в таких языках как Java или C#. Возможно, вы даже слышали такое название, как Spring Framework.

Ниже представлен один из вариантов того, как могли бы выглядеть части системы, использующей контейнер:

class EmailService {
  constructor(sender) {
    this.sender = sender
  }

  sendEmail(subject, body) {
    const email = new Email(subject, body)
    this.sender.send(email)
  }
}

Как видите, класс не запрашивает никаких зависимостей сам, они внедряются через конструктор какой-то внешней системой.

bottlejs

bottlejs — это библиотека, которая позиционирует себя как DI Micro Container. В отличие от своих старших собратьев, она очень простая. С ее помощью достаточно легко собирать зависимости:

const bottle = new Bottle()
// Регистрация сервиса
bottle.service('emailSender', EmailSender)
bottle.service('emailService', 'EmailSender')

// Обращение к сервису
// Возвращает объект, а не класс
const { emailService } = bottle.container

emailService.sendEmail('title', 'boby')

За сценой Bottlejs выполняет создание объектов и инъекцию нужных зависимостей в соответствии с конфигурацией. Пример выше работает так:

const { emailService } = bottle.container

// Где-то внутри
const emailService = new EmailService(new EmailSender())

Такой способ работы подходит в простых случаях, когда зависимости это объекты без конфигурации. В более сложных случаях понадобится метод factory():

bottle.factory('emailSender', (container) => {
  const { emailSender } = container

  // Что-то делаем с emailSender
  // Добавляем любую логику

  return new EmailSender(emailSender)
})

Количество возможных сервисов и способов их сбора никак не ограничено. Bottlejs универсальный инструмент. Он может собирать все что угодно, любым нужным способом.


Дополнительные материалы

  1. Dependency injection micro container BottleJS

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff