Рефакторинг программного обеспечения — это изменение внутренней структуры программы без изменения ее внешнего поведения. Цель — сделать код понятнее и дешевле в изменении: после рефакторинга приложение делает то же самое, но читать и дорабатывать его проще.
Содержание
Ниже — чем рефакторинг отличается от доработки, переписывания и оптимизации, по каким признакам понять, что он нужен, и полный пример на 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.
- Запустить тесты и получить успешный результат до первой правки.
- Сделать одно небольшое преобразование: переименовать, выделить функцию, убрать дубль.
- Запустить тесты. Прошли — зафиксировать шаг коммитом. Упали — отменить шаг и сделать его меньше.
- Повторять пункты 3-4, не добавляя новых функций и не исправляя попутно найденные ошибки: их записывают и делают отдельным изменением.
Переименование и выделение функции надежнее выполнять средствами IDE, а не поиском и заменой по тексту: инструмент меняет все обращения по структуре кода. Гарантии при этом не абсолютные — обращения по имени в строках, в шаблонах и через рефлексию инструмент может пропустить, так что тесты после автоматического шага тоже запускают.
В разработке через тестирование тот же прием встроен в цикл «красный — зеленый — рефакторинг»: сначала пишут падающий тест, затем минимальный код, с которым он проходит, и после этого улучшают структуру, пока тесты остаются зелеными.
Выводы
- Рефакторинг — изменение внутренней структуры программы без изменения внешнего поведения; цель — понятный и удобный для изменений код.
- От доработки он отличается тем, что пользователь не получает ничего нового, от оптимизации — целью, от переписывания — размером шага и тем, что рабочая версия есть в любой момент.
- Поводом служат запахи кода: непонятные имена, длинные функции, дублирование, магические числа, цепочки однотипных условий.
- Страховка — тесты, написанные до правок и проходящие без изменений после; особенно важны граничные значения.
- Тесты подтверждают поведение только на проверенных входах, поэтому сначала определяют контракт функции, затем покрывают его.
Где применяется / связь с практикой
Рефакторинг — ежедневная часть работы разработчика: им готовят код к новой функции, разбирают унаследованные модули, сокращают время на ревью. На уровне отдельной функции достаточно приемов из этой статьи, а на уровне модулей и сервисов те же вопросы решаются шаблонами проектирования и принципами вроде SOLID — они подсказывают, к какой структуре стоит прийти.
Освойте тему на практике
Системно эта тема разбирается на курсе Архитектура и шаблоны проектирования. Познакомиться с форматом и преподавателями можно на открытых уроках Otus.
FAQ
Можно ли проводить рефакторинг, если тестов нет совсем?
Можно, но риск выше. Безопаснее сначала покрыть изменяемый участок характеризационными тестами, а до этого ограничиться автоматическими преобразованиями IDE и самыми мелкими шагами.
Что такое технический долг и как он связан с рефакторингом?
Технический долг — накопленные упрощения в структуре кода, из-за которых каждое следующее изменение обходится дороже. Рефакторинг — основной способ этот долг уменьшать, обычно небольшими частями в ходе текущих задач.
Как объяснить заказчику время на рефакторинг, если функций не прибавилось?
Привязать его к конкретной задаче: показать, какую доработку он упрощает и на каком участке до этого чаще всего появлялись ошибки. Рефакторинг «вообще», без связи с планируемыми изменениями, обосновать трудно, и часто он действительно не нужен.



