Как стать автором
Обновить

Зачем нужно внедрение зависимостей в JS

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров5.3K

Этот пост является ещё одной попыткой сформулировать идею, зачем нужно внедрение зависимостей в ванильном JavaScript (именно в ES6+, а не в TS).

Основная сложность в том, что шаблон “внедрение зависимостей” (DI) есть следствие применение на практике “принципа инверсии зависимостей” (DIP). Классическая формулировка этого принципа выглядит так:

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

  • B. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Для JS-программиста данная формулировка представляет определённую сложность в силу того, что в JS нет классических абстракций (в виде “интерфейсов” из других ЯП). В JS вообще нет абстракций, тут всё очень конкретно: вот объекты, вот примитивы - комбинируй.

Тем не менее, если спуститься с уровня теории на уровень практики, внедрение зависимостей вполне успешно может применяться даже в таком “конкретном” языке.

Наверное, самое главное, что нужно принимать во внимание, что DIP относится к архитектуре приложения. Резон использовать этот принцип появляется в тот момент, когда разработчики начинают задумываться не о том, как реализовывать бизнес-функции, а о том, как организовать код так, чтобы можно было продолжать реализовывать бизнес-функции с приемлемой скоростью.

Развитие DIP в практической плоскости во “взрослых” языках программирования (с интерфейсами, классами и прочим) привело к появлению такого архитектурного решения, как “инверсия управления” (IoC). Внедрение зависимостей - это один из методов реализации данного архитектурного решения (наряду с “Локатором служб”, шаблоном “Фабрика” и контекстным поиском). И вот на этом уровне всё становится уже несколько более понятным даже в JS.

Прямой код

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

globals

Например, мы можем считать, что данные объекты каким-то образом попадают в globals, и тогда js-код сервиса мог бы выглядеть так (./service.js):

const logger = self.logger;
const config = self.config;

export function service({name, count}) {
    const total = count * config.price;
    logger(`Product '${name}' is sold for ${total}$.`);
    return total;
}

А его вызов из HTML-кода:

<script>
    self.config = {price: 10};
    self.logger = (msg) => console.log(msg);
</script>
<script type="module">
    import {service} from './service.js';

    const total = service({name: 'Beer', count: 6});
</script>

Modules

Либо разместить логгер и конфигурацию во внешних es-модулях:

// ./logger.js
export default function (msg) {
    console.log(msg);
}
// ./config.js
export default {
    price: 15,
};

и подгружать эти модули в коде сервиса:

import config from './config.js';
import logger from './logger.js';

export function service({name, count}) {...}

В общем, разработчику сервиса нужно знать, какая зависимость где и в каком виде находится. Более того, при смене одного метода размещения зависимостей на другой (globals <=> modules) придётся изменять код всех сервисов, замкнутых на эти зависимости.

Инверсия

Инверсия контроля предполагает, что код нашего сервиса предоставляет возможность некоему внешнему управляющему добавить в него необходимые сервису зависимости. Где этот внешний код будет брать эти зависимости (из глобалов или модулей), наш сервис это не волнует:

export default function (logger, config) {
    return function ({name, count}) {
        const total = count * config.price;
        logger(`Product '${name}' is sold for ${total}$.`);
        return total;
    };
}

Первая функция является фабрикой, вторая - непосредственно сервисом. Внешний код использует фабрику для внедрения зависимостей в сервис и возвращает уже готовый, проинициализированный соответствующим образом сервис в том месте приложения, где этот сервис понадобился (опять же, разработчика сервиса не интересует, где именно).

В целом, приложение даже усложнилось (помимо объектов logger, config, service появился ещё какой-то “внешний управляющий” и фабричная функция). Но с точки зрения разработчика именно сервиса положение слегка упростилось - он задекларировал нужные ему зависимости в фабричной функции и ему уже не важно, в каком виде эти зависимости будут имплементированы и как подключены. Всё, что ему нужно знать - это имена зависимостей и их API.

Если использовать “классовый подход”, то фабричная функция замещается конструктором класса:

export default class Service {
    constructor(logger, config) {
        this.exec = function ({name, count}) {
            const total = count * config.price;
            logger(`Product '${name}' is sold for ${total}$.`);
            return total;
        };
    }
}

В таком виде внедрение зависимостей становится более похожа на классические варианты "внедрения зависимостей через конструктор" из других ЯП.

Резюме

Использование инверсии контроля в архитектуре приложения позволяет значимо уменьшить связность кода отдельных модулей (функций, классов), выводя проблемы нахождения и инициализации зависимостей на внешнюю, по отношению к коду, орбиту.

Это не значит, что приложение в целом стало проще - наоборот, оно стало сложнее. Но за счёт упрощения отдельного блока (модуля, функции, класса) появилась возможность строить более сложные приложения. Так, из стандартизованных кирпичей стало проще возводить бОльшие здания, чем из необработанного камня (такая себе аналогия, но тем не менее).

IoC (и DI) есть смысл применять там, где приложение состоит из множества модулей, которые распределены по множеству пакетов. В первую очередь - в nodejs-приложениях. В браузерах, где js-код зачастую внедряется в HTML-код страницы фрагментами, этот подход вряд ли будет оправдан, за исключением SPA/PWA - эти приложения по своей архитектуре уже приближаются к приложениям enterprise-уровня.

P.S. Я специально не описываю в посте деталей "внешнего управляющего", потому что суть IoC (и DI) - это "кирпичи", а не "строитель".

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии17

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань