Рефакторинг — это когда ты переделываешь существующий код, не меняя его поведение. Звучит просто, но на практике это искусство. За 10 лет я видел, как рефакторинг спасал проекты от смерти и как он же их убивал. Разница — в подходе.
Начну с главного: рефакторинг это не оптимизация. Не добавление фич. Не переписывание на новый язык. Это улучшение качества существующего кода при сохранении функциональности. Если после рефакторинга приложение работает точно так же, но код стал понятнее, проще и дешевле в поддержке — это успех.
Когда рефакторинг становится необходимостью
По-хорошему, рефакторинг нужен постоянно. Маленькие порции. Но есть моменты, когда это становится критическим.
Я помню проект в одном стартапе, где функция обработки платежей была на 800 строк. Восьмиста! Локальные переменные перекрывали друг друга, логика была размазана по всему коду, и каждое изменение требовало полного понимания всей функции. Это был код, который никто не хотел трогать. Классический признак того, что нужен рефакторинг.
Вот типичные ситуации:
Код становится сложнее для чтения, чем для написания. Если новому разработчику нужна неделя, чтобы разобраться в модуле, который работает уже год — это сигнал.
Одна функция делает слишком много. Когда метод отвечает за валидацию, трансформацию данных, логирование и отправку в базу одновременно, это code smell.
Дублирование кода. Если один и тот же блок кода повторяется в трёх местах, нужно извлечь его в отдельный метод.
Сложно добавлять новые фичи. Когда каждое новое требование требует изменений в 5+ файлах, это знак того, что архитектура разъезжается.
Тесты пишутся с мучением. Если юнит-тесты требуют подготовки 15 моков и 40 строк setup кода, функция слишком связана с окружением.
На одном проекте я видел, как рефакторинг покупки это поняли только после того, как на добавление одного поля ушло два спринта вместо половины дня. Тогда все согласились: нужна переделка архитектуры.
Основные техники рефакторинга с примерами
Есть проверенные методы, которые работают независимо от языка и масштаба проекта.
Extract Method — извлечение метода
Самая простая и мощная техника. Берёшь кусок кода, который делает одно, и выносишь в отдельный метод.
# ДО: всё в одном методе
def process_order(order):
total = 0
for item in order['items']:
total += item['price'] * item['quantity']
if total > 1000:
total *= 0.9 # скидка 10%
tax = total * 0.18
final_price = total + tax
print(f"Order total: {final_price}")
return final_price
# ПОСЛЕ: логика разбита на понятные части
def calculate_subtotal(items):
return sum(item['price'] * item['quantity'] for item in items)
def apply_discount(amount):
if amount > 1000:
return amount * 0.9
return amount
def calculate_tax(amount):
return amount * 0.18
def process_order(order):
subtotal = calculate_subtotal(order['items'])
discounted = apply_discount(subtotal)
tax = calculate_tax(discounted)
final_price = discounted + tax
print(f"Order total: {final_price}")
return final_price
Разница видна сразу. Второй вариант легко тестировать, понять и модифицировать. Если завтра появится требование добавить прогрессивную скидку, ты просто меняешь функцию apply_discount в одном месте.
Rename для понятности
Иногда достаточно просто переименовать переменную или функцию.
# ДО: никакой понятности
def calc(d):
r = 0
for x in d:
r += x['p'] * x['q']
return r
# ПОСЛЕ: сразу ясно, что происходит
def calculate_order_total(items):
total = 0
for item in items:
total += item['price'] * item['quantity']
return total
Звучит мелко? Но честно: на 60% проблем с читаемостью кода я наталкивался именно на названия переменных. Одно слово — и код становится на порядок понятнее.
Remove Dead Code
Удаление неиспользуемого кода. Это не просто очистка, это снижение когнитивной нагрузки. Каждая лишняя строка — это что-то, что разработчик должен держать в голове.
# ДО: есть функция, которую никто не вызывает
def get_user_legacy(user_id): # Deprecated since 2021
# старая логика получения юзера
pass
def get_user(user_id): # новая версия
# новая логика
pass
# ПОСЛЕ: просто удаляем старое
def get_user(user_id):
# новая логика
pass
Replace Conditional with Polymorphism
Когда у тебя цепочка if-else, которая зависит от типа объекта, это явный кандидат на полиморфизм.
# ДО: один огромный if-else
class PaymentProcessor:
def process(self, payment):
if payment.method == 'card':
# логика для карт (15 строк)
elif payment.method == 'paypal':
# логика для PayPal (20 строк)
elif payment.method == 'crypto':
# логика для крипто (25 строк)
else:
raise ValueError("Unknown method")
# ПОСЛЕ: каждый метод сам знает, как себя обработать
class PaymentMethod:
def process(self):
pass
class CardPayment(PaymentMethod):
def process(self):
# логика для карт
pass
class PayPalPayment(PaymentMethod):
def process(self):
# логика для PayPal
pass
class PaymentProcessor:
def process(self, payment: PaymentMethod):
payment.process()
Второй вариант легче расширяется. Добавил новый способ оплаты? Просто создал новый класс, не трогая процессор.
Code smells: признаки того, что нужен рефакторинг
Code smell — это не баг. Это стиль кода, который намекает на проблему глубже. Вот что я ищу в первую очередь:
Long Method. Функция больше 30 строк — уже подозрительно. Больше 100 — точно проблема.
Large Class. Класс с 20+ методами обычно делает слишком много. Нужно разбить на несколько.
Duplicate Code. Самое очевидное. Если код повторяется, значит, его нужно абстрагировать.
Long Parameter List. Если функция требует 5+ параметров, это признак того, что она делает слишком много или плохо спроектирована.
Primitive Obsession. Когда ты везде используешь строки и числа вместо объектов. Например, передаёшь координаты как два отдельных числа вместо объекта Point.
Честно? Большинство команд, с которыми я работал, знали о code smells в теории, но не видели их в своем коде. Потому что когда пишешь функцию, она кажется логичной. Её нужно увидеть со стороны.
Как не сломать всё при рефакторинге
Это главная опасность. Рефакторинг должен быть безопасным. Вот как я к этому подхожу.
Сначала — тесты. Если у тебя нет тестов на код, который ты собираешься рефакторить, напиши их. Да, это добавит времени, но это страховка. Я видел, как рефакторинг без тестов вводил баги в production.
# Пишем тесты ДО рефакторинга
def test_calculate_order_total():
items = [
{'price': 100, 'quantity': 2},
{'price': 50, 'quantity': 3}
]
assert calculate_order_total(items) == 350
def test_apply_discount_large_order():
assert apply_discount(1500) == 1350
def test_no_discount_small_order():
assert apply_discount(500) == 500
После этого рефакторинг становится почти безопасным. Если ты что-то сломаешь, тесты сразу скажут.
Маленькие шаги. Не переписывай целый модуль за раз. Извлеки один метод, запусти тесты. Потом следующий. За день ты переделаешь больше и безопаснее, чем если потратишь неделю на полный рефакторинг.
Отдельная ветка в гите. Серьёзный рефакторинг должен быть в отдельной ветке. Так ты можешь экспериментировать, а потом просмотреть все изменения перед мёржем.
git checkout -b refactor/payment-processing
# Работаешь несколько дней
# Потом создаёшь MR и просматриваешь все изменения целиком
Code review для рефакторинга. Второй взгляд очень помогает. На одном проекте мы внедрили правило: любой рефакторинг должен быть ревьюирован. Это сэкономило нам массу проблем.
Запусти весь набор тестов. И интеграционные, и юнит-тесты, и smoke-тесты. Если тестовое покрытие хорошее (60%+), ты практически гарантирован от регрессии.
На одном проекте в Яндексе мы рефакторили сложный модуль поиска. Делали это в течение двух недель, маленькими шагами, с ревью каждого коммита. Результат: код сократился на 30%, стал на порядок проще, и ни одного бага в production. Потому что у нас было 150+ тестов, которые проверили каждый шаг.
Рефакторинг как часть workflow
Лучший рефакторинг — это тот, который происходит постоянно, небольшими порциями. Не отдельный проект, а часть каждого дня.
Я рекомендую такой подход: когда работаешь над фичей, потрати 15-20% времени на рефакторинг кода вокруг. Извлеки метод, переименуй переменную, удали дублирование. Это занимает мало времени, но накапливается. За месяц ты переделаешь порядочный кусок кода без больших перерывов на рефакторинг.
И вот тут помогает автоматизация. Когда в каждом MR ты видишь замечания про code smells, duplications, сложные функции — это напоминание. На одном проекте мы использовали анализатор кода, который флагировал потенциальные проблемы. Это снизило количество code smells на 40%.
Если говорить честно, большинство проблем с качеством кода можно предотвратить на этапе review. Когда коллега видит, что функция выросла до 100 строк, он это замечает и предлагает рефакторинг до мёржа. Вместо того, чтобы потом разбираться с монстром.
Рефакторинг — это не роскошь и не задача на потом. Это инвестиция в скорость разработки. Код, который чистый и понятный, дешевле менять, проще тестировать, быстрее разбирают новые разработчики.
Если ты хочешь автоматизировать поиск code smells и потенциальных проблем в рефакторинге, попробуй Distiq. Это AI-бот, который анализирует каждый MR в GitLab или GitHub и подсказывает, где можно улучшить код. Не все замечания будут верны (это AI), но 80% точно помогут тебе увидеть проблемы раньше, чем они станут дорогими.
