Код стареет. Это факт. То, что три года назад казалось элегантным решением, сегодня выглядит как спагетти, которые невозможно размотать. И в какой-то момент ты понимаешь: дальше так жить нельзя. Надо делать рефакторинг.
Но как сделать рефакторинг кода так, чтобы не уйти в него на полгода и не положить продакшен? Разберём по шагам.
Что такое рефакторинг и зачем он нужен
Рефакторинг — это изменение внутренней структуры кода без изменения его внешнего поведения. Ключевое слово: без изменения поведения. Пользователь не должен заметить, что вы что-то переделали.
Зачем это нужно? Не ради красивого кода, если что. Рефакторинг нужен, когда:
- Добавить новую фичу сложнее, чем написать её с нуля
- Баги плодятся быстрее, чем вы их фиксите
- Каждый раз, открывая файл, вы тратите 15 минут на понимание, что там происходит
- Новые разработчики в команде впадают в депрессию на код-ревью
По моему опыту, большинство команд откладывают рефакторинг до последнего. А потом тратят месяцы на переписывание. На одном проекте мы три года терпели легаси, а потом потратили восемь месяцев на полный рефакторинг. Если бы делали постепенно — заняло бы меньше времени и с меньшим риском.
Когда рефакторинг действительно нужен
Не каждый «некрасивый» код требует рефакторинга. Есть правило трёх, которое вывел Мартин Фаулер: рефакторьте, когда замечаете паттерн проблемы в третий раз. Первый раз — просто напишите код. Второй — отметьте сходство. Третий — выделяйте общую логику.
Но есть явные сигналы, которые называют code smells. Это симптомы проблем в коде.
Длинный метод. Если метод не помещается на один экран — это проблема. Такой код сложно понять и тестировать. Честно? Я стараюсь держать методы до 10-15 строк. Звучит экстремально, но работает.
Дублирование кода. Два похожих куска — плохо. Три — катастрофа. Однажды я видел 40 копий одной и той же функции с минимальными изменениями. Править баг в таком «коде» — особый вид боли.
Большой класс. Класс на 500 строк делает слишком много всего. Нарушает принцип единственной ответственности. Такие классы превращаются в бабушкин сундук, где лежит всё подряд.
Длинный список параметров. Функция с 7 аргументами — это крик о помощи. Значит, она делает слишком много или ей не хватает собственного объекта-параметра.
Feature envy. Метод одного класса слишком интересуется данными другого класса. Нужен рефакторинг — возможно, метод живёт не там, где должен.
Методы рефакторинга: что делать с code smells
Когда вы обнаружили проблему, её надо исправить. Есть конкретные техники рефакторинга.
Extract Method — самая частая техника. Выделяете часть кода в отдельный метод с понятным названием. Было так:
def process_order(order):
# валидация
if not order.items:
raise ValueError("Empty order")
if order.total < 0:
raise ValueError("Negative total")
# расчёт скидки
discount = 0
if order.customer.is_vip:
discount = order.total * 0.1
if order.total > 10000:
discount = max(discount, order.total * 0.05)
# сохранение
order.discount = discount
order.final_total = order.total - discount
db.save(order)
# уведомление
send_email(order.customer.email, "Order processed")
log_audit(order.id, "processed")
Стало так:
def process_order(order):
validate_order(order)
discount = calculate_discount(order)
apply_discount(order, discount)
finalize_order(order)
def validate_order(order):
if not order.items:
raise ValueError("Empty order")
if order.total < 0:
raise ValueError("Negative total")
def calculate_discount(order):
discount = 0
if order.customer.is_vip:
discount = order.total * 0.1
if order.total > 10000:
discount = max(discount, order.total * 0.05)
return discount
def apply_discount(order, discount):
order.discount = discount
order.final_total = order.total - discount
def finalize_order(order):
db.save(order)
send_email(order.customer.email, "Order processed")
log_audit(order.id, "processed")
Каждый метод делает одно дело. Названия говорят сами за себя. Теперь легко тестировать каждый шаг отдельно.
Rename Variable/Method — переименование. Звучит банально, но это половина успеха. Переменная x или метод process ни о чём не говорят. А discounted_total или apply_vip_discount — уже история.
Replace Magic Numbers with Constants. Что такое 0.1 в коде выше? Скидка для VIP? А почему 10%? А что если изменится?
VIP_DISCOUNT_RATE = 0.10
LARGE_ORDER_THRESHOLD = 10000
LARGE_ORDER_DISCOUNT_RATE = 0.05
def calculate_discount(order):
discount = 0
if order.customer.is_vip:
discount = order.total * VIP_DISCOUNT_RATE
if order.total > LARGE_ORDER_THRESHOLD:
discount = max(discount, order.total * LARGE_ORDER_DISCOUNT_RATE)
return discount
Теперь код сам документирует себя. И changing business requirements меняются в одном месте.
Extract Class — когда класс делает слишком много. На одном проекте был класс OrderManager на 2000 строк. Он валидировал, считал, сохранял, отправлял письма, генерировал отчёты. Мы разделили его на пять классов: OrderValidator, OrderCalculator, OrderRepository, OrderNotifier, OrderReporter. Каждый отвечает за свою область. Тесты стали в разы проще.
Как не сломать всё при рефакторинге
Вот мы подошли к главному. Рефакторинг опасен. Можно изменить поведение кода так, что вы этого не заметите. Как этого избежать?
Тесты — ваша страховка. Нет тестов — нет рефакторинга. Ну, можно, но на свой страх и риск. По-хорошему, перед рефакторингом надо написать тесты на текущее поведение. Даже если код ужасен. Характеризационные тесты — они описывают, как код работает прямо сейчас, а не как должен.
def test_calculate_discount_for_vip():
order = Order(total=1000, customer=Customer(is_vip=True))
assert calculate_discount(order) == 100
def test_calculate_discount_for_large_order():
order = Order(total=15000, customer=Customer(is_vip=False))
assert calculate_discount(order) == 750
Маленькие шаги. Не переписывайте всё сразу. Один рефакторинг за раз. Выделили метод — закоммитили. Переименовали переменную — закоммитили. Переименовали ещё одну — закоммитили. Каждое изменение должно быть атомарным.
Запускайте тесты после каждого шага. Упали — сразу понятно, где накосячили. Прошли — можно двигаться дальше.
Используйте IDE. Современные IDE умеют делать рефакторинг безопасно. Rename в PyCharm или VS Code переименует переменную везде, где она используется. Extract Method выделит код и корректно обработает параметры. Это надёжнее, чем руками править.
Code review. Даже если вы опытный разработчик, вторая пара глаз не помешает. Рефакторинг может быть коварным — вроде выглядит нормально, а потом находишь граничный кейс, который сломался.
Автоматический рефакторинг кода
Часть рефакторинга можно автоматизировать. IDE умеют делать базовые преобразования. Линтеры находят проблемы. Форматтеры приводят код к единому стилю.
Но есть уровень выше — AI-инструменты. Они могут анализировать код и предлагать улучшения. Не просто «переименуй переменную», а «этот метод слишком длинный, вот как его можно разбить».
На одном проекте мы подключили автоматический анализ кода в CI/CD pipeline. Теперь каждый merge request проверяется на code smells. Это не заменяет полноценный рефакторинг, но не даёт коду деградировать дальше. Мелкие проблемы ловятся сразу, крупные — по крайней мере отмечаются.
Автоматический рефакторинг кода особенно полезен для:
- Выявления дублирования
- Предложения оптимальных имён
- Поиска неиспользуемого кода
- Обнаружения потенциальных багов
Правда, слепо доверять AI тоже не стоит. Он может предложить изменение, которое выглядит логично, но ломает бизнес-логику. Всегда проверяйте предложения.
Практический пример: рефакторинг реального кода
Вот кусок кода, который я встретил в реальном проекте. С небольшими упрощениями:
def get_user_data(user_id):
conn = psycopg2.connect("dbname=prod user=admin password=secret")
cur = conn.cursor()
cur.execute("SELECT * FROM users WHERE id = %s" % user_id)
user = cur.fetchone()
if user:
data = {
'name': user[1],
'email': user[2],
'age': user[3] if user[3] else 'N/A',
'status': 'active' if user[4] else 'inactive',
'last_login': user[5].strftime('%Y-%m-%d') if user[5] else 'never'
}
cur.execute("SELECT * FROM orders WHERE user_id = %s" % user_id)
orders = cur.fetchall()
total = 0
for o in orders:
total += o[3]
data['total_spent'] = total
data['order_count'] = len(orders)
return data
else:
return None
cur.close()
conn.close()
Вижу проблемы? Их много. SQL-инъекция через строковую интерполяцию. Хардкод подключения к базе. Магические индексы user[1], user[2]. Недостижимый код после return. Нет обработки исключений. Нет закрытия соединений в случае ошибки.
Отрефакторим:
from dataclasses import dataclass
from datetime import datetime
from contextlib import contextmanager
@dataclass
class UserData:
name: str
email: str
age: int | str
status: str
last_login: str
total_spent: float
order_count: int
@contextmanager
def get_db_connection():
conn = psycopg2.connect(os.environ['DATABASE_URL'])
try:
yield conn
finally:
conn.close()
def fetch_user_by_id(conn, user_id: int) -> dict | None:
with conn.cursor() as cur:
cur.execute("SELECT name, email, age, is_active, last_login FROM users WHERE id = %s", (user_id,))
return cur.fetchone()
def fetch_user_orders(conn, user_id: int) -> list:
with conn.cursor() as cur:
cur.execute("SELECT total FROM orders WHERE user_id = %s", (user_id,))
return cur.fetchall()
def format_user_data(user_row: tuple, orders: list) -> UserData:
name, email, age, is_active, last_login = user_row
return UserData(
name=name,
email=email,
age=age if age else 'N/A',
status='active' if is_active else 'inactive',
last_login=last_login.strftime('%Y-%m-%d') if last_login else 'never',
total_spent=sum(order[0] for order in orders),
order_count=len(orders)
)
def get_user_data(user_id: int) -> UserData | None:
with get_db_connection() as conn:
user_row = fetch_user_by_id(conn, user_id)
if not user_row:
return None
orders = fetch_user_orders(conn, user_id)
return format_user_data(user_row, orders)
Что изменилось:
- Параметризованные запросы — защита от SQL-инъекций
- Контекстный менеджер для соединений — гарантированное закрытие
- Dataclass вместо словаря — типобезопасность
- Маленькие функции с единственной ответственностью
- Явные типы через аннотации
- Конфигурация через переменные окружения
- Распаковка по именам вместо магических индексов
Код стал длиннее. Но он надёжнее, тестируется и понимается быстрее.
Когда рефакторинг — плохая идея
Иногда рефакторинг делать не надо. Да, серьёзно.
Сроки горят, релиз через неделю. Рефакторинг можно сделать после. Наверное. Хотя если код настолько плох, что тормозит разработку — возможно, стоит выделить время.
Код работает, его никто не трогает, и не планируется. Пусть живёт. Не чините то, что не ломается.
Нет тестов и нет времени их написать. Тогда риск слишком велик. Лучше оставить как есть и добавить тесты постепенно.
Команда не понимает, зачем это нужно. Рефакторинг ради рефакторинга — путь в никуда. Команда должна видеть ценность.
Заключение
Рефакторинг — это инвестиция. Вы тратите время сейчас, чтобы экономить его потом. Код становится понятнее, баги находятся быстрее, новые фичи реализуются проще.
Главное — делайте это правильно. Тесты, маленькие шаги, частые коммиты. И используйте инструменты: IDE для безопасных преобразований, линтеры для поиска проблем, AI-помощники для анализа кода.
Кстати, про AI-помощники. Мы в Distiq как раз делаем такой — он анализирует merge request'ы и находит code smells, потенциальные баги и места для рефакторинга. Интегрируется в GitLab, GitHub и GitVerse за пару минут. Не то чтобы я прям сильно рекомендую, но если хотите автоматизировать часть рутинной работы — попробуйте. Бесплатно работает для небольших команд.
