Запахи кода и антипаттерны

Код редко ломается внезапно — сначала он начинает «пахнуть».

Запах кода (code smell) — поверхностный признак того, что в коде, скорее всего, есть более глубокая проблема дизайна. Сам по себе он не баг, но повод присмотреться.

Запахи — это словарь, на котором команда обсуждает качество кода: вместо размытого «как-то некрасиво» можно сказать «здесь God object» или «тут feature envy», и все понимают, о чём речь, и какой рефакторинг напрашивается. Антипаттерн — это запах, доросший до устойчивого способа делать плохо: повторяемое решение, которое кажется удобным, но систематически вредит.

Зачем это на практике

Распознавание запахов — навык дешёвой профилактики. Заметить раздувающийся класс на 200 строк проще, чем разбирать монстра на 2000: чем раньше пойман запах, тем дешевле лечение. Этот навык окупается и на код-ревью — отзыв «вот тут feature envy, перенеси метод» конкретнее и полезнее, чем «мне не нравится». Ниже — самые частые запахи, как их увидеть глазами и чем лечить. Хорошая новость: лечение почти всегда сводится к небольшому, повторяемому набору приёмов рефакторинга — «выделить метод», «выделить класс», «переместить метод», «заменить число константой». Их стоит довести до автоматизма; современные IDE умеют выполнять многие из них одной командой, не ломая поведение.

God object — божественный объект

Один класс знает и делает слишком много: держит половину состояния системы и управляет всем.

Признаки: класс с именем вроде Manager, Helper, Utils, сотни строк, десятки несвязанных методов, его импортируют отовсюду. Это прямое нарушение SRP. «Плохо»:

class App:
    def parse(self, raw): ...
    def validate(self, data): ...
    def save(self, data): ...
    def send_email(self, user): ...
    def render_html(self, data): ...  # всё в одном классе

Лечение — «выделить класс»: разносим обязанности по отдельным классам (Parser, Validator, Repository, Mailer), а App лишь оркестрирует.

Длинный метод

Функция, которая делает слишком много шагов и не помещается на экран.

Длинный метод тяжело читать (надо держать в голове весь контекст) и тяжело тестировать (много веток). Лечение — «выделить метод»: каждый осмысленный шаг становится отдельной функцией с говорящим именем. «Хорошо»:

def normalize(name):
    return name.strip().title()

def is_valid(name):
    return len(name) >= 2

def register(name):
    name = normalize(name)
    if not is_valid(name):
        return "отклонено"
    return f"добро пожаловать, {name}"

print(register("  анна "))
print(register("x"))

Вывод:

добро пожаловать, Анна
отклонено

Хорошее имя извлечённой функции работает как комментарий, который не устаревает: тело register читается почти как текст.

Feature envy — зависть к чужим данным

Метод одного класса больше интересуется данными другого класса, чем своими.

Если метод то и дело дёргает геттеры чужого объекта и считает что-то на их основе, логика, скорее всего, должна жить внутри этого объекта. «Плохо»: внешняя функция лезет в поля заказа.

class Order:
    def __init__(self, price, qty):
        self.price = price
        self.qty = qty

def total(order):  # завидует данным Order
    return order.price * order.qty

Лечение — «переместить метод»: переносим расчёт туда, где живут данные.

class Order:
    def __init__(self, price, qty):
        self.price = price
        self.qty = qty
    def total(self):
        return self.price * self.qty

print(Order(100, 3).total())

Вывод:

300

Спагетти-код

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

Глубокая вложенность if, флаги, прыжки логики, отсутствие границ модулей. Спагетти обычно возникает не сразу, а из накопленных «быстрых правок». Лечится сочетанием приёмов: ранние return вместо лестницы else, выделение методов, введение чётких слоёв. Часто именно здесь уместно вспомнить про паттерны: Strategy убирает ветвление по типу, State приводит в порядок хаос переходов между состояниями.

Другие частые запахи

ЗапахПризнакЧем лечить
Дублированиеодин код в нескольких местахвыделить метод/класс (DRY)
Длинный список параметровфункция на 6+ аргументовобъединить в объект-параметр
Магические числаx * 0.18 без названияименованная константа
Комментарии-костыликомментарий объясняет странный кодпереписать код понятно

Как это работает под капотом

Почему запахи вообще накапливаются? Из-за энтропии изменений: каждая правка под давлением сроков чуть ухудшает структуру, если не вкладываться обратно. Это явление называют техническим долгом — как с кредитом, маленькие «проценты» (лишняя минута на чтение мутного метода) набегают на каждой будущей правке. Рефакторинг — это выплата долга. Запахи — индикаторы того, где «процент» уже высок и пора платить.

Частые ошибки

  • Гнаться за всеми запахами разом. Запах — не баг. Рефакторить стоит то, что вы и так собираетесь менять, а не «всё подряд ради чистоты».
  • Рефакторинг без тестов. Менять структуру вслепую — значит ловить регрессии. Сначала тесты на текущее поведение, потом правка.
  • Большой рефакторинг «за один присест». Безопаснее серия мелких шагов, каждый из которых оставляет код рабочим.
  • Путать запах с осознанным выбором. Иногда длинный, но линейный метод читается лучше, чем десяток мелких. Контекст важнее каталога.

Итоги

  • Запах кода — поверхностный сигнал о возможной проблеме дизайна, а не сам баг.
  • God object и длинный метод лечатся выделением класса и метода (опора на SRP).
  • Feature envy лечится перемещением логики туда, где живут данные.
  • Спагетти-код приводят в порядок ранними return, слоями и иногда паттернами (Strategy, State).
  • Запахи — это карта технического долга; гасить его лучше там, где всё равно работаешь, и под прикрытием тестов.
Проверьте себя
1. Класс называется AppManager, занимает 1500 строк, парсит данные, валидирует, сохраняет в БД и шлёт письма. Какой это запах/антипаттерн?
AFeature envy
BGod object (божественный объект)
CМагические числа
DДлинный список параметров
2. Метод считает итог заказа, постоянно дёргая геттеры объекта Order (order.price, order.qty). Как называется этот запах и чем его лечить?
AСпагетти-код; распутать ранними return
BДублирование; выделить общую константу
CFeature envy; переместить метод внутрь Order
DGod object; разбить на несколько классов
3. Что такое технический долг?
AДеньги, которые компания должна за лицензии на ПО
BНакопленное ухудшение структуры кода, из-за которого каждое будущее изменение обходится дороже
CКоличество открытых задач в трекере
DВремя, потраченное на код-ревью