Как стать автором
Обновить
2627.46
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Этот опасный рефакторинг

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров6.8K
Автор оригинала: Miroslav Nikolov

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

▍ Любой рефакторинг сопряжён с рисками


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

Некоторые задачи рефакторинга подразумевают крупные изменения и затрагивают несколько подсистем. Другие при этом ограничиваются одним компонентом, но могут непредвиденно повлиять на другие части системы и вызвать поломку важнейших бизнес-операций. В этом случае речь может идти о действующем потоке приобретения товара. Третья категория – это доработки, позволяющие внести новые возможности – например, изменение потока приобретения одного товара для поддержки большего числа его единиц и добавление ещё одного потока после.
Объединяет все эти сценарии то, что они сопряжены с высоким риском.
  1. В случае ошибки такие доработки навредят бизнесу (потеря прибыли, недовольство клиентов), команде (подорвут доверие, мотивацию) или другим связанным функциям (разработка встанет).
  2. Причём реализация этих изменений весьма затратна, так как требует повышенного внимания, усилий и времени. Предпочтительнее для таких задач задействовать опытных разработчиков, хорошо разбирающихся в этой области.

▍ Как снизить риски


Рекомендую использовать чек-лист:
  • Определите ограничения. Как далеко можно зайти?
  • Изолируйте доработки от функциональности. Не применяйте их вместе.
  • Напишите обширные тесты, более высокоуровневые (интеграционные) с меньшим числом деталей реализации, и сопровождайте ими внесение изменений.
  • Проверьте всё наглядно. Запустите браузер.
  • Не пропускайте тесты. Не ленитесь.
  • Не полагайтесь слишком сильно на код-ревью и контроль качества (QA). Люди ошибаются.
  • Не смешивайте масштабные зачистки с другими изменениями. Это можно делать в случае небольших доработок.
Иногда очевидно, какие и где именно в базе кода нужны доработки, будь то вынесение части кода из компонента или дополнительная уборка, чтобы не ставить новую функциональность на руины старой. Рано или поздно разработчику приходится со всем этим сталкиваться. Но в сложном рефакторинге скрываются подводные камни – проверить изменение бывает непросто из-за помех со стороны среды разработки, зависимостей, баз данных/API, нестабильных тестов или отсутствия времени. Код также может ломаться и в процессе внесения доработок. Как же перехватывать сбои на ранних стадиях?

Может возникнуть соблазн просто всё переписать, но…

▍ Не спешите


1. Заранее оцените, насколько рисковым (затратным) окажется изменение с позиции разработки и бизнеса.

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

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

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

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

3. Предварительно убедитесь в работоспособности системы.

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

▍ Такой рефакторинг – серьёзное дело


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

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

// Тест компонента React.

it("should show all addons", () => {
  // Имитация ответа сервера.
  // Утилита, имитирующая минимальный возможный уровень – window.fetch()
  server.get(endpoints.loadAddonsData, { addons: ["addon1", "addon2"] });
  // Свойство, управляющее кнопкой компонента.
  props.shouldShowMoreAddons = true;

  // Отрисовка.
  render(<Addons {...props} />);

  // Клик по кнопке для показа всех аддонов.
  fireEvent.click(screen.getByRole("button"));

  // Вывод списка аддонов.
  await waitFor(() => {
    expect(screen.queryByRole("list")).toBeInTheDocument();
  });
})

Когда все тесты будут пройдены, передавайте код отделу QA для следующего уровня верификации.

▍ Рефакторинг параллельно с добавлением новой функциональности


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

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

Небольшие доработки можно вносить смело.

▍ {} Примеры


В примере ниже показан компонент React Dashboard, отрисовывающий несколько виджетов и окно дополнительной продажи (<UpsellBox1 />), данные которого определяются вызовом бэкенда (loadUpsell1Data()).

// Пример очень упрощён.
export function Dashboard({ data }) {
  // Прочая бизнес-логика.
  const [shouldShowUpsell1, setShouldShowUpsell1] = useState(false);
  const [upsell1Data, setUpsell1Data] = useState(null);

  // Прочая бизнес-логика
  // …
  // …
  // …

  useEffect(() => {
    // Сложная логика для определения, нужно ли показывать UpsellBox1.
    if (condition1 && condition2 && !condition3) {
      setShouldShowUpsell1(true);

      loadUpsell1Data().then((bannerData) => {
        setUpsell1Data(bannerData);
      });
    }
  }, [...]);

  return (
    <div>
      <Widget1 />
      <Widget2 />
      ...
      {shouldShowUpsell1 && <UpsellBox1 data={upsell1Data} />}
    </div>
  );
}

Представьте, что вам нужно отрисовать второе окно апсейла (<UpsellBox2 />) и в случае показа обоих оформить их каруселью.

return (
  <div>
    <Widget1 />
    <Widget2 />
    ...
    <!-- Показ обоих окон апсейла -->
    {shouldShowUpsell1 && shouldShowUpsell2 && (
      <UpsellSlider>
        <UpsellBox1 data={upsell1Data} />
        <UpsellBox2 data={upsell2Data} />
      </UpsellSlider>
    )}
    <!-- ИЛИ только <UpsellBox1 /> -->
    {shouldShowUpsell1 && <UpsellBox1 data={upsell1Data} />}
    <!-- ИЛИ только <UpsellBox2 /> -->
    {shouldShowUpsell2 && <UpsellBox2 data={upsell2Data} />}
  </div>
);

Поскольку здесь у нас два окна апсейла (в будущем, возможно, больше), логика в <Dashboard /> разрастается. Есть смысл вынести код апсейла наружу – в кастомный хук useUpsellData (а не в <UpsellSlider />), чтобы сохранить явность – но это будет непросто, поскольку виджеты и окна апсейла смешиваются.

Вот как может выглядеть компонент после этих изменений:

// Пример сильно упрощён.
export function Dashboard({ data }) {
  // ✅ Тестирование перед рефакторингом.
  // ❓ Логика устанавливает «условия», необходимые в useUpsellData.
  // Здесь размещается остальная бизнес-логика.

  // ✅ Новые тесты, проверяющие, чтобы существующий код не сломался.
  // ❓ Подтверждение, что новый код работает.
  // ? Высокий риск – оба блока апсейла могут сломаться, что создаст хлопоты для техподдержки и приведёт к потере продаж.
  // Тесты хука будут пересекаться с интеграционными тестами Dashboard.
  // Это нормально.
  const {
    upsell1: { shouldShowUpsell1, upsell1Data },
    upsell2: { shouldShowUpsell2, upsell2Data },
  } = useUpsellData(conditions);

  // ✅ Тестирование перед рефакторингом, особенно если последующая 
  // логика разветвляется апсейлами. 
  // ❓ Цель – проследить, чтобы существующий код не сломался. 
  // ? Высокий риск – может нарушить важную функциональность, за чем будет сложно проследить.

  // Прочая бизнес-логика
  // …
  // …
  // …

  // ✅ Теперь этого useEffect нету, но его реализацию нужно 
  // протестировать в рамках useUpsellData() ?
  // ❓ Цель – проследить, чтобы существующий код не сломался.
  // ? Высокий риск – может сломать первый блок апсейла и привести к потере продаж.
  // useEffect(() => {
  //   if (condition1 && condition2 && !condition3) {
  //     setShouldShowUpsell1(true);
  //
  //     loadUpsell1Data().then((bannerData) => {
  //       setUpsell1Data(bannerData);
  //     });
  //   }
  // }, [...]);

  return (
    <div>
      <!--
        ✅ Тестирование виджетов перед рефакторингом.
        ❓ Цель – проследить, чтобы существующий код не сломался.
        ? Риск средний/высокий – может нарушить работу чего-то важного.
      -->
      <Widget1 />
      <Widget2 />
      ...
      <!--
        ✅ Новые тесты изменений для карусели из двух слайдов.
        ? Риск высокий/средний – может нарушить существующую функциональность.
      -->
      {shouldShowUpsell1 && shouldShowUpsell2 && (
        <UpsellSlider>
          <UpsellBox1 data={upsell1Data} />
          <UpsellBox2 data={upsell2Data} />
        </UpsellSlider>
      )}
      <!--
        ✅ Тестирование конкретного случая перед рефакторингом.
        ❓ Цель – проследить, чтобы существующий код не сломался.
        ? Риск высокий/средний – может нарушить существующую функциональность.
      -->
      {shouldShowUpsell1 && <UpsellBox1 data={upsell1Data} />}
      <!--
        ✅ Новые тесты для проверки изменения.
        ? Риск средний.
      -->
      {shouldShowUpsell2 && <UpsellBox2 data={upsell2Data} />}
    </div>
  );
}

Я привёл эти примеры, чтобы показать, насколько неустойчивым может быть рефакторинг при параллельном внесении функциональности в работающий компонент. Проблемы в основном возникают из-за тесно связанной бизнес-логики <Dashboard />, которая обрабатывает множество виджетов и разделов, выступающих её прямыми потомками.

▍ Когда проводить рефакторинг?

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

Telegram-канал со скидками, розыгрышами призов и новостями IT ?
Теги:
Хабы:
Всего голосов 27: ↑24 и ↓3+37
Комментарии19

Публикации

Информация

Сайт
ruvds.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
ruvds