Рефакторинг — это не про переписывание всего с нуля. Это про улучшение внутренней структуры кода без изменения его поведения. Звучит просто? На деле это одна из самых сложных операций в разработке. Я видел команды, которые превращали рефакторинг в многомесячный кошмар, потому что не знали, как к нему подойти правильно.
По-хорошему, рефакторинг нужно делать постоянно. Маленькими шагами. А не накапливать техдолг годами, а потом пытаться переделать всё в спринт. Но об этом позже.
Что вообще такое рефакторинг и зачем он нужен
Рефакторинг — это преобразование кода, которое улучшает его качество, но не меняет функциональность. Ключевое слово тут "не меняет". Если ты переделываешь логику — это не рефакторинг, это развитие фичи или баг-фикс.
Зачем нужен? Если честно, от нехватки рефакторинга страдают все проекты после года-двух разработки. Код накапливает:
- Дублирование (один и тот же кусок скопирован в пять мест)
- Длинные функции (500 строк в одном методе — это норма для старых проектов)
- Неясные имена переменных (что такое
tmp,data,process_v2?) - Сложную логику там, где её можно упростить
- Нарушения принципов SOLID (когда один класс делает пять разных вещей)
Результат: разработчики тратят в два раза больше времени на понимание кода. Баги вылезают чаще. Новичков тяжелее онбордить. Скорость разработки падает линейно.
Рефакторинг замораживает это падение. Иногда даже разворачивает его вспять.
Code smells: как узнать, что пора рефакторить
"Code smell" — это не баг. Это признак, что с кодом что-то не так. Как запах из холодильника: может быть, там уже плесень, а может, просто скоро будет.
Вот самые частые:
Дублированный код (Duplicated Code)
Если один и тот же блок повторяется в трёх местах — это беда. На одном проекте я нашёл функцию валидации email-а, скопированную в восьми местах. Каждая слегка отличалась. Когда нужно было поправить регулярное выражение, пришлось менять в пяти местах (два раза забыли, потом на продакшене упали). Решение — извлечь в отдельную функцию.
# ДО: дублирование
def register_user(email, password):
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
return False
# ... регистрация
def update_user_email(user_id, email):
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
return False
# ... обновление
# ПОСЛЕ: извлекли в функцию
def is_valid_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def register_user(email, password):
if not is_valid_email(email):
return False
# ...
def update_user_email(user_id, email):
if not is_valid_email(email):
return False
# ...
Просто? Да. Но когда такого дублирования в проекте тысячи строк, это экономит часы отладки.
Длинные функции (Long Method)
Функция в 200 строк — это уже красный флаг. В 500 — это катастрофа. Я видел метод в старом коде, который парсил XML, валидировал данные, писал в базу, отправлял письмо и логировал результат. Всё в одной функции. Изменить хотя бы один шаг было страшно.
# ДО: монолит
def process_order(order_data):
# Парсим данные (30 строк)
items = json.loads(order_data)
# Валидируем (40 строк)
for item in items:
if 'price' not in item:
raise ValueError(...)
if item['price'] < 0:
raise ValueError(...)
# Считаем итого (20 строк)
total = sum(item['price'] * item['quantity'] for item in items)
# Пишем в БД (35 строк)
db.execute(...)
# Отправляем письмо (25 строк)
send_email(...)
# Логируем (20 строк)
logger.info(...)
return total
# ПОСЛЕ: разбили на функции
def process_order(order_data):
items = parse_order(order_data)
validate_items(items)
total = calculate_total(items)
save_to_db(items, total)
notify_user(items)
log_transaction(items, total)
return total
def parse_order(order_data):
return json.loads(order_data)
def validate_items(items):
for item in items:
if 'price' not in item or item['price'] < 0:
raise ValueError(f"Invalid item: {item}")
def calculate_total(items):
return sum(item['price'] * item['quantity'] for item in items)
# ... остальные функции
Теперь каждую функцию легко тестировать, менять, понимать.
Неясные имена (Unclear Naming)
Переменные типа x, tmp, data, result ничего не говорят. Я потратил два часа на понимание кода, потому что переменная называлась m. Оказалось, это "maximum". Не "money", не "message", не "map". Просто "m".
# ДО
def calc(arr):
m = 0
for i in arr:
if i > m:
m = i
return m
# ПОСЛЕ
def find_maximum_value(numbers):
maximum = 0
for number in numbers:
if number > maximum:
maximum = number
return maximum
Или даже лучше:
def find_maximum_value(numbers):
return max(numbers) if numbers else 0
Слишком много параметров (Long Parameter List)
Функция с десятью параметрами — это криптография. Каждый раз нужно помнить, в каком порядке их передавать.
# ДО: 8 параметров
def create_user(first_name, last_name, email, phone, address,
city, country, age):
# ...
# ПОСЛЕ: используем класс или словарь
class UserData:
def __init__(self, first_name, last_name, email, phone,
address, city, country, age):
self.first_name = first_name
self.last_name = last_name
# ... и т.д.
def create_user(user_data: UserData):
# ...
Божественные классы (God Class)
Класс, который делает всё: парсит данные, работает с БД, отправляет письма, логирует. Обычно это сервис-класс на 2000 строк.
Решение — разбить на несколько классов по ответственности (SOLID, Single Responsibility Principle).
Методы рефакторинга: как переделать код правильно
Есть несколько стандартных техник. Мартин Фаулер описал их в книге "Refactoring", и за 25 лет они не устарели.
Extract Method (Извлечение метода)
Самая частая. Берёшь кусок кода, выделяешь его в отдельную функцию.
# ДО
def generate_report(data):
# ... 50 строк подготовки данных
# ... 30 строк форматирования
# ... 20 строк записи в файл
pass
# ПОСЛЕ
def generate_report(data):
prepared_data = prepare_data(data)
formatted_output = format_output(prepared_data)
write_report(formatted_output)
def prepare_data(data):
# 50 строк
pass
def format_output(data):
# 30 строк
pass
def write_report(output):
# 20 строк
pass
Replace Magic Numbers with Named Constants
Числа без контекста — это яд. Что такое 86400? Что такое 0.15?
# ДО
def calculate_daily_earnings(hourly_rate, hours_worked):
tax_rate = 0.15
seconds_in_day = 86400
daily_rate = hourly_rate * 8
return (daily_rate * hours_worked - daily_rate * hours_worked * tax_rate)
# ПОСЛЕ
STANDARD_WORK_HOURS = 8
TAX_RATE = 0.15
SECONDS_IN_DAY = 86400
def calculate_daily_earnings(hourly_rate, hours_worked):
daily_rate = hourly_rate * STANDARD_WORK_HOURS
tax_amount = daily_rate * hours_worked * TAX_RATE
return daily_rate * hours_worked - tax_amount
Rename Variable/Function
Банально, но работает. Переименовать usr в user, calc в calculate_salary — и код становится читаемым в два раза лучше.
Remove Dead Code
Старый код, на который никто не ссылается. Комментарий вида # это больше не используется, но оставляю на всякий случай. Удалить. Это не история, это мусор. У тебя есть Git.
# ДО
def get_user(user_id):
# Старый способ (не используется)
# user = db.query("SELECT * FROM users WHERE id = " + str(user_id))
# Новый способ
user = db.query(User).filter(User.id == user_id).first()
return user
# ПОСЛЕ
def get_user(user_id):
return db.query(User).filter(User.id == user_id).first()
Move Method/Field
Если метод работает с данными другого класса, может, ему там и место?
# ДО: метод в неправильном классе
class User:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def calculate_tax(self): # Это про деньги, не про пользователя
return self.salary * 0.15
# ПОСЛЕ
class User:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class PayrollCalculator:
def calculate_tax(self, salary):
return salary * 0.15
Как не сломать всё при рефакторинге
Вот где большинство команд спотыкается. Рефакторинг — это не просто переделка кода. Это переделка кода с гарантией, что ничего не сломалось.
Тесты — твой щит
Без тестов рефакторинг — это русская рулетка. Ты меняешь код, надеясь, что всё работает как раньше, но не знаешь точно. Результат: на продакшене падает.
Правило простое: перед рефакторингом напиши тесты на текущее поведение. Потом переделывай код. Если тесты зелёные — ты всё сделал правильно.
# Сначала напиши тест
def test_calculate_total():
items = [
{'price': 100, 'quantity': 2},
{'price': 50, 'quantity': 3}
]
assert calculate_total(items) == 350
# Потом переделывай код, тест гарантирует результат
def calculate_total(items):
return sum(item['price'] * item['quantity'] for item in items)
Маленькие шаги, частые коммиты
Не меняй сразу 500 строк. Меняй 20-50 строк, коммитишь, пушишь, видишь результат в CI. Если что-то сломалось, откатиться легко.
git commit -m "Refactor: extract calculate_total function"
git push
# CI запустится, ты увидишь результат
Code review для рефакторинга
Если честно, большинство команд пропускают код-ревью для рефакторинга. "Ну это же не фича, что там смотреть". Ошибка. Второй глаз часто видит, что ты что-то упустил.
На проекте в Яндексе мы рефакторили парсер JSON. Я переделал функцию, думал, что всё окей. Ревьюер нашёл edge case с кодировкой UTF-8. Хорошо, что нашёл до продакшена.
Автоматизация проверок
Статические анализаторы типа pylint, flake8, eslint ловят часть проблем автоматически. Не полагайся на них, но используй. Это экономит время на code review.
# Python
pylint my_module.py
flake8 my_module.py
# JavaScript
eslint src/
# Java
mvn checkstyle:check
Профилирование перед и после
Если рефакторинг касается производительности — меряй. До и после. Иногда "красивый" код работает медленнее.
import time
# ДО
start = time.time()
result_old = old_function(large_data)
print(f"Old: {time.time() - start:.4f}s")
# ПОСЛЕ
start = time.time()
result_new = new_function(large_data)
print(f"New: {time.time() - start:.4f}s")
assert result_old == result_new # Проверяем результат
