Статьи

Сохранено из ада обратного вызова

Дьявол стоит над официантом, повсюду висят приемники

Эта статья была рецензирована Мэллори ван Ачтербергом , Дэном Принсом и Вилданом Софтиком . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

Хорошей новостью является то, что есть простые шаги, чтобы спастись от ада обратного вызова. Я чувствую, что устранение обратных вызовов в вашем коде похоже на ампутацию хорошей ноги. Функция обратного вызова является одним из столпов JavaScript и одной из его хороших частей. Когда вы заменяете обратные вызовы, вы часто просто меняете проблемы.

Друг говорит мне, что обратные вызовы — это уродливые бородавки и причина для изучения лучших языков. Ну, а обратные вызовы такие уродливые?

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

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

Что такое Callback Hell?

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

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

function receiver(fn) { return fn(); } function callback() { return 'foobar'; } var callbackResponse = receiver(callback); // callbackResponse == 'foobar' 

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

Проблема с обратными вызовами связана с наличием асинхронного кода, который зависит от другого обратного вызова. Я проиллюстрирую использование setTimeout для имитации асинхронных вызовов с функциями обратного вызова.

Не стесняйтесь следовать, репо на GitHub . Большинство фрагментов кода придут оттуда, чтобы вы могли подыграть.

Вот, пирамида гибели!

 setTimeout(function (name) { var catList = name + ','; setTimeout(function (name) { catList += name + ','; setTimeout(function (name) { catList += name + ','; setTimeout(function (name) { catList += name + ','; setTimeout(function (name) { catList += name; console.log(catList); }, 1, 'Lion'); }, 1, 'Snow Leopard'); }, 1, 'Lynx'); }, 1, 'Jaguar'); }, 1, 'Panther'); 

Глядя на вышесказанное, setTimeout получает функцию обратного вызова, которая выполняется через одну миллисекунду. Последний параметр просто передает обратный вызов с данными. Это похоже на Ajax-вызов, за исключением того, что возвращаемый параметр name приходит с сервера.

В этой предыдущей статье SitePoint есть хороший обзор функции setTimeout .

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

Анонимные функции

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

Использование анонимных функций в вашем коде не рекомендуется некоторыми стандартами программирования . function getCat(name){} лучше называть, поэтому function getCat(name){} вместо function (name){} . Ввод имен в функции добавляет ясности вашим программам. Эти анонимные функции легко набрать, но отправляют вас в ад на шоссе. Когда вы идете по этой извилистой дороге углублений, лучше всего остановиться и переосмыслить.

Один наивный подход к устранению этой путаницы обратных вызовов заключается в использовании объявлений функций:

 setTimeout(getPanther, 1, 'Panther'); var catList = ''; function getPanther(name) { catList = name + ','; setTimeout(getJaguar, 1, 'Jaguar'); } function getJaguar(name) { catList += name + ','; setTimeout(getLynx, 1, 'Lynx'); } function getLynx(name) { catList += name + ','; setTimeout(getSnowLeopard, 1, 'Snow Leopard'); } function getSnowLeopard(name) { catList += name + ','; setTimeout(getLion, 1, 'Lion'); } function getLion(name) { catList += name; console.log(catList); } 

Вы не найдете этот фрагмент в репозитории, но в этом коммите добавочное улучшение.

Каждая функция получает свое собственное объявление. Один плюс — мы больше не получаем ужасную пирамиду. Каждая функция изолируется и фокусируется лазером на своей конкретной задаче. У каждой функции теперь есть одна причина для изменения, поэтому это шаг в правильном направлении. Обратите внимание, что getPanther() , например, получает параметр. JavaScript не волнует, как вы создаете обратные вызовы. Но каковы недостатки?

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

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

Это запахи кода, унаследованные от ада обратного вызова. Иногда стремление войти в свободу обратного вызова требует упорства и внимания к деталям. Может начаться ощущение, что болезнь лучше лечения. Есть ли способ кодировать это лучше?

Инверсия зависимостей

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

Этот твердый принцип гласит :

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

Так что же означает этот кусок текста? Хорошей новостью является назначение обратного вызова для параметра, угадайте, что? Вы уже делаете это! По крайней мере частично, чтобы отделиться, думайте о обратных вызовах как о зависимостях. Эта зависимость становится контрактом. С этого момента вы делаете твердое программирование.

Один из способов получить свободу обратного вызова — создать контракт:

 fn(catList); 

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

Эта зависимость теперь может передаваться через параметр:

 function buildFerociousCats(list, returnValue, fn) { setTimeout(function asyncCall(data) { var catList = list === '' ? data : list + ',' + data; fn(catList); }, 1, returnValue); } 

Примечание. Выражение функции asyncCall попадает в область видимости замыкания buildFerociousCats . Этот метод является мощным в сочетании с обратными вызовами в асинхронном программировании. Контракт выполняется асинхронно и получает необходимые data со звуковым программированием. Контракт получает свободу, в которой он нуждается, поскольку он отделен от реализации. Прекрасный код использует гибкость JavaScript в своих интересах.

Все остальное, что должно произойти, становится самоочевидным. Можно сделать:

 buildFerociousCats('', 'Panther', getJaguar); function getJaguar(list) { buildFerociousCats(list, 'Jaguar', getLynx); } function getLynx(list) { buildFerociousCats(list, 'Lynx', getSnowLeopard); } function getSnowLeopard(list) { buildFerociousCats(list, 'Snow Leopard', getLion); } function getLion(list) { buildFerociousCats(list, 'Lion', printList); } function printList(list) { console.log(list); } 

Обратите внимание, что нет дублирования кода. Обратный вызов теперь отслеживает свое собственное состояние без глобальных переменных. Обратный вызов, например, getLion может быть getLion с любым, что следует за контрактом. Это любая абстракция, которая принимает список свирепых кошек в качестве параметра. Этот пример кода на GitHub .

Полиморфные обратные вызовы

Какого черта, давайте немного сойдем с ума. Что если я захочу изменить поведение с создания списка, разделенного запятыми, на список с разделителями? Одна проблема, которую я вижу, заключается в том, что buildFerociousCats приклеен к деталям реализации. Обратите внимание на использование list + ',' + data для этого.

Простой ответ — полиморфное поведение с обратными вызовами. Принцип остаётся: относись к обратным вызовам как к контракту и делай реализацию несущественной. Как только обратный вызов превращается в абстракцию, конкретные детали могут меняться по желанию.

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

Давайте определим контракт. Можно использовать list и параметры data в этом контракте:

 cat.delimiter(cat.list, data); 

Затем возьмите buildFerociousCats и сделайте несколько изменений:

 function buildFerociousCats(cat, returnValue, next) { setTimeout(function asyncCall(data) { var catList = cat.delimiter(cat.list, data); next({ list: catList, delimiter: cat.delimiter }); }, 1, returnValue); } 

Объект JavaScript JavaScript теперь инкапсулирует данные list и функцию- delimiter . next обратные вызовы связывают асинхронные обратные вызовы, ранее это называлось fn . Обратите внимание, что есть свобода группировать параметры по желанию с объектом JavaScript. Объект cat ожидает два конкретных ключа, и list и delimiter . Этот объект JavaScript теперь является частью контракта. Остальная часть кода остается прежней.

Чтобы запустить это, можно сделать:

 buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar); buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar); 

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

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

Эффективный модульный тест вокруг разделителя канала может выглядеть примерно так:

 describe('A pipe delimiter', function () { it('adds a pipe in the list', function () { var list = pipeDelimiter('Cat', 'Cat'); assert.equal(list, 'Cat|Cat'); }); }); 

Я позволю себе представить, как выглядят детали реализации. Не стесняйтесь проверить коммит на GitHub .

Вывод

Освоение обратных вызовов в JavaScript — это понимание всех мелочей. Я надеюсь, что вы видите тонкие изменения в функциях JavaScript. Функция обратного вызова становится неправильно понятой, когда не хватает основ. Как только функции JavaScript станут понятными, вскоре последуют принципы SOLID. Это требует сильного понимания основ, чтобы получить представление о программировании SOLID. Гибкость, присущая языку, ложится бременем ответственности на программиста.

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