Как не лажать с JavaScript. Часть 2

Java Script

Часть 1, Часть 2

Значение рефакторинга

Photo by Jason Leung on Unsplash

Рефакторинг — это контролируемый процесс улучшения кода без написания новой функциональности. При правильном применении рефакторинг может стать мощным оружием против монстра под названием “технический долг”. Без постоянного рефакторинга технический долг будет накапливаться, что, в свою очередь, приведет к низкой производительности и плохому настроению разработчиков. Технический долг является одной из основных причин эмоционального выгорания программистов.

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

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

Основной источник проблем

Photo by Leonel Fernandez on Unsplash

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

Большие файлы

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

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

Как можно решить эту проблему? Просто разбейте большие файлы на менее крупные и более детализированные модули.

Предлагаемая конфигурация ESLint:

rules:
  max-lines:
  - warn
  - 200

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

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

Давайте рассмотрим следующий отрывок кода 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,
  });
});

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

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

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

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

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

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

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

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

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

Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming