Три правила работы: Избегайте беспорядка и ищите простоту; От раздора найди гармонию; В центре трудностей кроется возможность ~ Альберт Эйнштейн

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

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

Но сначала…

Что такое внедрение зависимостей?

Согласно Википедии,

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

Бла-бла-бла… но что все это означает? Вот моя любимая ментальная модель:

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

Чтобы увидеть, что это означает в действии, давайте рассмотрим пример.

Пример для начала

Если вы хотите увидеть код для этой статьи и следовать ему, вы можете найти готовый пример в моем репозитории GitHub.

Чтобы получить максимальную отдачу от этой статьи, вы должны иметь хотя бы некоторое представление о:

  1. Обещания
  2. API XMLHttpRequest
  3. Модульное тестирование с помощью Mocha и Chai
  4. Замыкания в JavaScript

Предположим, мы пишем клиентский модуль и хотим написать основанную на Promise функцию, которая делает HTTP-запрос GET на предоставленный URL. Если у вас есть опыт работы с JavaScript, то вы, вероятно, знаете, что существует множество существующих библиотек, которые сделают это за вас, но этот пример послужит иллюстрацией концепции. Использование функции будет выглядеть примерно так:

Чтобы реализовать эту функцию, мы могли бы сначала сделать что-то вроде этого:

Однако в этой функции много чего происходит. Давайте попробуем провести рефакторинг, чтобы упростить рассуждение.

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

Вы могли заметить, что функция onHttpResponse зависит от переменных httpRequest, resolve и reject, но эти переменные не существуют в том месте, где функция обработчика событий теперь определена. Итак, как мы можем решить эту проблему? Используя внедрение зависимостей!

Внедрение зависимостей как стратегия рефакторинга

Поскольку функция onHttpResponse зависит от переменных httpRequest, resolve и reject, мы можем явно передать эти зависимости в работают как параметры, как показано ниже. Это базовый пример внедрения зависимостей.

Этот пример почти верен, но здесь все еще скрывается очень скрытая ошибка. Еще раз, прежде чем двигаться дальше, посмотрите, сможете ли вы его заметить:

Проблема здесь в том, что функция onHttpResponse является функцией обработчика событий. Это означает, что мы не контролируем, что передается в функцию в качестве аргументов при ее вызове. Обработчик событий всегда вызывается только с одним аргументом, а именно с объектом события. У нас есть дилемма: мы хотим иметь возможность передавать httpRequest, resolve и reject в функцию onHttpResponse в качестве параметров, но когда она вызывается , он будет вызываться только с объектом события.

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

Обратите внимание, как httpRequest, resolve и reject передаются в функцию onHttpResponse в строке 4, которая возвращает функцию обработчика событий, которая назначена свойству onload HTTP-запроса. Несмотря на то, что функция обработчика событий не получает явным образом resolve, reject, и httpRequest в качестве параметров, у нее по-прежнему есть доступ к этим переменным, поскольку они существовали на момент определения ( подробнее о закрытии, если этот момент сбивает с толку).

В качестве примечания, такого рода функция высшего порядка может быть очень лаконично написана с использованием синтаксиса стрелочной функции ES6:

Что мы уже сделали? Мы использовали внедрение зависимостей как инструмент, чтобы взять большую сложную функцию и преобразовать ее в более мелкие и более управляемые фрагменты. Однако оказывается, что мы также бесплатно получаем кое-что еще: эти функции теперь можно легко протестировать!

Внедрение зависимостей как стратегия тестирования

Теперь, когда функция onHttpResponse явно принимает httpRequest, resolve и reject в качестве параметров, теперь мы можем использовать библиотеку например, sinon.js, чтобы проверить эту функцию, передав поддельные версии тех параметров, которые называются шпионами. Такую стратегию передачи поддельных версий зависимостей для целей тестирования часто называют насмешкой.

При написании теста важно хорошо подумать о том, каким на самом деле должно быть поведение функции. Обработчик событий, созданный onHttpResponse, должен делать следующее:

  1. Если ответ возвращается с кодом состояния 200, он должен вызвать resolve и передать тело ответа.
  2. Если ответ возвращается с другим кодом состояния (например, 404), он должен вызвать reject и передать объект ошибки.

Вот как может выглядеть тест:

В этом примере следует обратить внимание на пару вещей:

  1. Перед каждым тестовым примером resolve и reject заменяются на spies, которые представляют собой функции, которые могут сообщать, были ли они вызваны. Они также могут сказать вам, с какими аргументами они были вызваны.
  2. В каждом тестовом случае литерал объекта передается в параметр httpRequest функции onHttpResponse, который заменяет реальный HTTP-запрос, который обычно передается. Это литерал объекта имеет код состояния и текст ответа жестко запрограммирован.
  3. Хотя функция onHttpResponse предназначена для работы как часть системы, которая выполняет HTTP-запросы, этот тест на самом деле не выполняет никаких внешних запросов. Это означает, что мы тестируем эту функцию в изоляции, что исключает возможность того, что наши тесты могут дать непредсказуемые результаты из-за внешних факторов.

Теперь, когда у нас есть работающая и протестированная функция onHttpResponse, давайте посмотрим на функцию get, которая и была сутью этого примера. Вот где мы были:

В нынешнем виде функцию get было бы довольно сложно протестировать, потому что она отправляет http-запрос на какой-то внешний сервер. Один из способов сделать это более тестируемым - использовать - как вы уже догадались - внедрение зависимостей! А именно, вместо того, чтобы разрешать функции создавать собственный объект XMLHttpRequest, мы можем передать его в качестве параметра функции:

Теперь, если мы хотим протестировать эту функцию, мы можем заменить HTTP-запрос поддельным, содержащим шпионов, аналогично тому, что мы делали ранее с resolve и reject. Это позволит нам протестировать функцию без необходимости полагаться на какой-либо внешний сервер. Наш тестовый файл теперь будет выглядеть так:

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

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

Очистка интерфейса

В этом примере есть много способов упростить интерфейс. Так или иначе, наша цель будет заключаться в том, чтобы функция get больше не требовала явной передачи объекта HTTP-запроса. Таким образом, использование функции должно быть таким же, как и раньше:

Но как мы можем добиться этого, не жертвуя своей способностью тестировать функцию? Один из способов - установить значение по умолчанию для параметра httpRequest, которое мы можем выбрать, чтобы переопределить, если мы хотим передать наш собственный поддельный HTTP-запрос:

Теперь, когда мы запускаем наши тесты, мы можем явно передать фальшивый HTTP-запрос, но когда мы используем функцию get в других частях системы, он по умолчанию создаст свой собственный реальный объект HTTP-запроса. .

Последние мысли

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

Также стоит отметить, что примеры внедрения зависимостей, которые я показал здесь, чрезвычайно просты по сравнению с некоторыми более реальными применениями этой техники. Тем не менее, я надеюсь, что этого было достаточно, чтобы вы познакомились с концепцией и стали практическими аргументами в пользу того, почему это эффективный принцип дизайна. Ознакомьтесь с частью 2, чтобы увидеть более реалистичные примеры!

Удачного кодирования!

Если вам понравилась эта статья или вы нашли ее полезной, нажмите на сердце ниже, чтобы порекомендовать ее другим, которым она тоже может понравиться!

Вам также может быть интересно узнать о:

Дэниел Кинг - профессиональный инженер-программист и преподаватель из Лос-Анджелеса, который занимается разработкой программного обеспечения и проводит инструктаж по контракту.

[email protected]