Рефакторинг — это не то, что ты делаешь в выходной, когда скучно. Это систематическая работа, которая либо спасает проект, либо превращает его в болото. Я видел оба варианта. На одном проекте мы потратили три месяца на рефакторинг legacy-кода и ускорили разработку новых фич в два раза. На другом — попытались "немного переделать" и месяц ловили баги в продакшене.
Давай разберёмся, как делать рефакторинг правильно.
Чистый код — это не эстетика, это экономика
Для начала честный разговор. Почему вообще рефакторинг? Потому что грязный код стоит денег. Много денег.
Когда я работал в стартапе, наш backend был написан на Node.js с копипастой функций в каждом роуте. Параллельно занимался рефакторингом, вынес логику в сервисы, избавился от дублирования. За три недели:
- Время на фиксинг багов упало с 8 часов в день на 2
- Новые джуниоры стали разбираться в коде за неделю, а не за месяц
- Мы добавили две новые фичи вместо того, чтобы заниматься дебагингом
Это не красиво. Это просто выгодно.
Чистый код — это код, который:
- Легко понять за разумное время
- Легко модифицировать без страха
- Не содержит повторяющихся кусков
- Делает одно и делает хорошо
Вот и всё. Остальное — детали.
Code smells: как узнать, что код уже грязный
Есть такая штука — code smells. Это не ошибки и не баги. Это сигналы, что что-то не так. Запах, если угодно. Ты не видишь проблему с первого взгляда, но чувствуешь.
Вот самые частые:
Дублирование (DRY violation)
# Плохо
def calculate_user_tax():
income = get_income()
expenses = get_expenses()
taxable = income - expenses
tax = taxable * 0.13
return tax
def calculate_company_tax():
income = get_company_income()
expenses = get_company_expenses()
taxable = income - expenses
tax = taxable * 0.15
return tax
Видишь паттерн? Логика расчёта одна и та же, только коэффициент разный. Джуниор это не видит. Сеньор это чует на запах.
# Хорошо
def calculate_tax(income, expenses, tax_rate):
taxable = income - expenses
return taxable * tax_rate
user_tax = calculate_tax(get_income(), get_expenses(), 0.13)
company_tax = calculate_tax(get_company_income(), get_company_expenses(), 0.15)
Длинные функции (God Function)
Функция, которая делает три дела — это не функция, это сценарий. По моему опыту, если функция больше 20-30 строк, пора её дробить.
# Плохо — 50 строк в одной функции
def process_order(order_id):
order = db.get_order(order_id)
# Валидация
if not order.items:
raise ValueError("No items")
# ещё 10 строк валидации
# Расчёты
total = sum(item.price * item.qty for item in order.items)
# ещё 15 строк расчётов
# Отправка
email.send(order.customer_email, f"Order {order_id} confirmed")
# ещё 10 строк отправки
# Логирование
logger.info(f"Order {order_id} processed")
# ещё логирование
# Хорошо — разбито на части
def process_order(order_id):
order = db.get_order(order_id)
validate_order(order)
total = calculate_order_total(order)
notify_customer(order, total)
log_order_processing(order_id)
Длинные параметры (Long Parameter List)
Если функция принимает 5+ параметров — это звонок. Обычно это значит, что нужен объект.
# Плохо
def create_invoice(customer_id, customer_name, customer_email,
amount, currency, date, description):
pass
# Хорошо
def create_invoice(customer: Customer, invoice_data: InvoiceData):
pass
Магические числа
# Плохо
if user.age > 18 and user.experience_years > 2:
salary = user.base_salary * 1.25
# Хорошо
MIN_AGE_FOR_RAISE = 18
MIN_EXPERIENCE_FOR_RAISE = 2
SENIOR_MULTIPLIER = 1.25
if user.age > MIN_AGE_FOR_RAISE and user.experience_years > MIN_EXPERIENCE_FOR_RAISE:
salary = user.base_salary * SENIOR_MULTIPLIER
Вложенность (Nested Conditionals)
# Плохо
if user:
if user.is_active:
if user.has_permission('edit'):
if document.owner_id == user.id:
return True
# Хорошо (ранние выходы)
if not user:
return False
if not user.is_active:
return False
if not user.has_permission('edit'):
return False
if document.owner_id != user.id:
return False
return True
Это работает лучше. Меньше нестинга, проще читать.
Как анализировать код: инструменты и методы
Ручной анализ — это медленно и ненадёжно. Нужны инструменты.
Статические анализаторы
Для Python — Pylint, Flake8, Black. Для JavaScript — ESLint. Они ловят половину проблем автоматически.
# Запуск Flake8
flake8 app/ --max-line-length=120
# Запуск Black (автоматический форматер)
black app/
Метрики кода
Смотри на эти цифры:
- Cyclomatic complexity (сложность по Маккейбу) — сколько путей в функции. Больше 10 — уже плохо
- LOC (lines of code) — просто считай строки
- Дублирование — какой процент кода повторяется
Для Python есть radon:
radon cc app/ -a # Cyclomatic complexity
radon mi app/ # Maintainability Index
Code review как анализ
Честно? Самый эффективный способ — это когда код смотрит человек. Автоматика ловит синтаксис, а человек видит логику.
На Distiq мы как раз автоматизировали эту часть. AI-бот анализирует каждый PR и выловит те code smells, которые я описал выше. Плюс найдёт потенциальные баги и уязвимости. Это не заменит code review, но сильно его ускорит.
Методы рефакторинга: пошаговая инструкция
Рефакторинг без плана — это путь к боли. Нужна стратегия.
Метод 1: Микро-рефакторинг (маленькие шаги)
Это самый безопасный способ. Ты меняешь маленький кусок, прогоняешь тесты, коммитишь. Потом следующий.
# Шаг 1: Переименовать переменную
result = process_data(x) # Было
result = process_data(user_input) # Стало
# Шаг 2: Вынести магическое число
if len(user_input) > 255: # Было
MAX_INPUT_LENGTH = 255
if len(user_input) > MAX_INPUT_LENGTH: # Стало
# Шаг 3: Разбить функцию
def validate_and_process(): # Было
validate(user_input) # Новое
process(user_input) # Новое
Каждый шаг — зелёные тесты. Спокойно спишь ночью.
Метод 2: Extract Method
Самый частый рефакторинг. Вынимаешь кусок логики в отдельную функцию.
# Было
def create_user(data):
user = User(data['name'], data['email'])
user.created_at = datetime.now()
user.updated_at = datetime.now()
db.session.add(user)
db.session.commit()
logger.info(f"User created: {user.id}")
return user
# Стало
def create_user(data):
user = _build_user(data)
_save_user(user)
_log_user_creation(user)
return user
def _build_user(data):
user = User(data['name'], data['email'])
user.created_at = datetime.now()
user.updated_at = datetime.now()
return user
def _save_user(user):
db.session.add(user)
db.session.commit()
def _log_user_creation(user):
logger.info(f"User created: {user.id}")
Теперь каждая функция делает одно. Тестировать проще. Переиспользовать проще.
Метод 3: Replace Magic Number with Named Constant
# Было
if user.failed_attempts >= 5:
user.is_locked = True
# Стало
MAX_FAILED_LOGIN_ATTEMPTS = 5
if user.failed_attempts >= MAX_FAILED_LOGIN_ATTEMPTS:
user.is_locked = True
Просто? Да. Работает? Да.
Метод 4: Consolidate Duplicate Code
Если видишь, что один и тот же код повторяется в разных местах — это нужно объединить. Я это проверял на пяти проектах: 80% багов потом вводились именно в эти дублированные куски.
# Было (в разных местах)
def get_active_users():
users = User.query.filter(User.is_active == True).all()
return users
def get_active_admins():
admins = Admin.query.filter(Admin.is_active == True).all()
return admins
# Стало
def get_active_entities(model):
return model.query.filter(model.is_active == True).all()
get_active_users = lambda: get_active_entities(User)
get_active_admins = lambda: get_active_entities(Admin)
Метод 5: Introduce Parameter Object
Когда функция принимает кучу параметров — собери их в объект.
# Было
def create_order(customer_id, customer_name, customer_email,
items, total, tax, shipping_address):
pass
# Стало
class OrderData:
def __init__(self, customer, items, total, tax, shipping_address):
self.customer = customer
self.items = items
self.total = total
self.tax = tax
self.shipping_address = shipping_address
def create_order(order_data: OrderData):
pass
Как не сломать всё при рефакторинге
Это самый важный раздел. Я видел, как хороший рефакторинг превращался в кошмар.
Правило 1: Тесты должны быть перед рефакторингом
Если у тебя нет тестов — сначала напиши тесты, потом рефакти. Иначе ты не поймёшь, что сломал.
# Сначала напиши тест
def test_calculate_user_tax():
income = 100000
expenses = 20000
tax = calculate_tax(income, expenses, 0.13)
assert tax == 10400 # (100000 - 20000) * 0.13
# Потом безопасно рефакти функцию
Правило 2: Маленькие коммиты
Не делай огромный PR на 500 строк. Делай 10 маленьких. Легче ревьюить, легче откатывать, если что.
git commit -m "Extract _validate_order to separate function"
git commit -m "Extract _calculate_total to separate function"
git commit -m "Rename variable 'tmp' to 'temporary_result'"
Правило 3: CI/CD должен быть зелёным
Каждый коммит — зелёные тесты, зелёный линтер, зелёный type checker (если Python/TypeScript).
# .github/workflows/ci.yml
- name: Run tests
run: pytest --cov
- name: Run linter
run: flake8 app/
- name: Type checking
run: mypy app/
Правило 4: Отделяй рефакторинг от функционала
Не рефакти и не добавляй фичу в одном PR. Это убивает ревьюер. Делай два PR.
PR #1: Refactor: Extract _calculate_total function (без изменения поведения)
PR #2: Feature: Add discount logic (использует новую функцию из PR #1)
Правило 5: Мониторь метрики после релиза
Рефакторинг может скрыть проблему производительности. После релиза смотри на:
- Время ответа API
- CPU/память
- Error rate
- Медленные запросы
Если что-то подскочило — откатывай.
Практический пример: рефакторинг реального кода
Давай я покажу реальный пример. Код из боевого проекта (немного упрощённый).
Было (грязный код):
def process_payment(payment_id, user_id, amount, currency, card_token):
# Валидация
if not payment_id or not user_id or not amount:
raise ValueError("Invalid input")
# Получение платежа
p = db.get_payment(payment_id)
if not p:
raise ValueError("Payment not found")
# Проверка пользователя
u = db.get_user(user_id)
if not
