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 универсальный инструмент. Он может собирать все что угодно, любым нужным способом.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.