Прощай, плохой код: вот как не лажать в JavaScript

Перевод
6
6153
Добавить в избранное

На JavaScript легко писать работающие решения, но легко и совершать ошибки. Рассказываем и показываем, как стать лучше в JavaScript.

Прощай, плохой код: вот как не лажать в JavaScript

JavaScript – это сила, с которой приходится считаться. Он является самым широко-используемым языком программирования в мире. Благодаря своей простоте и изобилию источников обучения он доступен для начинающих. Большой кадровый резерв привлекает к JavaScript компании всех размеров. Большая экосистема инструментов и библиотек повышают продуктивность разработчиков. Использование единого языка для фронтенда и бэкенда является огромным преимуществом, можно использовать одинаковый набор навыков во всём стеке.

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

На заметку начинающим

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

Оснастка

Прощай, плохой код: вот как не лажать в JavaScript

Одно из достоинств JavaScript – обилие инструментов. Воспользуемся этим преимуществом на примере ESLint.

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

Многие едва используют ESLint: они просто включают предустановленный конфиг типа eslint-config-airbnb и думают, что на этом можно закончить. Такой подход не раскрывает возможностей ESLint. В JavaScript нет ограничений. Неадекватная настройка линтинга имеет далеко идущие последствия.

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

Настройка ESLint

Решили воспользоваться советами из этой статьи? Тогда настройте ESLint следующим образом. Познакомьтесь со всеми подсказками поочерёдно и включайте правила в свой проект одно за другим. Настройте их изначально как warn, позже вы сможете конвертировать некоторые правила в error.

Запустите в корне вашего проекта:

Там же создайте файл .eslintrc.yml:

Используете VSCode? Установите плагин ESLint.

Вы можете запускать ESLint вручную в командной строке:

Самый большой источник сложности

Прощай, плохой код. Здравствуй, улучшенный JavaScript!

Возможно, это прозвучит странно, но код сам по себе – самый большой источник сложности. По факту, лучший способ писать безопасный и стабильный софт – это не писать его. К сожалению или к счастью, это не всегда возможно. Поэтому второй лучший способ – снизить количество кода. Меньше кода – меньше сложность, вот так легко! Малое количество кода уменьшает вероятность возникновения багов. Говорят, что джуниор пишет код, а сеньор удаляет 🙂

Длинные функции

Давайте рассмотрим следующий отрывок кода express.js, который обновляет запись в блоге:

Тело функции длиной в 38 строк выполняет несколько действий: считывает id публикации, находит существующий пост в блоге, проверяет пользовательский ввод, возвращает ошибки в случае неверного ввода, обновляет коллекцию постов и возвращает обновлённые посты.

Определённо можно провести рефакторинг на несколько малых функций. Конечный обработчик маршрута будет выглядеть так:

Рекомендованный конфиг ESLint:

Сложные функции

Сложные функции идут рука об руку с длинными. Длинные функции всегда сложнее коротких. Что делает функции сложными? Из того, что можно легко пофиксить: вложенные обратные вызовы и высокая цикломатическая сложность.

Вот пример функции с глубоко вложенными обратными вызовами:

Цикломатическая сложность

Очередной существенный источник сложности функций – цикломатическая сложность. Если коротко, она ссылается на количество операторов (логики) в любой данной функции. Имеются в виду оператор if, циклы и оператор switch. Такие функции тяжело понять, а их использование должно быть ограниченным. Пример:

Рекомендованный конфиг ESLint:

Есть ещё один способ уменьшить количество кода, а вместе с тем и его сложность. Подробнее о декларативном коде позже.

Изменчивое состояние

Прощай, плохой код. Здравствуй, улучшенный JavaScript!

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

Проблемы с изменчивым состоянием

Посмотрим на практике, как изменчивое состояние может сделать код проблемным:

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

Как избежать таких проблем? Созданием и возвратом нового объекта:

Рекомендованный конфиг ESLint:

Не используйте метод push с массивами

Те же проблемы свойственны в изменении массивов при использовании таких методов, как push:

Кажется, вы ожидали, что массив b не изменится? Эту ошибку можно обойти, создав новый массив вместо вызова push.

Подобные ошибки легко предотвратить созданием нового массива:

Рекомендованный конфиг ESLint:

Избегайте использования let

Прощай, плохой код. Здравствуй, улучшенный JavaScript!

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

Так каковы альтернативы let? Кончено же const! Хотя оно не гарантирует неизменность, оно улучшает читаемость кода, запрещая переназначения. И, честно говоря, вам не нужен let – в большинстве случаев код, который переназначает переменные можно вынести в отдельную функцию. Давайте посмотрим пример:

И тот же пример, извлечённый в функцию:

Рекомендованный конфиг ESLint:

Декларативный код

Прощай, плохой код. Здравствуй, улучшенный JavaScript!

Если вы программировали некоторое время, скорее всего, вы использовали императивный стиль, который описывает набор определённых шагов, ведущих к желаемому результату. Декларативный стиль, напротив, описывает желаемый результат, а не определённые инструкции.

Примеры распространённых декларативных языков – это SQL и HTML. И даже JSX в React!

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

Это можно представить грубо в императивном JavaScript:

Или в декларативном JavaScript, используя экспериментальный оператор конвейера:

Какой способ вы бы предпочли?

Предпочитайте выражения операторам

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

Какие операторы обычно используются? Считайте таковыми if, return, switch, for, while.

Давайте посмотрим на простой пример:

Это можно легко переписать в троичное выражение (которое декларативно):

И если в лямбда-функции содержится только оператор возврата, JavaScript позволяет нам избавиться от лямбда-оператора:

Тело функции сократилось с шести строчек кода до одной-единственной. Супермощь декларативного кода!

Декларативное программирование требует усилий

Декларативному программированию нельзя научиться за ночь. Особенно учитывая то, что большинство людей в основном обучались императивному программированию. Декларативное программирование требует дисциплины и умения мыслить совершенно по-новому. Как научиться декларативному программированию? Первым делом научиться программировать без изменчивого состояния – не использовать ключевое слово let и не изменять состояние.

Рекомендованный конфиг ESLint:

Избегайте передачи множества параметров функциям

Прощай, плохой код. Здравствуй, улучшенный JavaScript!

JavaScript – это не статически типизированный язык, и в нём нет гарантированного вызова функции с правильными и ожидаемыми параметрами. ES6 вносит много классных функций, включая деструктуризацию объектов, которую можно использовать для аргументов функции.

Вам понятен следующий код? Вы можете сразу сказать, каковы параметры?

А как насчёт этого примера?

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

Рекомендованный конфиг ESLint:

Предпочитайте возврат объектов из функций

Как много следующий отрывок говорит вам о сигнатуре функции? Что она возвращает? Она возвращает объект пользователя, id пользователя, статус операции? Сложно понять без окружающего контекста.

Возврат объекта из функции ясно отражает намерения разработчика, а код становится более читаемым:

Исключения для контроля выполнения

Прощай, плохой код. Здравствуй, улучшенный JavaScript!

Вам понравится Internal Server Error 500 при попытке неверного ввода в форму? Как насчёт работы с API, которое не даёт никаких деталей и выводит только эту ошибку? Наверняка с подобной проблемой сталкивались все, и это нельзя назвать приятным опытом.

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

Исключения ломают безопасность типов

Даже в статически типизированных языках. В соответствии со своей сигнатурой функция fetchUser(id: number): User должна возвращать пользователя. В сигнатуре нет и намёка на исключение в случае, если пользователь не найден. Если ожидается исключение, то более подходящей сигнатурой будет: fetchUser(...): User|throws UserNotFoundError. Конечно, такой синтаксис неверен вне зависимости от языка.

Сложно понять программу с исключениями – можно никогда не узнать, будет ли функция генерировать исключение. Да, можно обернуть каждый отдельный вызов функции в блок try/catch, но это непрактично и снизит читаемость кода.

Исключения ломают композицию функции

Исключения делают виртуально невозможным использование композиции функции. В следующем примере сервер возвращает 500 Internal Error, если один из постов блога не может быть найден:

Что, если один из постов был удалён, но пользователь всё ещё пытается получить доступ к посту из-за неизвестного бага? Это существенно снизит user experience.

Кортежи как альтернативный способ обработки ошибок

Не вдаваясь в подробности функционального программирования: простой способ обработки ошибок – это возврат кортежа, содержащего результат и ошибку вместо генерации исключения. Да, JavaScript не поддерживает кортежи, но их легко эмулировать с помощью двузначного массива в форме [error, result]. Кстати, это хороший способ обработки ошибок в Go:

Иногда исключения – это нормально

Исключения до сих пор имеют своё место в коде. Как правило, вы должны задать себе один вопрос: хочу ли я, чтобы программа упала? Любое сгенерированное исключение может сорвать весь процесс. Даже если мы тщательно продумали все потенциальные крайние случаи, исключения всё ещё небезопасны, и вызовут падение программы когда-нибудь в будущем. Генерируйте исключения, только когда вы действительно намерены «уронить» программу, например, из-за ошибки разработчика, или неудавшегося подключения к базе данных.

Исключения названы исключениями не случайно. Их нужно использовать, только когда случается что-то исключительное, и у программы не остаётся другого выбора кроме как упасть. Генерация и отлавливание исключений – не самый лучший способ контролировать исполнение. Мы должны прибегать к генерированию исключений только в том случае, если произошла неисправимая ошибка. Неправильный пользовательский ввод, к примеру, к таковым не относится.

Что дальше?

Прощай, плохой код. Здравствуй, улучшенный JavaScript!

Вам действительно нужен стабильный код? Решать вам. Ваша организация приравнивает продуктивность разработчиков к числу завершённых историй в Jira? Вы работаете на «фабрике идей», которая не ценит ничего, кроме количества нововведений? Надеемся нет. Но если это так, подумайте о лучшем месте работы…

Возможно, применять всё сразу из этой статьи не стоит. Добавляйте статью в закладки и возвращайтесь. Каждый раз выбирайте одну вещь, на которой вы намеренно сосредоточитесь. И включите соответствующие правила ESLint – главного помощника в вашем путешествии.

Какие ошибки в написании кода чаще всего совершаете вы? 😉

Хотите получать больше интересных материалов с доставкой?

Подпишитесь на нашу рассылку:

И не беспокойтесь, мы тоже не любим спам. Отписаться можно в любое время.




Комментариев: 6

  1. Denis Borzenko

    Сложный денёк, извините, если что. Эта статья — стёб?

    1. Эта статья — перевод хорошей популярной статьи 😉

  2. Никак не могу согласиться со скрытием ошибок через кортежи. JS не слишком удачен в механизмах работы с ошибками, но исключения нужны в том числе для их обработки, и нативные эксепшены в этом случае лучше чем текст. Если функция бросает исключения, а вы их не словили и упали — проблема не в функции. Реакт имеет для этого соответствующие хуки, в ноде есть механизм передачи ошибки через Middleware доп аргументом, куда прилетит исключение. Подобные механизмы стандартны и более ожидаемы,нежели подход, применяемый в го (не самый хороший, на мой взгляд).

  3. Павел Коковин

    В первых же строках и такая грубая ошибка (это я про то, что это якобы идентичные конструкции):
    const doStuff = (a, b, c) => {…}

    // то же самое:
    function doStuff(a, b, c) {

    }
    Дальше читать не стал

    1. Если мы не говорим о привязке к this, то в принципе эти конструкции аналогичны:

      function sum(a,b) { return a + b; }
      const sum = (a,b) => a + b; (или const sum = (a,b) => { return a + b; }) будут работать одинаково

      В статье этот пример приведен как схематическое напоминание о том, что вообще такое стрелочные функции, и в этом смысле вполне корректен. Углубление в специфику arrow functions тут не требуется, речь в статье не об этом идет. Плюс предполагается, что читатель знаком с ES6.

      чтобы снять все возражения, можно было бы написать не «то же самое», а «почти то же самое» 🙂

      1. Я думаю, Павел Коковин в данном случае имел в виду такой механизм js, как подъём (hoisting) объявлений функций.

        Функцию в js можно определить с помощью объявления или выражения.

        Это: const doStuff = (a, b, c) => { return a + b + c; } — пример функции-выражения
        И не важно стрелочная функция или нет. Стрелочная функция это всего лишь более короткая запись анонимной функции.

        А это: function doStuff(a, b, c) { return a + b + c; } — объявление функции

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

        Это означает, что вызовы функции могут предшествовать ее объявлению:
        sum(5, 3);
        function sum(a,b) { return a + b; }

        А с функцией-выражением такой вызов не сработает:
        sum(5, 3); // вызов функции приведёт к ошибке
        const sum = (a,b) => { return a + b; }

        Вот в этом главное отличие этих двух конструкций.

        Также не очень понятна рекомендация: «Не используйте метод push с массивами»

        const a = [‘apple’, ‘orange’];
        const b = a;

        a.push(‘microsoft’)

        // [‘apple’, ‘orange’, ‘microsoft’]
        // неожиданно?
        console.log(b);

        Автор: «Кажется, вы ожидали, что массив b не изменится? Эту ошибку можно обойти, создав новый массив вместо вызова push.»

        Нет никакой ошибки и ничего неожиданного в этом тоже нет. В js доступ к массивам осуществляется по ссылке, а не по значению.
        Если вы определяете массив const a = [‘apple’, ‘orange’], то в переменной «a» хранится не сам этот массив, а ссылка на него.
        И, соответственно, когда вы присваиваете const b = a , то в переменную «b» копируется не сам массив, а ссылка на него.
        Таким образом и «a», и «b» теперь указывают на один и тот же массив, поэтому вполне логично, что изменение «a» отображается на «b».
        И метод push тут совершенно ни при чём. С тем же успехом можно было бы написать a[a.length] = ‘microsoft’; — результат был бы аналогичный.

        Рекомендация «Избегайте использования let» тоже, на мой взгляд, какая-то сомнительная.

Добавить комментарий