Хабрахабр

[Перевод] Пишем чистый и масштабируемый JavaScript-код: 12 советов

Язык JavaScript родом из раннего веба. Сначала на нём писали простые скрипты, которые «оживляли» страницы сайтов. Теперь же JS превратился в полноценный язык программирования, который можно использовать даже для разработки серверных проектов.

Особенно это касается одностраничных приложений (Single-Page Application, SPA). Современные веб-приложения сильно зависят от JavaScript. С появлением библиотек и фреймворков, таких как React, Angular и Vue, JavaScript стал одним из основных строительных блоков веб-приложений.

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

Он говорит, что статья рассчитана на JS-программистов с любым уровнем подготовки. Автор статьи, перевод которой мы сегодня публикуем, хочет поделиться советами по написанию чистого JavaScript-кода. Но особенно полезной она будет для тех, кто знаком с JavaScript хотя бы на среднем уровне.

1. Изоляция кода

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

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

2. Разбивка кода на модули

Функции, которые используются похожим образом или выполняют похожие действия, можно сгруппировать в одном модуле (или, если хотите, в отдельном классе). Предположим, в вашем проекте нужно выполнять различные вычисления. В такой ситуации разные этапы подобных вычислений имеет смысл выразить в виде отдельных функций (изолированных блоков), вызовы которых можно объединять в цепочки. Однако все эти функции могут быть объявлены в одном файле (то есть — в модуле). Вот пример модуля calculation.js, который содержит подобные функции:

function add(a, b) function subtract(a, b) { return a - b } module.exports = { add, subtract
}

А вот как этим модулем можно воспользоваться в другом файле (назовём его index.js):

const { add, subtract } = require('./calculations') console.log(subtract(5, add(3, 2))

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

3. Используйте несколько параметров функций вместо одного объекта с параметрами

При объявлении функции следует стремиться к использованию нескольких параметров, а не одного объекта с параметрами. Вот пара примеров:

// Хорошо
function displayUser(firstName, lastName, age) { console.log(`This is ${firstName} ${lastName}. She is ${age} years old.`)
} // Плохо
function displayUser(user) { console.log(`This is ${user.firstName} ${user.lastName}. She is ${user.age} years old.`)
}

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

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

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

Если некоторые из параметров являются необязательными, то вместо них необходимо передавать функции нечто вроде undefined или null. Основная причина подобной рекомендации заключается в том, что отдельные параметры, ожидаемые функцией, должны передаваться ей в определённом порядке. При таком подходе можно обойтись и без установки необязательных параметров в undefined. При использовании объекта с параметрами порядок параметров в объекте значения не имеет.

4. Деструктурирование

Деструктурирование — полезный механизм, который появился в ES6. Он позволяет извлекать заданные поля из объектов и тут же записывать их в переменные. Им можно пользоваться при работе с объектами и модулями:

// Работа с модулем
const { add, subtract } = require('./calculations')

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

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

function logCountry({name, code, language, currency, population, continent}) { let msg = `The official language of ${name} ` if(code) msg += `(${code}) ` msg += `is ${language}. ${population} inhabitants pay in ${currency}.` if(contintent) msg += ` The country is located in ${continent}`
} logCountry({ name: 'Germany', code: 'DE', language 'german', currency: 'Euro', population: '82 Million',
}) logCountry({ name: 'China', language 'mandarin', currency: 'Renminbi', population: '1.4 Billion', continent: 'Asia',
})

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

Кстати, деструктурирование можно использовать и при работе с функциональными компонентами React.

5. Задавайте стандартные значения параметров функций

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

function logCountry({ name = 'United States', code, language = 'English', currency = 'USD', population = '327 Million', continent,
}) { let msg = `The official language of ${name} ` if(code) msg += `(${code}) ` msg += `is ${language}. ${population} inhabitants pay in ${currency}.` if(contintent) msg += ` The country is located in ${continent}`
} logCountry({ name: 'Germany', code: 'DE', language 'german', currency: 'Euro', population: '82 Million',
}) logCountry({ name: 'China', language 'mandarin', currency: 'Renminbi', population: '1.4 Billion', continent: 'Asia',
})

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

6. Не передавайте функциям ненужные данные

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

7. Ограничение числа строк в файлах и максимального уровня вложенности кода

Мне доводилось видеть большие файлы с программным кодом. Очень большие. В некоторых было более 3000 строк. В таких файлах очень сложно ориентироваться.

Я обычно стремлюсь к тому, чтобы размер моих файлов не превышал бы 100 строк. В результате рекомендуется ограничивать размер файлов, измеряемый в строках кода. И очень редко их размер доходит до 400 строк. Иногда, когда сложно бывает разбить некую логику на небольшие фрагменты, размеры моих файлов достигают 200-300 строк. Файлы, размеры которых превышают этот предел, тяжело читать и поддерживать.

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

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

Возможно, соблюдать эти рекомендации поможет применение подходящих правил линтера ESLint.

8. Пользуйтесь инструментами для автоматического форматирования кода

При командной работе над JavaScript-проектами необходимо выработать чёткое руководство по стилю и форматированию кода. Автоматизировать форматирование кода можно с помощью ESLint. Этот линтер предлагает разработчику огромный набор правил, поддающихся настройке. Существует команда eslint --fix, которая умеет исправлять некоторые ошибки.

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

9. Используйте хорошо продуманные имена переменных

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

▍Функции

Обычно функции выполняют какие-то действия. Люди, когда говорят о действиях, используют глаголы. Например — convert (конвертировать) или display (показать). Имена функций рекомендуется формировать так, чтобы они начинались с глагола. Например — convertCurrency или displayUser.

▍Массивы

Массивы обычно содержат в себе наборы каких-то значений. В результате к имени переменной, хранящей массив, имеет смысл добавлять букву s. Например:

const students = ['Eddie', 'Julia', 'Nathan', 'Theresa']

▍Логические значения

Имена логических переменных имеет смысл начинать с is или has. Это приближает их к конструкциям, которые имеются в обычном языке. Например, вот вопрос: «Is that person a teacher?». Ответом на него может служить «Yes» или «No». Аналогично можно поступать и подбирая имена для логических переменных:

const isTeacher = true // или false

▍Параметры функций, передаваемых стандартным методам массивов

Вот несколько стандартных методов массивов JavaScript: forEach, map, reduce, filter. Они позволяют выполнять с массивами некие действия. Им передают функции, которые описывают операции над массивами. Я видел, как многие программисты просто передают таким функциям параметры с именами наподобие el или element. Хотя такой подход избавляет программиста от размышлений об именовании подобных параметров, называть их лучше с учётом данных, которые в них оказываются. Например:

const cities = ['Berlin', 'San Francisco', 'Tel Aviv', 'Seoul']
cities.forEach(function(city) {
...
})

▍Идентификаторы

Часто бывает так, что программисту нужно работать с идентификаторами неких наборов данных или объектов. Если подобные идентификаторы являются вложенными, ничего особенного делать с ними не нужно. Я, например, при работе с MongoDB, обычно, перед возвратом объекта фронтенд-приложению, преобразую _id в id. При извлечении идентификаторов из объектов рекомендуется формировать их имена, ставя перед id тип объекта. Например:

const studentId = student.id
// или
const { id: studentId } = student // деструктурирование с переименованием

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

const StudentSchema = new Schema({ teacher: { type: Schema.Types.ObjectId, ref: 'Teacher', required: true, }, name: String, ...
})

10. Используйте там, где это возможно, конструкцию async/await

Использование коллбэков ухудшает читабельность кода. Особенно это касается вложенных коллбэков. Промисы немного выправили ситуацию, но я полагаю, что лучше всего читается код, в котором используется конструкция async/await. С таким кодом несложно разобраться даже новичкам и разработчикам, перешедшим на JavaScript с других языков. Самое главное здесь — освоить концепции, лежащие в основе async/await. Не стоит повсюду использовать эту конструкцию только из-за её новизны.

11. Порядок импорта модулей

В рекомендациях 1 и 2 была продемонстрирована важность правильного выбора места хранения кода для обеспечения его поддерживаемости. Аналогичные идеи применимы и к порядку импорта модулей. А именно, речь идёт о том, что логичный порядок импорта модулей делает код понятнее. Я, импортируя модули, придерживаюсь следующей простой схемы:

// Пакеты сторонних разработчиков
import React from 'react'
import styled from 'styled-components' // Хранилища
import Store from '~/Store // Компоненты, поддерживающие многократное использование
import Button from '~/components/Button' // Вспомогательные функции
import { add, subtract } from '~/utils/calculate' // Субмодули
import Intro from './Intro'
import Selector from './Selector'

Данный пример основан на React. Эту же идею несложно будет перенести и в любое другое окружение разработки.

12. Избегайте использования console.log

Команда console.log представляет собой простой, быстрый и удобный инструмент для отладки программ. Существуют, конечно, и более продвинутые средства такого рода, но я думаю, что console.log всё ещё пользуются практически все программисты. Если, используя console.log для отладки, не убирать вовремя вызовы этой команды, ставшие ненужными, консоль скоро придёт в полный беспорядок. При этом надо отметить, что некоторые команды логирования имеет смысл оставлять даже в коде проектов, полностью готовых к работе. Например — команды, выводящие сообщения об ошибках и предупреждения.

Среди них — loglevel и winston. В результате можно сказать, что для отладочных целей вполне можно пользоваться console.log, а в тех случаях, когда команды логирования планируется использовать в работающих проектах, имеет смысл прибегнуть к специализированным библиотекам. Это позволяет выполнять глобальный поиск и удаление подобных команд. Кроме того, для борьбы с ненужными командами логирования можно воспользоваться ESLint.

Итоги

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

Уважаемые читатели! Что бы вы могли добавить к приведённым здесь 12 советам по написанию чистого и масштабируемого JS-кода?

Теги
Показать больше

Похожие статьи

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть