Делаем свой JavaScript чистым
Оригинал статьи: Making your JavaScript Pure
Оглавление:- Чистые и нечистые функции
- Проблема с нечистыми функциями
- Преимущества чистых функций
- Какие функции надо делать чистыми
- Заключение
Как только размер вашего сайта или приложения, становится больше, чем несколько десятков строк, в нем неизбежно будут содержаться разного рода баги. Это свойственно не только JavaScript, а встречается практически во всех языках — очень сложно, если не невозможно, полностью исключить возможность появления каких-либо ошибок в вашем приложении. Однако это не означает, что при написании кода мы не можем принять меры, уменьшающие его уязвимость к ошибкам.
Чистые и нечистые функции
Чистая функция это функция, которая не зависит от наличия или изменения переменных вне своей зоны видимости. Это определение не слишком наглядно, поэтому сразу перейдем к практическим примерам кода.
Возьмем функцию, которая вычисляет, находится ли указатель мыши в левой половине страницы и в зависимости от этого выводит в консоль true
или false
. В реальности функция должна быть сложнее и делать больше, но и этот пример подходит для демонстрации:
function mouseOnLeftSide(mouseX) {
return mouseX < window.innerWidth / 2;
}
document.onmousemove = function(e) {
console.log(mouseOnLeftSide(e.pageX));
};
Функция mouseOnLeftSide()
принимает координату X и сравнивает ее с размером половины ширины окна — если она меньше, значит указатель мыши слева. Однако, mouseOnLeftSide()
не является чистой функцией. Мы знаем это потому, что внутри тела функции используется значение, которое не было ей явно передано:
return mouseX < window.innerWidth / 2;
Функции было передано значение mouseX
, но не window.innerWidth
. Это означает, что функция обращается к данным, которые ей не были переданы и поэтому она не является чистой.
Проблема с нечистыми функциями
Вы можете спросить, а в чем, собственно, проблема — этот кусок кода отлично работает и делает то, что от него ожидается. Представьте, что вы получили баг-репорт от пользователя, что при ширине окна меньше 500 пикселей, функция работает неверно. Как вы будете проверять это? У вас есть два варианта:
- Вы можете тестировать вручную, передвигая мышь в браузере, пока не найдете проблему.
- Вы можете написать юнит-тесты (для их написания есть неплохое введение от Ребекки Мерфи), которые не только отследят баг, но и предупредят его появление в будущем.
Чтобы проверить это сразу и избежать нового появления бага, мы выберем второй вариант и начнем писать тест. И тут мы встретимся с новой проблемой: как правильно настроить наш тест? Мы знаем, что надо настроить тест с шириной окна меньше 500 пикселей, но как? Функция зависит от значения window.innerWidth
и именно оно вызывает проблемы.
Преимущества чистых функций
Упрощение тестирования
Исходя из целей тестирования, представьте, что мы переписали тестируемую функцию следующим образом:
function mouseOnLeftSide(mouseX, windowWidth) {
return mouseX < windowWidth / 2;
}
document.onmousemove = function(e) {
console.log(mouseOnLeftSide(e.pageX, window.innerWidth));
};
Ключевое отличие этого варианта состоит в том, что mouseOnLeftSide()
теперь принимает два аргумента: координату X указателя мыши и ширину окна. Это значит, что mouseOnLeftSide()
теперь чистая функция: все необходимые ей данные явно передаются в качестве входных и она не пытается получить доступ к внешним данным.
С точки зрения функциональности она идентична предыдущему примеру, но при этом значительно улучшена ее поддерживаемость и тестируемость. Теперь нам не нужны хаки с подсовыванием в тесты фиктивного window.innerWidth
, вместо этого мы просто вызываем mouseOnLeftSide()
с нужными нам аргументами:
mouseOnLeftSide(5, 499) // ensure it works with width < 500
Самодокументирование
Кроме простоты в тестировании, у чистых функций есть и другие особенности, ради которых их стоит использовать при любой возможности. По своей природе они являются самодокументирующими. Если вы знаете, что функция не пытается получать доступ к внешним данным, значит, вы знаете, что все данные передаются в виде аргументов. Рассмотрим следующее объявление функции:
function mouseOnLeftSide(mouseX, windowWidth)
Вы знаете, что эта функция принимает два аргумента с данными и, если они правильно названы, вам сразу понятно, что они из себя представляют. Мы все сталкиваемся с болью пересмотра кода, который мы не трогали полгода и возможность быстро понять его снова является важным навыком.
Избегание глобальных переменных в функциях
Проблема глобальных переменных в JavaScript хорошо освещена — в языке изначально заложено глобальное хранение данных, доступных всем функциям. Это традиционный источник багов, так любая функция может изменить значения глобальной переменной, что, в свою очередь, может изменить поведение других функций.
Дополнительным свойством чистых функций является ссылочная прозрачность. Это сложный термин с простым смыслом: с одинаковыми входными данными мы получаем одинаковые выходные данные. Возвращаясь к mouseOnLeftSide
, взглянем на ее первый вариант:
function mouseOnLeftSide(mouseX) {
return mouseX < window.innerWidth / 2;
}
В этой функции нет ссылочной прозрачности. Я могу вызывать ее 5 раз с одним значением, изменить размер окна браузера и получить 5 разных результатов. Это немного надуманный пример, но с функциями, возвращающими разные значения при идентичных входных данных всегда сложнее работать — ведь вы не можете гарантировать их поведение. По той же причине их сложнее тестировать, так как у вас нет полного контроля над данными, передаваемыми в функцию.
С другой стороны, наша улучшенная функция mouseOnLeftSide
обладает ссылочной прозрачностью, так как все нужные данные подаются на вход и не запрашиваются из вне:
function mouseOnLeftSide(mouseX, windowWidth) {
return mouseX < windowWidth / 2;
}
Вы получаете ссылочную прозрачность просто за счет объявления всех данных в качестве входных, при этом исключаете возможность появления целого класса багов, связанных с побочными эффектами и функций с неожиданными действиями. Если у вас есть полный контроль над данными, то вы можете отлавливать и воспроизводить баги намного быстрее и надежнее, не трогая глобальные переменные, которые могут этому помешать.
Какие функции надо делать чистыми
Нельзя просто взять и сделать все функции чистыми — всегда будут ситуации, когда вам нужно будет извлечь внешние данные, самый общий пример это извлечение элемента из DOM для последующего взаимодействия. В JavaScript вы не можете обойтись без этого и в таких случаях вам не стоит переживать о чистоте. Вместо этого, подумайте, если у вас возможность структурировать код таким образом, чтобы нечистые функции были изолированы. Минимизируйте их эффекты для всей кодовой базы и старайтесь по возможности использовать чистые функции.
Рассмотрим пример кода, в котором мы выбираем элемент в DOM и меняем его фоновый цвет на красный:
function changeElementToRed() {
var foo = document.getElementById('foo');
foo.style.backgroundColor = "red";
}
changeElementToRed();
В этом фрагменте есть две проблемы, решаемые за счет переделки функции в чистую:
- Эта функция совершенно непригодна к повторному использованию, она привязана к конкретному элементу DOM. И мы не сможем применить ее к другому элементу.
- Эту функцию сложно тестировать по причине ее нечистоты. Для тестирования нам надо создавать элемент со специальным ID, вместо того, чтобы использовать обычный элемент.
С учетом этих двух моментов, я хотел бы переписать эту функцию следующим образом:
function changeElementToRed(elem) {
elem.style.backgroundColor = "red";
}
function changeFooToRed() {
var foo = document.getElementById('foo');
changeElementToRed(foo);
}
changeFooToRed();
Мы сделали функцию changeElementToRed()
не привязанной к конкретному элементу и более абстрактной. В то же время мы сделали ее чистой, получив все преимущества, о которых мы упоминали ранее.
Важно отметить, что у нас по-прежнему есть нечистый код — это функция changeFooToRed()
. Вы никогда не сможете этого избежать, но цель состоит в том, чтобы выявлять моменты, когда рефакторинг функции в чистую улучшает ее читаемость, используемость и тестируемость. Уменьшая места, где вы используете нечистые функции и создавая по максимуму чистые, вы избегаете многих неприятностей в будущем и просто пишите код лучшего качества.
Заключение
“Чистые функции”, “побочные эффекты”, “ссылочная прозрачность” это все термины, обычно ассоциируемые с чисто функциональными языками программирования, но это не значит, что мы не можем их применять и к JavaScript. Учет этих принципов и грамотное применение их на практике дают вам преимущество в виде более надежного, самодокументирующегося кода, с которым проще работать и который реже ломается. Я призываю вас учитывать это при написании или при рефакторинге существующего кода. Да, привыкание к этому займет некоторое время, но скоро вы сможете делать это, не задумываясь. А в будущем вы сами (и другие разработчики, которые будут работать с вашим кодом) оцените это по достоинству.