Рубріки: Теория

Как провести рефакторинг и не создать новые баги: полный разбор подхода

Ольга Змерзла

Что такое рефакторинг?

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

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

Рефакторинг — это метод разработки программного обеспечения, который включает улучшение дизайна, структуры и качества существующего кода без изменения его внешнего поведения.

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

Эта практика применина к практически всем языкам программирования и средам разработки, включая Java, Python и Ruby on Rails.

Цель рефакторинга — сделать код более понятным, модифицировать и поддерживать его с течением времени.

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

Рефакторинг можно делать разными методами. Вот несколько базовых примеров:

  • разбивать большие методы и классы на более мелкие;
  • переименовывать переменные, функции и классы;
  • добавлять комментарии с пояснениями;
  • удалять дублирующиеся куски кода.

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

Рефакторинг может занимать много времени, но оно того стоит. В будущем вы, наоборот, снизите затраты на обслуживание. Также эта практика повысит производительность труда разработчиков: оптимизированный код будет гораздо проще обновлять и развивать.

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

Правила рефакторинга

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

Основные правила рефакторинга перечислены ниже:

  1. Рефакторинг не должен изменять поведение кода с точки зрения пользователя. Например, данные, которые выводит код, после рефакторинга должны остаться прежними.
  2. До и после рефакторинга нужно тщательно протестировать код. После — чтобы убедиться, что вы не создали новые баги или ошибки. До — чтобы точно знать все особенности оригинального кода.
  3. Рефакторинг нужно выполнять поэтапно. Каждый шаг должен быть небольшим и включать только одно-два действия. Это сведет к минимуму риск появления новых ошибок или проблем технического характера.
  4. Прежде чем переходить к следующему шагу, нужно проверить, как повлиял на код предыдущий.
  5. Код должен иметь четкие, осмысленные имена для переменных, функций и классов. Это нужно, чтобы другим разработчикам было легко его понять.
  6. Дублирующиеся куски кода удаляют, чтобы уменьшить сложность и облегчить поддержку кода. Исключение: если нет возможности иначе написать функциональность.
  7. Код должен быть максимально простым и понятным. Сложный код трудно поддерживать с течением времени.
  8. Код должен быть хорошо организованным и удобным для навигации. Это упрощает поиск и исправление ошибок или проблем по мере их возникновения.

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

Принципы рефакторинга на примерах

Чтобы лучше понять, как применять вышеописанные правила, давайте рассмотрим их на примерах.

Улучшение кода без изменения его поведения с точки зрения пользователя

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

// до рефакторинга
function sum(a, b) {
  return a + b;
}

// после рефакторинга
function sum(a, b) {
  const result = a + b;
  return result;
}

Тестирование

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

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

// до рефакторинга
function sortArray(array) {
  return array.sort();
}

// после рефакторинга
function sortArray(array) {
  const sortedArray = [...array].sort((a, b) => a - b);
  return sortedArray;
}

Рефакторинг нужно проводить поэтапно

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

// до рефакторинга
function complexFunction(a, b, c, d) {
  // perform operation 1
  // perform operation 2
  // perform operation 3
  // ...
  return result;
}

// после рефакторинга
function operation1(a, b) {
  // perform operation 1
  return result1;
}

function operation2(c, d) {
  // perform operation 2
  return result2;
}

function operation3(result1, result2) {
  // perform operation 3
  return result3;
}

function complexFunction(a, b, c, d) {
  const result1 = operation1(a, b);
  const result2 = operation2(c, d);
  const result3 = operation3(result1, result2);
  return result3;
}

Четкие и осмысленные имена переменных, классов и методов

Например, у вас есть функция, которая вычисляет площадь прямоугольника. Она включает:

  • Название самое функции. Например, calculateRectangleArea.
  • Названия трех ее переменных: ширины и высоты, а также вычисляемой площади. Их можно так и назвать: width, height и area.
// до рефакторинга
function calculateArea(x, y) {
  return x * y;
}

// после рефакторинга
function calculateRectangleArea(width, height) {
  const area = width * height;
  return area;
}

Дубли кода удаляются

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

Во всех других случаях вы можете просто вынести повторяющийся код в отдельную функцию:

// до рефакторинга
function calculateTotalPrice(quantity, price) {
  const totalPrice = quantity * price;
  const discount = 0.1;
  const discountedPrice = (totalPrice * discount) + totalPrice;
  console.log(discountedPrice);
}
// после рефакторинга
function calculateDiscountedPrice(totalPrice, discount) {
  const discountedPrice = totalPrice * (1 - discount);
  return discountedPrice;
}

Зачем нужно проводить рефакторинг?

Обычно рефакторинг проводят по одной (или нескольких сразу) из следующих причин:

  1. Улучшение качества кода. Рефакторинг помогает обнаружить и устранить недостатки кода или дизайна, которые могут привести к ошибкам и снижению производительности.
  2. Простота обслуживания. Делая код более легким для чтения и понимания, рефакторинг помогает упростить его обслуживание и обновление со временем. Это особенно важно для больших и сложных программных систем, которыми трудно управлять без регулярного рефакторинга.
  3. Производительность. Рефакторинг может повысить производительность кода за счет устранения ненужных шагов, уменьшения сложности кода и оптимизации алгоритмов.
  4. Сокращение технического долга. Рефакторинг помогает сократить накопления мусора, мелких проблем и плохо выполненных задач при проектировании и разработке программного обеспечения. Часто именно они приводят к ухудшению качества кода и усложняют поддержку и обновление кода в будущем.
  5. Совместная работа. Рефакторинг помогает сделать код более читаемым и понятным для других разработчиков. Это облегчает совместную работу над проектами и позволяет избежать недоразумений и конфликтов.

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

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

Рефакторинг нужен:

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

Почему рефакторинг приносит результаты

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

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

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

Это, своего рода, самопроверка — сможете ли вы кратко в комментариях объяснить, почему нужен именно этот кусок кода?

Пример эффективного рефакторинга

Допустим, вы работаете над веб-приложением с формой авторизации. У формы есть одна-единственная функция проверки. Функция следит за тем, чтобы поля имени пользователя и пароля не были пустыми:

def validate_login(username, password):
    if not username:
        return "Username is required"
    if not password:
        return "Password is required"
    return None

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

Каждое из новых требований вы записываете все в той же одной-единственной функции, из-за чего она разрастается и становится все более сложной.

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

Логичное решение — вынести каждую из проверок в отдельную функцию с соответствующим названием и целью:

def validate_username(username):
    if not username:
        return "Username is required"
    if not re.match(r"[^@]+@[^@]+\.[^@]+", username):
        return "Username must be a valid email address"
    return None

def validate_password(password):
    if not password:
        return "Password is required"
    if len(password) < 8:
        return "Password must be at least 8 characters long"
    if not any(c.isupper() for c in password):
        return "Password must contain at least one uppercase letter"
    if not any(c.isdigit() for c in password):
        return "Password must contain at least one digit"
    return None

def validate_login(username, password):
    errors = []
    username_error = validate_username(username)
    if username_error:
        errors.append(username_error)
    password_error = validate_password(password)
    if password_error:
        errors.append(password_error)
    return errors or None

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

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

Более того, если в будущем вам потребуется добавить новые правила проверки, вы можете просто создать новую функцию и добавить ее к функции validate_login.

Когда рефакторинг не нужен?

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

  1. Код имеет ограниченный срок службы. Если код предназначен только для краткосрочного или одноразового использования, нет смысла оптимизировать его — его развития и расширения точно не будет.
  2. Код стабилен и хорошо работает. Если код стабилен и работает хорошо, без существенных проблем или ошибок, рефакторингу просто нечего будет улучшить.
  3. Стоимость рефакторинга слишком высокая. Это не всегда проблема, но при ограниченном бюджете и других приоритетах, лучше потратить эти деньги на задачи, которые имеют большее влияние на проект в целом.
  4. Код уже хорошо спроектирован. Например, рефакторинг уже проводился, или с нуля был написан хорошо. В улучшениях ради улучшений никогда нет смысла.

Худшее, что вы можете сделать — это начать делать рефакторинг, не понимая зачем. Так вы просто потратите деньги и время зря.

Рефакторинг и проектирование

Рефакторинг и проектирование (дизайн-код) — тесно связанные понятия в разработке программного обеспечения, но с некоторым ключевыми отличиями.

Рефакторинг — это процесс улучшения дизайна существующего кода без изменения его внешнего поведения.

Проектирование — это процесс создания структуры и организации кода с нуля.

Из-за этого основного отличия, рефакторинг проводят после проектирования. Другими словами, результат проектирования — это код, который используется для последующего рефакторинга. Но это не означает, что на этом все заканчивается.

Рефакторинг — это непрерывный процесс, который должен выполняться постоянно на протяжении всего цикла разработки, а не только как разовое мероприятие.

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

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

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

Рефакторинг и производительность

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

Тем не менее, бывают случаи, когда рефакторинг вообще не влиять не производительность. Давайте разберемся, как это работает:

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

НО:

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

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

Разработка тестов

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

Разберем подробнее, когда и как проводят тесты.

Перед рефакторингом

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

Пример:

Допустим, у вас есть функция на Python, которая изменяет размер изображения с помощью библиотеки Pillow:

from PIL import Image

def resize_image(input_path, output_path, max_size):
    with Image.open(input_path) as img:
        width, height = img.size
        if width > max_size or height > max_size:
            ratio = min(max_size / width, max_size / height)
            new_size = (int(width * ratio), int(height * ratio))
            img = img.resize(new_size)
        img.save(output_path)

Чтобы протестировать эту функцию, вы можете написать простой скрипт, который (1) создает файл изображения нужного размера, (2) вызывает функцию resize_image для уменьшения размера изображения, а затем (3) проверяет правильность размеров выходного файла:

import os
import tempfile

def test_resize_image():
    input_file = tempfile.NamedTemporaryFile(suffix='.jpg')
    output_file = tempfile.NamedTemporaryFile(suffix='.jpg')
    img = Image.new('RGB', (1000, 1000))
    img.save(input_file.name)
    resize_image(input_file.name, output_file.name, 500)
    assert os.path.exists(output_file.name)
    with Image.open(output_file.name) as img:
        width, height = img.size
        assert width <= 500 and height <= 500

В этом примере функция test_resize_image создает временный входной файл с использованием модуля tempfile с размером 1000×1000 пикселей. Затем он вызывает функцию resize_image, чтобы изменить размер изображения до максимального размера 500 пикселей, и проверяет, что выходной файл существует и имеет размеры, меньшие или равные 500×500 пикселей.

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

Во время рефакторинга

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

После рефакторинга

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

Если регулярное тестирование проводилось достаточно тщательно, проверка после рефакторинга не должна обнаружить проблемы.

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

Проблемы рефакторинга

Хотя у рефакторинга много преимуществ, он также может привести к возникновению определенных проблем:

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

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

Тестирование и документирование должны быть главным приоритетом на протяжении всего процесса, а изменения нужно вносить постепенно и с прицелом на удобство сопровождения и удобочитаемость.

Методы рефакторинга

В заключение поделимся с вами некоторыми эффективными методами рефакторинга:

  1. Метод извлечения. Включает извлечение раздела кода в отдельную функцию. Код становится более читабельным и удобным.
  2. Встроенный метод. Включает удаление функции и размещение ее кода непосредственно в вызывающей функции. Код становится проще и эффективнее.
  3. Извлечение переменной. Включает создание новой переменной для хранения значения, которое используется несколько раз в разделе кода. Упрощается чтение и обслуживание кода.
  4. Встроенная переменная. Включает замену переменной ее значением в коде. Упрощает код и повышает его производительность.
  5. Метод перемещения. Включает перемещение функции из одного класса в другой. Улучшает организацию и структуру кода.
  6. Метод переименования. Включает переименование функции или переменной, чтобы лучше отразить ее назначение и функциональность. Это может сделать код более понятным и удобным для работы.
  7. Введение объекта параметра. Включает создание нового объекта для хранения группы связанных параметров, которые передаются методу. Делает код более гибким и простым в обслуживании.

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

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023