Основной источник сложности в JavaScript
Это прозвучит странно, но основным и самым большим источником сложности является сам код. Собственно говоря, именно отсутствие какого-нибудь кода — это лучший способ написать безопасное и надёжное программное приложение. К сожалению, это возможно далеко не всегда, но выход всё же есть — уменьшить объём кода, ведь чем меньше кода, тем меньше сложность и меньше пространства для ошибок. Не зря в IT-мире говорят, что в то время, когда джуниоры пишут код, сеньоры его удаляют )).
Проблема № 1: длинные файлы
От природы люди ленивы, ведь лень, по сути, является краткосрочной стратегией выживания, которая заложена в мозгу и помогает сохранять энергию. Это понятно, но бывает, что человек не только ленив, но и малодисциплинирован.
Как известно, многие разработчики продолжают вмещать в один и тот же файл всё больше кода. А если ограничения на длину файла будут отсутствовать, файлы иногда растут бесконечно. При этом специалисты утверждают, что файл, содержащий более 200 строк кода, становится чересчур большим для восприятия. Такие файлы трудно поддерживать, а код делает слишком много, что, в свою очередь, нарушает принцип единственной ответственности.
Проблема решается путём разбиения больших файлов на более детализированные модули.
Конфигурация, предлагаемая ESLint:
rules: max-lines: - warn - 200
Проблема № 2: длинные функции
Очередной источник сложности — длинные функции. Чаще всего они имеют чересчур много обязанностей, поэтому их сложно проверить.
Давайте посмотрим на фрагмент кода express.js, предназначенный для обновления записи в блоге:
router.put('/api/blog/posts/:id', (req, res) => { if (!req.body.title) { return res.status(400).json({ error: 'title is required', }); } if (!req.body.text) { return res.status(400).json({ error: 'text is required', }); } const postId = parseInt(req.params.id); let blogPost; let postIndex; blogPosts.forEach((post, i) => { if (post.id === postId) { blogPost = post; postIndex = i; } }); if (!blogPost) { return res.status(404).json({ error: 'post not found', }); } const updatedBlogPost = { id: postId, title: req.body.title, text: req.body.text }; blogPosts.splice(postIndex, 1, updatedBlogPost); return res.json({ updatedBlogPost, }); });
Мы видим, что тело функции обладает длиной в 40 строк, плюс решает целый ряд задач: — выполняет анализ идентификатора сообщения; — ищет существующее сообщение в блоге; — осуществляет проверку данных, введённых пользователем, возвращая ошибку, если ввод неправилен; — обновляет коллекцию сообщений, возвращая обновлённые сообщения в блоге.
Всё это мы могли бы преобразовать в несколько функций объёмом меньше. Результат мог бы выглядеть приблизительно так:
router.put("/api/blog/posts/:id", (req, res) => { const { error: validationError } = validateInput(req.body); if (validationError) return errorResponse(res, validationError, 400); const { blogPost } = findBlogPost(blogPosts, req.params.id); const { error: postError } = validateBlogPost(blogPost); if (postError) return errorResponse(res, postError, 404); const updatedBlogPost = buildUpdatedBlogPost(req.body); updateBlogPosts(blogPosts, updatedBlogPost); return res.json({updatedBlogPost}); });
Смотрим конфигурацию ESLint:
rules: max-lines-per-function: - warn - 20
Проблема № 3: сложные функции
Рядом с длинными функциями идут сложные функции. Что делает функцию сложнее? Например, вложенные колбэки (callback) либо высокая цикломатическая сложность.
Те же вложенные колбэки нередко становятся причиной колбэк-ада (callback hell). При этом проблему можно решить с помощью промисов (promise) и асинхронных функций
Рассмотрим функцию с глубоко вложенными колбэками:
fs.readdir(source, function (err, files) { if (err) { console.error('Error finding files: ' + err) } else { files.forEach(function (filename, fileIndex) { gm(source + filename).size(function (err, values) { if (err) { console.error('Error identifying file size: ' + err) } else { aspect = (values.width / values.height) widths.forEach(function (width, widthIndex) { height = Math.round(width / aspect) this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) { if (err) console.error('Error writing file: ' + err) }) }.bind(this)) } }) }) } })
Теперь несколько слов про цикломатическую сложность, перегружающую функции. Речь идёт о числе логических операторов в вашей функции: (операторы if, switch-утверждения, циклы). Воспринимать такие функции трудно, поэтому их применение следует ограничивать:
if (conditionA) { if (conditionB) { while (conditionC) { if (conditionD && conditionE || conditionF) { ... } } } }
Что предлагает ESLint:
rules: complexity: - warn - 5 max-nested-callbacks: - warn - 2 max-depth: - warn - 3
Осталось напомнить, что есть ещё один способ уменьшить объём вашего кода — декларативное программирование. Но о нём мы уже писали.