Рефакторинг кода: что это такое, когда нужен и как проводить без поломок

Рефакторинг кода: что это такое, когда нужен и как проводить без поломок Полезное

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

Ниже — чем рефакторинг отличается от доработки, переписывания и оптимизации, по каким признакам понять, что он нужен, и полный пример на Python: код до, тесты-страховка, код после и ошибка, которую эти тесты поймали. Примеры проверены на Python 3.14 30 сентября 2026 года.

Что именно не должно меняться

Определение восходит к книге Мартина Фаулера «Рефакторинг» (первое издание — 1999 год, второе — 2018). В нем два условия, и оба обязательны.

Внутренняя структура — то, как код устроен: имена, разбиение на функции и классы, порядок и способ вычислений. Ее меняем.

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

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

Чем рефакторинг отличается от доработки, переписывания и оптимизации

Эти работы часто идут рядом и в разговоре сливаются в одно слово. Различает их ответ на два вопроса: меняется ли поведение и что является целью.

Работа Поведение для пользователя Цель Чем проверяется
Рефакторинг не меняется понятность и простота изменений прежние тесты проходят без правок
Доработка приложения меняется: новая функция или правило польза для пользователя новые тесты на новое поведение
Исправление ошибки меняется: неверный результат становится верным корректность тест, который раньше падал
Оптимизация результат тот же скорость, память замеры до и после
Переписывание с нуля должно повториться, но гарантий нет замена кода целиком полная повторная проверка

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

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

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

Когда нужен рефакторинг: запахи кода

Запах кода (code smell) — признак в исходном тексте, который подсказывает, что структуру стоит пересмотреть. Это повод присмотреться, а не доказательство ошибки: программа с запахами может работать верно.

Запах Как выглядит Типовой прием
Непонятные имена s, d, calc, data2 переименование
Длинная функция считает, проверяет и форматирует в одном месте выделение функции
Дублирование одинаковые фрагменты в нескольких местах вынос в общую функцию
Магические числа 3000, 0.2 без пояснения именованная константа
Цепочка однотипных if ветка на каждый тип или код таблица соответствий, полиморфизм
Комментарий-заголовок внутри функции # скидка, # доставка выделение функции с таким именем
Мертвый код неиспользуемые переменные и функции удаление

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

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

Отдельно стоит форматирование. Отступы, пробелы и кавычки расставляют автоматические форматтеры (Prettier для JavaScript, Ruff или Black для Python), а линтеры вроде ESLint находят неиспользуемые переменные. Это полезная гигиена, но структуру программы она не трогает, поэтому рефакторинг не заменяет.

Пример: рефакторинг функции на Python

Один пример пройдет через все шаги. Функция считает сумму заказа в целых рублях: стоимость товаров, скидка по типу клиента (1 — 10 %, 2 — 20 %), доставка 300 рублей, а от 3000 рублей — бесплатно.

Код до

Файл order_before.py:

def calc(o, t):
    s = 0
    for i in o:
        s = s + i[1] * i[2]
    # скидка
    if t == 1:
        s = s - s * 10 // 100
    if t == 2:
        s = s - s * 20 // 100
    # доставка
    if s >= 3000:
        d = 0
    else:
        d = 300
    return s + d

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

Шаг 1. Тесты как страховка

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

Файл test_order.py:

import unittest

from order_before import calc as order_total

CASES = [
    # (товары: название, цена, количество), тип клиента, ожидаемая сумма
    ([], 0, 300),
    ([("книга", 500, 2)], 0, 1300),
    ([("книга", 500, 2)], 1, 1200),
    ([("книга", 500, 2)], 2, 1100),
    ([("курс", 2999, 1)], 0, 3299),
    ([("курс", 3000, 1)], 0, 3000),
    ([("курс", 3000, 1)], 1, 3000),
    ([("курс", 1999, 1), ("книга", 333, 3)], 2, 2699),
    ([("книга", 500, 2)], 7, 1300),
]


class OrderTotalTest(unittest.TestCase):
    def test_known_cases(self):
        for items, customer_type, expected in CASES:
            with self.subTest(items=items, customer_type=customer_type):
                self.assertEqual(order_total(items, customer_type), expected)


if __name__ == "__main__":
    unittest.main()

Запуск: python -m unittest test_order. Ожидаемый результат — одна точка и OK:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

В наборе есть пустой заказ, оба типа скидки, неизвестный тип клиента и пара значений вокруг порога бесплатной доставки: 2999 и 3000. Границы — место, где рефакторинг ломает поведение чаще всего.

Шаг 2. Код после

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

Файл order_after.py:

FREE_DELIVERY_FROM = 3000
DELIVERY_PRICE = 300
DISCOUNT_PERCENT = {1: 10, 2: 20}


def items_subtotal(items):
    return sum(price * quantity for _name, price, quantity in items)


def apply_discount(amount, customer_type):
    percent = DISCOUNT_PERCENT.get(customer_type, 0)
    return amount - amount * percent // 100


def delivery_cost(amount):
    return 0 if amount >= FREE_DELIVERY_FROM else DELIVERY_PRICE


def order_total(items, customer_type):
    amount = apply_discount(items_subtotal(items), customer_type)
    return amount + delivery_cost(amount)

В тесте меняется одна строка импорта: from order_after import order_total. Список CASES остается прежним — это и есть проверка того, что поведение не изменилось. Повторный запуск снова дает OK.

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

Ошибка, которую ловит страховка

Типичная оплошность при выделении функции — перепутать строгое и нестрогое сравнение.

Неверный вариант delivery_cost:

def delivery_cost(amount):
    return 0 if amount > FREE_DELIVERY_FROM else DELIVERY_PRICE

Фактический результат запуска тестов (начало вывода, без трассировки):

F
======================================================================
FAIL: test_known_cases (test_order.OrderTotalTest.test_known_cases) (items=[('курс', 3000, 1)], customer_type=0)
----------------------------------------------------------------------
AssertionError: 3300 != 3000

Заказ ровно на 3000 рублей стал стоить 3300: бесплатная доставка перестала действовать на границе. Исправление — вернуть >=, как в листинге order_after.py. Без строки с суммой 3000 в наборе CASES эта правка прошла бы незамеченной.

Граница примера

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

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

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

Как проводить рефакторинг: порядок действий

  1. Убедиться, что на изменяемый участок есть тесты. Если их нет — написать характеризационные, как в шаге 1.
  2. Запустить тесты и получить успешный результат до первой правки.
  3. Сделать одно небольшое преобразование: переименовать, выделить функцию, убрать дубль.
  4. Запустить тесты. Прошли — зафиксировать шаг коммитом. Упали — отменить шаг и сделать его меньше.
  5. Повторять пункты 3-4, не добавляя новых функций и не исправляя попутно найденные ошибки: их записывают и делают отдельным изменением.

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

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

Выводы

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

Где применяется / связь с практикой

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

Освойте тему на практике

Системно эта тема разбирается на курсе Архитектура и шаблоны проектирования. Познакомиться с форматом и преподавателями можно на открытых уроках Otus.

FAQ

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

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

Как объяснить заказчику время на рефакторинг, если функций не прибавилось?
Привязать его к конкретной задаче: показать, какую доработку он упрощает и на каком участке до этого чаще всего появлялись ошибки. Рефакторинг «вообще», без связи с планируемыми изменениями, обосновать трудно, и часто он действительно не нужен.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)