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

Мне приходилось брать на себя и очищать множество проектов, при этом я не часто переписывал их с нуля. Фактически, я и сейчас этим занимаюсь. Я выучил многое о JavaScript: как сохранять кодовую базу чистой и, главное, как не сойти с ума от того, что сделали до меня. В этой статье я хочу описать свой опыт и показать последовательность шагов.

Анализ проекта

Самый первый шаг — это обзор того, что вообще происходит. Если это сайт, проверьте его функциональность: открывайте модальные окна, отправляйте формы и т.д. В ходе процесса держите открытыми инструменты разработчика, чтобы видеть появляющиеся ошибки и логи. Если это проект на Node.js, откройте интерфейс командной строки и пройдитесь по API. В лучшем случае у проекта есть входная точка (типа main.js, index.js, app.js и т.п.), в которой все модули либо инициализируются, в худшем случае там находится вся бизнес логика.

Определите, какие используются инструменты. jQuery? React? Express? Составьте список всего, что важно знать. Предположим, проект написан на Angular 2 и вы не работали с ним — вам надо ознакомиться с документацией, чтобы получить базовое понимание. Ищите лучшие практики по этим инструментам.

Понимание проекта на высоком уровне

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

Создание основы

Главное это последовательность. Теперь, когда у вас есть все информация об инструментарии проекта, вы знаете его структуру и как подсоединяется логика, пришло время создать базовую линию. Я рекомендую добавить файл .editorconfig, чтобы стили кода оставались последовательными, независимо от редакторов, IDE и разработчиков.

Последовательные отступы

Знаменитый вопрос (больше похожий на холивар) — пробелы или табы, не имеет значения. Кодовая база написана с пробелами? Продолжайте использовать пробелы. Табы? Значит, табы. Только когда в кодовой базе смешаны разные отступы имеет смысл решать, что выбрать. Разные мнения это прекрасно, но в хорошем проекте разработчик не должен париться над такими вещами.

Почему это вообще важно? У каждого свой способ использования редактора или IDE. Я, например, ярый фанат сворачивания. Без этой возможности я буквально теряюсь в файле. Когда отступы непоследовательны, эта возможность не работает. Поэтому каждый раз, когда я открываю файл, я должен исправить все отступы прежде чем я смогу начать работать. Это огромная потеря времени.

// Хотя это  валидный JavaScript, блок
// не сворачивается из-за разных отступов.
 function foo (data) {
  let property = String(data);

if (property === 'bar') {
   property = doSomething(property);
  }
  //... остальная логика.
 }

// Правильные отступы делают блок сворачиваемым,
// это улучшает работу с ним и очищает кодовую базу.
function foo (data) {
 let property = String(data);

 if (property === 'bar') {
  property = doSomething(property);
 }
 //... остальная логика.
}

Именование

Убедитесь, что соглашение об именовании применяется во всем проекте. Обычно в JavaScript используется CamelCase, но я видел много смешанных соглашений. Например, проекты на jQuery часто смешивают именование переменных объекта jQuery и остальных переменных.

// Непоследовательное именование осложняет
// просмотр и понимание кода. Оно также
// может привести к ложным ожиданиям.
const $element = $('.element');

function _privateMethod () {
  const self = $(this);
  const _internalElement = $('.internal-element');
  let $data = element.data('foo');
  //... остальная логика.
}

// Этот код понимается быстрее и проще.
const $element = $('.element');

function _privateMethod () {
  const $this = $(this);
  const $internalElement = $('.internal-element');
  let elementData = $element.data('foo');
  //... остальная логика.
}

Линтинг и еще раз линтинг

Предыдущие шаги были в большей степени косметическими и облегчающими просмотр кода, здесь же мы вводим меры по обеспечению лучших практик и качества кода. ESLint, JSLint и JSHint это наиболее популярные линтеры JavaScript на данный момент. Лично я долго использовал JSHint, но сейчас ESLint становится моим фаворитом, в основном благодаря его кастомным правилам и ранней поддержке ES2015.

Когда вы приступаете к линтингу и у вас обнаруживается куча ошибок — исправляйте их! Не приступайте к чему-то другому, пока ваш линтер не будет доволен.

Обновление зависимостей

Обновлять зависимости надо аккуратно. Легко добавить новых ошибок, если не уделять внимание тому, какие изменения вносятся в зависимости. Некоторые проекты могут работать с фиксированными версиями (например, v1.12.5), другие могут работать с диапазоном версий (типа v1.12.x). Номер версии конструируется следующим образом: MAJOR.MINOR.PATCH. Если вы не знакомы с работой семантического версионирования, я рекомендую статью Тима Оксли.

Нет общего правила при обновлении зависимостей. Каждый проект индивидуален и требует своего подхода. Обновление номера PATCH в ваших зависимостях не будет проблемой, как правило, то же самое обычно относится и к MINOR. Только когда вы обновляете номер MAJOR, вам надо следить за тем, что поменялось. Может полностью поменяться API и вам придется переписать большую часть приложения. Если это переписывание не стоит усилий, я бы избегал обновления на следующую мажорную версию.

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

Список обновлений для пакетов npm

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

Начало работы

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

Создание модульных тестов

Наличие модульных тестов гарантирует вам понимание того, как этот код должен работать, а, значит, вы ничего случайно не поломаете. Модульное тестирование JavaScript это тема отдельных статей, поэтому я не могу погружаться в детали. Здесь широко применяются фреймворки Karma, Jasmine, Mocha или Ava. Если вы также хотите тестировать пользовательский интерфейс, то Nightwatch.js и DalekJS являются рекомендованными инструментами браузерной автоматизации.

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

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

Ребекка Мёрфи написала великолепную статью о создании модульных тестов в существующем JavaScript.

Архитектура

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

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

Если в вашем проекте архитектура отсутствует как таковая (все приложение это один огромный app.js), то пришло время изменить это. Не делайте все сразу — это надо делать шаг за шагом. Опять — здесь нет общего способа и настройка каждого проекта различна. Структура каталогов в проектах меняется в зависимости от их размера и сложности. Обычно, на самом базовом уровне в структуре разделяются сторонние библиотеки, модули, данные и входная точка (типа index.js или main.js), в которой все модули и логика инициализируются.

Так мы плавно переходим к модуляризации.

Модульность везде?

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

Как разбить какую-нибудь большую часть функционала с тесно связанной логикой? Попробуем сделать это.

// Этот пример использует Fetch API для запросов к API. Предположим,
// нам возвращается файл JSON с каким-нибудь простым содержимым. 
// Затем мы создаем новый элемент, считаем все символы содержимого и 
// вставляем его в каком-то месте пользовательского интерфейса.
fetch('https://api.somewebsite.io/post/61454e0126ebb8a2e85d', { method: 'GET' })
  .then(response => {
    if (response.status === 200) {
      return response.json();
    }
  })
  .then(json => {
    if (json) {
      Object.keys(json).forEach(key => {
        const item = json[key];
        const count = item.content.trim().replace(/\s+/gi, '').length;
        const el = `
          <div class="foo-${item.className}">
            <p>Total characters: ${count}</p>
          </div>
        `;
        const wrapper = document.querySelector('.info-element');

        wrapper.innerHTML = el;
      });
    }
  })
  .catch(error => console.error(error));

Это не очень модульно. Все части тесно связаны и взаимозависимы. Представьте то же самое с большими и сложными функциями, а затем процесс отладки, когда что-то поломается. Например, не отвечает API, после того как, что-то изменилось внутри JSON или где-то еще. Кошмар и боль, да.

Разделим этот код на основе различных обязанностей.

// У нас была функция для подсчета символов — 
// сделаем из нее отдельный модуль.
function countCharacters (text) {
  const removeWhitespace = /\s+/gi;
  return text.trim().replace(removeWhitespace, '').length;
}

// Кусок разметки также вынесен в отдельный модуль.
// Мы используем DOM API для созданияHTML,
// вместо того, чтобы вставлять готовую строку.
function createWrapperElement (cssClass, content) {
  const className = cssClass || 'default';
  const wrapperElement = document.createElement('div');
  const textElement = document.createElement('p');
  const textNode = document.createTextNode(`Total characters: ${content}`);

  wrapperElement.classList.add(className);
  textElement.appendChild(textNode);
  wrapperElement.appendChild(textElement);

  return wrapperElement;
}

// Анонимная функция из метода .forEach() 
// тоже стала отдельным модулем.
function appendCharacterCount (config) {
  const wordCount = countCharacters(config.content);
  const wrapperElement = createWrapperElement(config.className, wordCount);
  const infoElement = document.querySelector('.info-element');

  infoElement.appendChild(wrapperElement);
}

Итак, теперь у нас три новых модуля. Проведем рефакторинг вызова fetch:

fetch('https://api.somewebsite.io/post/61454e0126ebb8a2e85d', { method: 'GET' })
  .then(response => {
    if (response.status === 200) {
      return response.json();
    }
  })
  .then(json => {
    if (json) {
      Object.keys(json).forEach(key => appendCharacterCount(json[key]))
    }
  })
  .catch(error => console.error(error));

Мы также можем взять логику внутри .then() и разделить ее, но для целей демонстрации хватит и этого.

А что если не модуляризировать?

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

Документируйте свой код

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

JSDoc это генератор документации API для JavaScript. Обычно он доступен в виде плагина для всех широко распространенных редакторов и IDE. Рассмотрим его на примере:

function properties (name, obj = {}) {
  if (!name) return;
  const arr = [];

  Object.keys(obj).forEach(key => {
    if (arr.indexOf(obj[key][name]) <= -1) {
      arr.push(obj[key][name]);
    }
  });

  return arr;
}

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

**
 * Проходит по объекту и собирает все свойства, совпадающие с 'name' 
 * в новый объект, но не более одного раза
 * @param  {String}  propertyName - Имя искомого свойства
 * @param  {Object}  obj          - Объект, по которому проводится итерация
 * @return {Array}
 */
function getArrayOfProperties (propertyName, obj = {}) {
  if (!propertyName) return;
  const properties = [];
  Object.keys(obj).forEach(child => {
    if (properties.indexOf(obj[child][propertyName]) <= -1) {
      properties.push(obj[child][propertyName]);
    }
  });
  return properties;
}

Я не слишком сильно трогал сам код. Простое переименование функции и добавление краткого, но детального блока комментариев сразу улучшило читаемость.

Организация коммитов

Рефакторинг сам по себе это серьезная миссия. Чтобы иметь возможность всегда откатить свои изменения (в случае, если вы что-то поломали и не сразу это заметили), я рекомендую коммитить каждое обновление. Переписали метод? Значит, git commit (или svn commit, если вы работаете с SVN). Переименовали пространство имен, каталог или несколько изображений? Опять git commit. Именно так. Это может показаться утомительным для некоторых, но это помогает вам проводить очистку правильно и получить организованный проект.

Создайте новую ветку для всего проекта по рефакторингу. Никогда не работайте в мастер-ветке. У вас должна быть возможность быстро изменить или отправить баг-фиксы на рабочий сервер и вам не нужно развертывать код, пока он не завершен и не протестирован. Поэтому всегда рекомендуется работать в другой ветке.

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

Не теряйте голову

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

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

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

Заключение

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

  1. Анализ проекта
    • Забудьте на минуту, что вы разработчик и взгляните на все с позиции пользователя.
    • Пройдите по кодовой базе и создайте список используемых инструментов.
    • Изучите документацию и лучшие практики по этим инструментам.
    • Запустите модульные тесты, чтобы получить понимание проекта на более высоком уровне.
  2. Создайте базовую линию
    • Создайте .editorconfig, чтобы стиль написания кода был последовательным во всех IDE.
    • Сделайте отступы единообразными — не важно, табы это будут или пробелы.
    • Соблюдайте систему именования.
    • Если в проекте нет линтера, добавьте его — ESLint, JSLint и JSHint.
    • Обновите зависимости, но делайте это разумно, отслеживая, что именно изменилось.
  3. Очистка
    • Внедрите модульное тестирование и инструменты браузерной автоматизации типа Karma, Jasmine или Nightwatch.js.
    • Обеспечьте последовательность реализации архитектуры и паттернов проектирования.
    • Не смешивайте разные паттерны проектирования, придерживайтесь тех, что уже есть.
    • Определитесь, будете ли вы разбивать кодовую базу на модули. У каждого из модулей должна быть единственная цель и он не должен ничего знать об остальной логике вашего кода.
    • Если вы не хотите разделять код на модули, сфокусируйтесь на написании тестируемого кода, разбивая его на простые блоки.
    • Сбалансированно документируйте ваши функции и код за счет правильного именования функций.
    • Используйте JSDoc для генерации документации.
    • Делайте коммиты после важных изменений. Если что-то поломается, вам будет проще откатить все назад.
  4. Не теряйте голову
    • Не сходите с ума из-за предыдущего разработчика — негативизм принесет вам только трату времени на необязательный рефакторинг.
    • Код может быть написан так в связи с рабочими причинами. Учитывайте, что вы тоже можете оказаться в такой ситуации.