Новое в журнале. Каталог code smell для начинающих разработчиков
Практика и проекты

Каталог code smell для начинающих разработчиков: как распознать и что делать

Code smell — не баг, а сигнал в структуре кода, который предсказывает будущие проблемы: его видно по форме кода, а не по тому, ломается ли он прямо сейчас. Разбираем пять категорий запахов по Фаулеру, конкретные примеры — длинный метод, класс-бог, дублирование, Feature Envy, примитивная одержимость — чек-лист поиска и порядок безопасного исправления.

29 октября 202618 мин чтенияПрактикам
Каталог code smell для начинающих разработчиков: как распознать и что делать
5 категорийзапахов по Фаулеру
7 вопросовв чек-листе поиска

Любой код можно собрать так, чтобы он заработал один раз. Вопрос в том, можно ли его потом безопасно менять: добавлять фичу, чинить баг, передавать другому разработчику — без риска что-то сломать в соседнем месте. Code smell — это индикатор именно такой хрупкости: формально код работает, тесты (если они есть) зелёные, но структура уже сигналит, что следующее изменение дастся дороже, чем должно.

Это особенно заметно в заказной разработке, где код сдают одной командой, а поддерживает его потом другая. Если за разработку сайта под ключ отвечает внешняя команда, вроде команда YUSMP Group, такие запахи стоит ловить на этапе код-ревью, а не после сдачи проекта — тогда их исправление стоит часы, а не недели переписывания модуля с нуля.

Что такое code smell и почему это не баг

Code smell — это характеристика в структуре кода, которая не мешает ему работать прямо сейчас, но указывает на более глубокую проблему дизайна и повышает риск будущих изменений. Термин ввёл Кент Бек, а по-настоящему закрепил в обиходе разработчиков Мартин Фаулер в книге «Рефакторинг» 1999 года — там же появилась первая систематическая каталогизация запахов, на которую опирается и эта статья.

Важно различать два понятия. Баг — это код, который делает не то, что должен: неверный расчёт, упавший тест, исключение в проде. Code smell не ломает функциональность — программа с длинным методом на 300 строк вполне может проходить все тесты и годами работать в продакшене без единого инцидента. Запах — это не про «работает/не работает», а про «легко ли с этим жить дальше».

Смысл каталога запахов не в том, чтобы повесить ярлык на код и устыдить автора. Смысл — дать общий словарь: когда вся команда называет одно и то же явление «Feature Envy» или «класс-богом», разговор о рефакторинге переходит от расплывчатого «мне не нравится этот код» к конкретному, узнаваемому паттерну с известным набором техник исправления.

Пять категорий code smell: карта, а не список из 50 терминов

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

КатегорияЧто объединяетПримеры запахов
Bloaters (раздутые элементы)Код, параметр или класс вырос за разумный предел и стал неудобнымДлинный метод, класс-бог, длинный список параметров, примитивная одержимость
Object-Oriented AbusersПринципы ООП применены неправильно или не до концаSwitch/case вместо полиморфизма, временное поле, отказ от наследования там, где оно уместно
Change PreventersОдно логическое изменение требует правок в нескольких не связанных на вид местахРасходящиеся модификации, «хирургия дробовиком» (shotgun surgery)
Dispensables (лишнее)Код, без которого проект станет только чищеДублирование, мёртвый код, избыточные комментарии-костыли
Couplers (чрезмерная связанность)Классы знают друг о друге больше, чем нужно для своей задачиFeature Envy, неуместная близость (intimacy), класс-посредник

Эта таблица — не для заучивания английских терминов. Она нужна как чек-лист направления: если метод стал трудно читать — дело, скорее всего, в Bloaters; если правка в одном месте тянет за собой правки в трёх других файлах — дело в Change Preventers. Дальше разобраны самые частые запахи для начинающих — в основном из Bloaters, Dispensables и Couplers, которые чаще всего всплывают в код-ревью junior-разработчиков.

Стоит сразу проговорить, зачем вообще категории, а не просто список: у начинающего разработчика часто есть смутное ощущение «этот код плохой», но нет языка, чтобы объяснить, почему именно. Категория подсказывает направление вопроса. Код плохо читается и тяжело разрастается — ищите в Bloaters. Правка в одном месте неожиданно ломает другое — смотрите в сторону Couplers или Change Preventers. Это экономит время: вместо того чтобы перебирать двадцать с лишним терминов, разработчик сразу сужает поиск до пяти-шести запахов нужной категории.

Длинный метод и класс-бог — когда код разрастается без контроля

Длинный метод и класс-бог — пожалуй, два самых узнаваемых запаха категории Bloaters: оба описывают один и тот же процесс постепенного разрастания, просто на разных уровнях — функции и класса.

Длинный метод: где проходит грань. Формального порога в строках не существует — метод на 15 строк с пятью уровнями вложенности читается тяжелее метода на 60 строк с линейной логикой. Практичнее ориентироваться не на длину, а на вопрос: можно ли дать методу одно короткое имя, которое честно описывает всё, что он делает? Если имя получается только через «и» (вроде `validateAndSaveAndNotify`), метод делает слишком много для одной единицы кода. Первый сигнал на практике — приходится скроллить, чтобы увидеть метод целиком; второй — комментарии внутри метода вида «// теперь считаем скидку», которые по сути уже размечают границы будущих отдельных методов.

Класс-бог: как он появляется из «удобных» правок. Класс-бог (god class) — класс, который знает и делает слишком много: хранит состояние половины системы и содержит методы на любой случай. Он редко появляется как решение «сделаю один класс на всё». Чаще это результат десятков мелких «удобных» правок: проще добавить метод в уже существующий сервис, чем создавать новый класс и продумывать его границы. Каждая правка по отдельности разумна, а сумма — нет. Узнать класс-бога просто: если при описании его ответственности нужно использовать союз «и» больше одного раза, или список импортов класса занимает половину экрана, перед вами кандидат.

Переплетённые красные и синие провода как метафора тесной связанности кода

Дублирование кода и мёртвый код — что остаётся после быстрых фиксов

Дублирование кода — самый растиражированный запах категории Dispensables и при этом самый обманчивый: скопировать-вставить кажется самым быстрым способом закрыть задачу прямо сейчас, а цена обнаруживается только при следующем изменении правила.

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

Мёртвый код — функция, метод или целый класс, которые больше никто не вызывает. Он остаётся из страха: «а вдруг это где-то ещё используется, лучше не трогать». Опыт показывает обратное: мёртвый код не просто бесполезен, он активно мешает — увеличивает время на чтение проекта новым разработчиком, путает поиск по кодовой базе и создаёт ложное впечатление, что функциональность реализована там, где она давно не работает. В проекте с системой контроля версий (а она есть почти всегда) удалить мёртвый код безопасно: в истории git он остаётся, и в случае ошибки его легко вернуть.

Частный случай дублирования, который новички не всегда замечают, — копирование не строк кода, а структуры решения. Два метода выглядят по-разному текстуально, но решают одну и ту же задачу одинаковым набором шагов: сначала валидация, потом обращение к хранилищу, потом формирование ответа. Текстовый поиск по дублям такое не найдёт, а правило всё равно одно и то же — если два куска кода меняются синхронно при любой правке бизнес-правила, перед вами дублирование, даже если дословного совпадения строк нет.

Длинный список параметров и Feature Envy — признаки не той границы между классами

Длинный список параметров и Feature Envy на первый взгляд не связаны — один запах про сигнатуру функции, второй про то, где функция физически расположена. Но оба указывают на одну и ту же проблему: данные и методы, которые должны быть вместе, оказались в разных местах.

Длинный список параметров. Метод с пятью-шестью параметрами трудно вызвать правильно без постоянного подглядывания в сигнатуру, а порядок однотипных параметров — особенно нескольких булевых или строк — легко перепутать местами: компилятор такую ошибку не поймает. Решение обычно одно из двух: объединить параметры, которые всегда передаются вместе, в отдельный объект (Introduce Parameter Object), либо, если часть параметров достаётся из объекта, у которого они уже есть, просто передать сам объект вместо набора его полей (Preserve Whole Object).

Feature Envy. Дословно «зависть к чужим данным» — метод одного класса настойчиво обращается к полям и методам другого класса вместо того, чтобы работать со своими. Классический пример — метод в классе `Order`, который вызывает пять геттеров у объекта `Customer`, чтобы посчитать скидку. Если метод явно «завидует» другому классу больше, чем своему, это сигнал, что он физически находится не там: техника Move Method переносит его туда, где живут данные, с которыми он в основном работает.

Switch/case и примитивная одержимость — когда тип данных прячет логику

Switch/case сам по себе не запах — переключатель по типу операции в одном месте кодовой базы совершенно нормален. Запахом он становится, когда одна и та же конструкция switch по одному и тому же признаку — например, по типу пользователя или статусу заказа — повторяется в нескольких разных методах и классах. Это означает, что при добавлении нового типа придётся искать и чинить каждый такой switch, а найти все копии руками почти никогда не получается с первого раза. Решение из категории Object-Oriented Abusers — заменить переключение полиморфизмом: каждый тип получает свой класс-наследник с собственной реализацией метода, и добавление нового типа превращается в добавление одного нового класса, а не правку пяти существующих switch.

Примитивная одержимость (primitive obsession) — соседний по смыслу запах: вместо небольшого класса для email, денег или диапазона дат код использует голую строку, float или пару int. Это выглядит экономнее на старте, но означает, что проверка формата email, правила округления денег или логика пересечения диапазонов дублируются в каждом месте, где эти данные встречаются, вместо того чтобы жить в одном маленьком классе-значении (value object).

Проверить это на себе легко по одному вопросу: если завтра правило валидации email изменится (например, разрешат новый домен верхнего уровня), сколько файлов придётся открыть и поправить? Если ответ «один класс `Email`» — одержимости нет. Если ответ «все места, где email проверяется руками через регулярное выражение», — это она, и чем больше таких мест, тем дороже обходится отложенный рефакторинг.

Как искать code smell в своём коде — чек-лист из 7 вопросов

Специально искать запахи постранично по каталогу — медленно и утомительно. Практичнее держать в голове короткий чек-лист и прогонять его по коду, который и так открыт по рабочей задаче.

  1. Можно ли дать методу одно короткое и честное имя без союза «и»?
  2. Если удалить комментарий из середины метода — изменится ли смысл кода, или комментарий просто размечает границу следующего метода?
  3. Есть ли в проекте два места, которые почти дословно повторяют друг друга?
  4. Метод обращается к полям чужого класса чаще, чем к полям своего?
  5. Сигнатура метода — пять и больше параметров, часть из которых логически одно целое?
  6. Вызывается ли этот метод или класс хоть где-нибудь в проекте — или это код, который просто «на всякий случай» ещё никто не удалил?
  7. Если добавить новый тип или вариант, сколько файлов придётся поправить — один или несколько разбросанных switch/if?

Два «да» на этот список — повод присмотреться внимательнее, а не повод переписывать всё сразу.

Что делать, когда запах нашли — порядок действий для начинающего

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

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

Начинать стоит с самых маленьких и обратимых техник, а не с масштабного переписывания:

  • Extract Method — вынести фрагмент длинного метода в отдельный метод с говорящим именем; самая частая и самая безопасная первая техника.
  • Extract Variable — дать имя сложному выражению внутри условия, чтобы код читался как предложение, а не как формула.
  • Inline Method — обратная операция: если метод короче и понятнее своего вызова, его стоит «развернуть» обратно в месте использования.

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

Пример из практики: метод на 80 строк, который валидирует заказ, считает скидку и отправляет уведомление, разбирается не одним большим шагом, а последовательностью мелких. Сначала выражение условия скидки выносится в переменную с именем вроде `isEligibleForLoyaltyDiscount` — уже на этом шаге код читается заметно легче. Дальше блок расчёта скидки выносится в отдельный метод `calculateDiscount`, затем блок отправки уведомления — в `notifyCustomer`. После трёх небольших Extract Method исходный метод превращается в пять-шесть строк, которые читаются как план действий, а вся логика расчёта и валидации остаётся доступной под своими именами и тестируется по отдельности.

Как не растерять дисциплину разбора code smell в командной работе

Отдельный разработчик, который научился видеть запахи, решает проблему только для своего кода. Без договорённостей в команде один человек методично убирает дублирование, а рядом другой с той же скоростью создаёт новое — и общий уровень кодовой базы не меняется.

Рабочая практика — встроить разбор запахов в код-ревью как отдельный явный пункт, а не держать его в голове ревьюера как смутное ощущение «что-то здесь не так». Тот же чек-лист из семи вопросов, вынесенный в шаблон pull request, снимает часть споров: вопрос «это действительно длинный метод или я придираюсь» превращается в «отвечает ли метод на пункт чек-листа да или нет».

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

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

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

Частые вопросы

Code smell — это баг?

Нет. Баг — это неверное поведение программы; code smell — работающий код, структура которого предсказывает проблемы в будущем. Один и тот же метод может не иметь ни одного бага и при этом быть классическим длинным методом.

Чем code smell отличается от технического долга?

Code smell — конкретный узнаваемый паттерн в коде: длинный метод, дублирование, класс-бог. Технический долг — более широкое экономическое понятие, совокупная стоимость всех таких компромиссов для команды. Запахи — симптомы, долг — их суммарная цена в часах и деньгах.

Нужно ли рефакторить сразу, как увидел запах?

Не обязательно. Если участок кода стабилен и его давно никто не трогает, разумнее оставить его как есть и исправить запах тогда, когда придётся вносить туда изменение по другой задаче — по правилу бойскаута.

Какие code smell чаще всего встречаются у новичков?

Чаще всего — длинный метод, дублирование кода через копипасту и примитивная одержимость: использование голых строк и чисел вместо небольших классов-значений. Все три появляются из желания решить задачу максимально быстро, без оглядки на следующее изменение.

Можно ли искать code smell автоматически — линтерами и статическим анализом?

Частично да. Статические анализаторы вроде PHPStan, SonarQube или PHP_CodeSniffer с правилами сложности надёжно ловят формальные признаки: длину метода, цикломатическую сложность, процент дублирования. Feature Envy, класс-бог и примитивную одержимость обычно находят в код-ревью человеком, а не линтером.

Что делать, если рефакторинг ломает тесты?

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

С чего начать, если не видишь запахов в собственном коде?

Начать не с поиска в своём коде, а с чтения чужого. Легче заметить длинный метод или дублирование в коде коллеги, чем в своём — свой код мозг воспринимает как уже знакомый и понятный. После нескольких код-ревью чужих pull request те же паттерны начинают бросаться в глаза и в собственных файлах.

Таксономия пяти категорий в этой статье — пересказ классификации из каталога запахов кода на refactoring.guru, где у каждого запаха из таблицы выше есть отдельная страница с более подробным разбором техник исправления.

Код, который не стыдно передать

Начните с принципов, а дальше двигайтесь по рубрикам. Каждый разбор заканчивается чек-листом.

Случайный разбор