Когда я впервые прочитал "Refactoring" Мартина Фаулера, был 2019-й год. Я сидел в офисе Яндекса, смотрел на кодовую базу, которая копилась 10 лет, и понял: я делал всё не так. Не плохо, но неправильно. Фаулер описал то, что я интуитивно ощущал, но никогда не формулировал в слова.
Вот честное признание: большинство разработчиков слышали о рефакторинге, но не понимают, что это на самом деле. Путают с переписыванием. Думают, что это опция, которая может подождать. Нет. Это то, без чего код деградирует.
Давайте разберёмся, что это на самом деле.
Что такое рефакторинг по Фаулеру
Мартин Фаулер дал чёткое определение: рефакторинг — это переструктурирование существующего кода без изменения его внешнего поведения. Ключевое слово здесь — "без изменения". Вы не добавляете фичи. Вы не ловите новые баги. Вы делаете ровно одно: улучшаете структуру.
Это очень важно отличать от переписывания. Переписывание — это когда вы удаляете старый код и пишете новый с нуля. Рефакторинг — это когда вы меняете форму, но не суть. Как лепка скульптуры из одного куска мрамора: добавляешь деталь, удаляешь лишнее, но мрамор тот же.
На практике это выглядит так:
# ДО рефакторинга
def calculate_price(items):
total = 0
for item in items:
if item['type'] == 'book':
total += item['price'] * 0.9
elif item['type'] == 'food':
total += item['price'] * 0.95
else:
total += item['price']
if len(items) > 10:
total *= 0.95
return total
# ПОСЛЕ рефакторинга
def calculate_price(items):
total = sum(get_item_price(item) for item in items)
return apply_bulk_discount(total, len(items))
def get_item_price(item):
discount_rate = get_discount_rate(item['type'])
return item['price'] * discount_rate
def get_discount_rate(item_type):
rates = {'book': 0.9, 'food': 0.95}
return rates.get(item_type, 1.0)
def apply_bulk_discount(total, item_count):
return total * 0.95 if item_count > 10 else total
Функция делает то же самое. Но теперь вы видите логику. Она разложена на части. Каждая часть отвечает за одно. Если надо добавить скидку для электроники — добавляете одну строку в словарь. Не переписываете всю функцию.
Поведение не изменилось. Входные и выходные данные те же. Но код теперь живой, а не застывший.
Code smells: когда пора рефакторить
Фаулер ввёл понятие "code smells" — признаки того, что код нуждается в рефакторинге. Не баги. Не ошибки. Просто запахи, которые говорят: здесь что-то не правильно.
Самые частые:
Длинный метод. Если функция не помещается на экран — это проблема. По-хорошему, функция должна делать одно. Если вы прокручиваете код вверх-вниз, пытаясь понять, что происходит — нужен рефакторинг.
# Это плохо
def process_user_order(user_id, items):
user = fetch_user(user_id)
if user is None:
raise Exception("User not found")
total = 0
for item in items:
product = fetch_product(item['id'])
if product is None:
raise Exception("Product not found")
total += product['price'] * item['quantity']
tax = total * 0.1
total += tax
order = create_order(user_id, items, total)
send_confirmation_email(user['email'], order)
update_inventory(items)
return order
# Это хорошо
def process_user_order(user_id, items):
user = validate_user(user_id)
total = calculate_order_total(items)
order = create_order(user_id, items, total)
send_confirmation_email(user['email'], order)
update_inventory(items)
return order
Дублирование кода. Если одна и та же логика копируется в три места — это признак того, что надо извлечь функцию. На одном проекте я нашёл одну и ту же валидацию почты скопированной 14 раз. Каждый раз немного по-другому. Когда нашли уязвимость в валидации, пришлось патчить все 14 мест. Было грустно.
Большой класс. Если класс делает слишком много — разделите его. Если у класса больше 5-7 публичных методов — подумайте, может быть это несколько классов?
# Плохо: один класс делает всё
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def validate_email(self):
# Валидация
pass
def send_email(self):
# Отправка письма
pass
def save_to_database(self):
# Работа с БД
pass
def generate_report(self):
# Отчёты
pass
# Хорошо: разделили ответственность
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class EmailValidator:
@staticmethod
def validate(email):
pass
class EmailService:
def send(self, email, message):
pass
class UserRepository:
def save(self, user):
pass
class UserReporter:
def generate(self, user):
pass
Длинный список параметров. Если функция принимает 5+ параметров — что-то не так. Либо вы передаёте слишком много данных, либо надо создать объект.
# Плохо
def create_invoice(user_id, user_name, user_email, user_phone, items, discount, tax_rate, shipping_cost):
pass
# Хорошо
class InvoiceData:
def __init__(self, user, items, discount, tax_rate, shipping_cost):
self.user = user
self.items = items
self.discount = discount
self.tax_rate = tax_rate
self.shipping_cost = shipping_cost
def create_invoice(invoice_data):
pass
Условная логика, которая запутана. Вложенные if на три уровня — это красный флаг. Используйте guard clauses.
# Плохо
def process_payment(user, amount):
if user is not None:
if user.is_active:
if amount > 0:
if user.balance >= amount:
user.balance -= amount
return True
return False
# Хорошо
def process_payment(user, amount):
if not user or not user.is_active:
return False
if amount <= 0:
return False
if user.balance < amount:
return False
user.balance -= amount
return True
Видите разницу? Во втором варианте вы сразу понимаете, при каких условиях платёж не пройдёт. Не надо следить за вложенностью.
Техники рефакторинга: что на что менять
Фаулер описал десятки техник. Я расскажу о самых полезных.
Extract Method. Самая частая. Берёте кусок кода, выделяете в отдельную функцию, даёте ей понятное имя. Вуаля.
# ДО
def print_invoice(invoice):
print("INVOICE")
print("=" * 50)
total = 0
for item in invoice['items']:
print(f"{item['name']}: {item['price']} x {item['quantity']}")
total += item['price'] * item['quantity']
print("=" * 50)
print(f"Total: {total}")
# ПОСЛЕ
def print_invoice(invoice):
print_header()
print_items(invoice['items'])
print_total(invoice['items'])
def print_header():
print("INVOICE")
print("=" * 50)
def print_items(items):
for item in items:
print(f"{item['name']}: {item['price']} x {item['quantity']}")
def print_total(items):
total = sum(item['price'] * item['quantity'] for item in items)
print("=" * 50)
print(f"Total: {total}")
Replace Magic Numbers with Named Constants. Числа без смысла — это зло.
# Плохо
if user.age >= 18 and user.experience_years >= 3:
salary = base_salary * 1.25
# Хорошо
MIN_AGE = 18
MIN_EXPERIENCE_YEARS = 3
SENIOR_SALARY_MULTIPLIER = 1.25
if user.is_senior():
salary = base_salary * SENIOR_SALARY_MULTIPLIER
def is_senior(user):
return user.age >= MIN_AGE and user.experience_years >= MIN_EXPERIENCE_YEARS
Replace Conditional with Polymorphism. Если у вас есть if item_type == 'book' в 10 местах — используйте наследование или интерфейсы.
# Плохо: проверка типа везде
def get_discount(item):
if item['type'] == 'book':
return 0.1
elif item['type'] == 'food':
return 0.05
else:
return 0
# Хорошо: каждый тип знает свою скидку
class Item:
def get_discount(self):
raise NotImplementedError
class Book(Item):
def get_discount(self):
return 0.1
class Food(Item):
def get_discount(self):
return 0.05
class Other(Item):
def get_discount(self):
return 0
Introduce Parameter Object. Когда одни и те же параметры передаются повсеместно — соберите их в класс.
# Плохо
def create_user(name, email, phone):
pass
def send_welcome_email(name, email, phone):
pass
def save_to_database(name, email, phone):
pass
# Хорошо
class UserData:
def __init__(self, name, email, phone):
self.name = name
self.email = email
self.phone = phone
def create_user(user_data):
pass
def send_welcome_email(user_data):
pass
def save_to_database(user_data):
pass
Rename. Просто переименуйте переменную, если её имя не понятно. Это звучит просто, но работает чудесно.
# Плохо
def calc(a, b, c):
return a + b * c
# Хорошо
def calculate_total_price(base_price, item_count, discount_rate):
return base_price + item_count * discount_rate
Как не сломать всё при рефакторинге
Вот это самое важное. Рефакторинг без тестов — это самоубийство.
Шаг первый: напишите тесты для существующего кода. Даже если его нет. Если код работает — напишите тесты, которые проверяют его текущее поведение. Это ваша подушка безопасности.
# Тест для функции выше
def test_calculate_price():
items = [
{'type': 'book', 'price': 100},
{'type': 'food', 'price': 50}
]
assert calculate_price(items) == 135 # 100*0.9 + 50*0.95
def test_calculate_price_with_bulk_discount():
items = [{'type': 'other', 'price': 100}] * 11
assert calculate_price(items) == 1045 # 1100 * 0.95
Шаг второй: рефакторьте маленькими шагами. Не переписывайте всю функцию сразу. Один рефакторинг — один запуск тестов. Если тесты зелёные — коммитьте. Если красные — откатываете.
# Процесс выглядит так
git add .
git commit -m "Extract discount calculation"
npm test # или pytest, или что у вас
# Если всё зелёное — идём дальше
git add .
git commit -m "Extract bulk discount logic"
npm test
# И так далее
Шаг третий: используйте инструменты. IDE помогут. PyCharm, VS Code, IntelliJ — все они умеют рефакторить. Используйте встроенные команды: Extract Method, Rename, Move. Они безопаснее, чем руками.
Шаг четвёртый: просить code review. Даже рефакторинг — это не одиночный процесс. Коллега может заметить, что вы потеряли какой-то edge case или усложнили логику.
На практике я вижу, что команды часто пропускают этот шаг. "Это же просто рефакторинг, не фича". Ошибка. Рефакторинг может сломать так же легко, как и новый код.
Когда начинать рефакторинг
Честно? Постоянно. Но есть моменты, когда это критично.
Перед добавлением новой фичи. Если вы видите, что код грязный, и вам сложно добавить фичу — сначала рефакторьте. Будет быстрее. На одном проекте мы потратили день на рефакторинг, зато новая фича была готова на следующий день вместо трё
