Рефакторинг8 мин чтения2026-03-06

Как сделать рефакторинг кода и не сломать продакшен

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

Код стареет. Это факт. То, что три года назад казалось элегантным решением, сегодня выглядит как спагетти, которые невозможно размотать. И в какой-то момент ты понимаешь: дальше так жить нельзя. Надо делать рефакторинг.

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

Что такое рефакторинг и зачем он нужен

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

Зачем это нужно? Не ради красивого кода, если что. Рефакторинг нужен, когда:

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

Когда рефакторинг действительно нужен

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

Но есть явные сигналы, которые называют 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)

Что изменилось:

Код стал длиннее. Но он надёжнее, тестируется и понимается быстрее.

Когда рефакторинг — плохая идея

Иногда рефакторинг делать не надо. Да, серьёзно.

Сроки горят, релиз через неделю. Рефакторинг можно сделать после. Наверное. Хотя если код настолько плох, что тормозит разработку — возможно, стоит выделить время.

Код работает, его никто не трогает, и не планируется. Пусть живёт. Не чините то, что не ломается.

Нет тестов и нет времени их написать. Тогда риск слишком велик. Лучше оставить как есть и добавить тесты постепенно.

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

Заключение

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

Главное — делайте это правильно. Тесты, маленькие шаги, частые коммиты. И используйте инструменты: IDE для безопасных преобразований, линтеры для поиска проблем, AI-помощники для анализа кода.

Кстати, про AI-помощники. Мы в Distiq как раз делаем такой — он анализирует merge request'ы и находит code smells, потенциальные баги и места для рефакторинга. Интегрируется в GitLab, GitHub и GitVerse за пару минут. Не то чтобы я прям сильно рекомендую, но если хотите автоматизировать часть рутинной работы — попробуйте. Бесплатно работает для небольших команд.

Попробуйте Distiq для автоматического code review

AI-бот анализирует каждый MR/PR и оставляет комментарии с замечаниями. Интеграция за 2 минуты.

Попробовать бесплатно

Похожие статьи