«Запахи кода»: что это такое и как они связаны с техническим долгом.

«Запахи кода»: что это такое и как они связаны с техническим долгом.

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

Этот термин был популяризирован Мартином Фаулером и Кентом Беком в книге Фаулера «Рефакторинг: улучшение дизайна существующего кода» (1999), в которой были перечислены 22 «запаха кода» и каждому из них была присвоена соответствующая методика рефакторинга. Этот каталог остается каноническим справочником, и названные Фаулером «запахи кода» — «Длинный метод», «Божественный класс», «Дублированный код», «Зависть к функциям», «Расходящиеся изменения», «Мастерская дробовика» и другие — сегодня встречаются в наборах правил SonarQube, инструментах статического анализа и контрольных списках проверки кода по всей отрасли.

Очистите код от запахов

SMART TS XL помогает сопоставлять и устранять их в сложных системах.

Подробнее

Что такое «запах кода»?

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

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

Запахи кода, ошибки и технический долг

Эти три понятия взаимосвязаны, но различны, и их путаница приводит к неправильной расстановке приоритетов:

КонцепцияОпределениеМгновенный провал?Как найти
ОшибкаКод, приводящий к некорректному поведениюДа, тесты не проходят, пользователи сообщают об ошибках.Тестирование, мониторинг, журналы ошибок
запах кодаСтруктурная модель, предсказывающая будущие проблемы.Нет, код работает корректно.Проверка кода, статический анализ
Технический долгНакопленная стоимость прошлых ошибок и неверных решений.Нет, но со временем это накапливается.Метрики, анализ сложности, оценка трудозатрат на рефакторинг.

«Запахи кода» — это механизм, посредством которого накапливается технический долг. Каждый добавленный в код метод типа Long Method — это единица накопленного технического долга; выплата процентов по нему — это дополнительное время, которое каждый будущий разработчик тратит на его понимание, и каждое будущее изменение тратит на предотвращение побочных эффектов, связанных с его размером.

Что такое «запах кода» в SonarQube?

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

«Запахи кода» Мартина Фаулера: классическая таксономия

Первоначальный перечень из 22 «запахов кода» Фаулера, систематизированный по категориям, остается стандартным справочником. На основе этой таксономии разработаны правила для всех основных инструментов статического анализа.

КатегорияКод Запахи
Вздутие животакод, разросшийся до неуклюжих размеров.Длинный метод, большой класс, одержимость примитивами, длинный список параметров, скопления данных.
Злоупотребления объектно-ориентированным программированиемзлоупотребление принципами объектно-ориентированного программированияОператоры switch, временные поля, отклоненное завещание, альтернативные классы с различными интерфейсами.
Предотвращающие переменызатрудняют переменыДивергентные изменения, хирургическое вмешательство по принципу "дробовик", параллельные иерархии наследования
Излишестваненужный кодКомментарии (избыточные), Дублирующийся код, Ленивый класс, Класс данных, Мертвый код, Спекулятивная общность
Couplersчрезмерное соединениеЗависть к внешности, неуместная близость, цепочки сообщений, посредник.

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

Наиболее распространённые «запахи кода»: краткий справочник

Запах кодаНа что это похожеОсновной риск
Дублированный кодТа же логика встречается во многих местах.Исправления ошибок необходимо применять повсеместно; со временем копии начинают расходиться.
Длинный методМетоды, содержащие более 20-30 строк кода и выполняющие множество функций.Высокая когнитивная нагрузка; сложно проверить изолированное поведение.
Божественный класс / Большой классОдин класс, который делает всё.Каждое изменение функциональности затрагивает один и тот же класс; конфликты слияния, хрупкость.
Длинный список параметровМетоды, принимающие 4 и более параметровЛегко передать неверные значения; сложно прочитать информацию на сайтах вызовов.
Зависть к функциямМетод, который использует данные другого класса чаще, чем свои собственные.Тесная взаимосвязь; изменения в одном классе разрушают другой.
Расходящиеся измененияОдин класс был изменен по множеству разных причин.Нарушение принципа единоличной ответственности; непредсказуемые побочные эффекты.
Хирургия с применением дробовикаОдно изменение требует внесения правок во многие классы.Высокие затраты на изменение; легко упустить важный момент.
Мертвый кодКод, который никогда не вызывается и не выполняетсяЗапутывает разработчиков; накапливается годами; усложняет миграцию.
Первобытная одержимостьИспользование базовых типов (строки, целые числа) вместо доменных объектов.Подтверждение достоверности разбросано повсюду; низкая выразительность.
Сгустки данныхОдна и та же группа полей неоднократно проходила вместе.Должен быть объектом предметной области; сигнализирует об отсутствии абстракции.
Спекулятивная общностьКод, написанный с учетом предполагаемых будущих потребностей.Излишняя сложность; никто не понимает, зачем она нужна.
Непоследовательная обработка ошибокСкрытые перехваты, различные стратегии обработки исключенийСбои остаются незамеченными; отладка занимает гораздо больше времени.

Определения и примеры «запахов кода»

Дублированный код

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

Ява

// ServiceA -- discount calculation
double calculateDiscount(double amount) {
    if (amount > 1000) return amount * 0.1;
    return 0;
}

// ServiceB -- same logic, copied and forgotten
double computeDiscount(double value) {
    if (value > 1000) return value * 0.1;
    return 0;
}

Когда изменяется бизнес-правило (пороговое значение становится 1500, скорость — 12%), обновляется одна копия, а другая — нет. В результате два модуля расходятся во мнениях относительно фундаментальной бизнес-логики, и это несоответствие выявляется в производственной среде во время аудита, а не на этапе тестирования.

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

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

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

Порог обнаружения : Методы, содержащие более 20-30 строк кода, требуют проверки; при содержании более 50 строк рефакторинг почти всегда оправдан. В COBOL абзацы, превышающие 100 операторов, считаются эквивалентными.

питон

class OrderProcessor:
    def process_order(self, order):
        # Validate order -- 40 lines
        # Calculate discounts -- 30 lines
        # Update inventory -- 25 lines
        # Send notification emails -- 20 lines
        # Generate invoice -- 35 lines
        # 150+ lines total
        pass

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

Божественный класс

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

Сигнал обнаружения : Класс, содержащий более 20-30 открытых методов, или класс, в имени которого присутствуют слова «Manager», «Processor», «Handler», «Utils» или «Helper», применяемый к нескольким несвязанным областям.

Расходящиеся изменения

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

Определение : Класс, который постоянно меняется по разным причинам. Противоположность «хирургии из дробовика».

Хирургия с применением дробовика

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

SQL

-- Tax logic duplicated across queries
SELECT amount * 0.05 FROM invoices;
SELECT amount * 0.05 FROM payments;
SELECT amount * 0.05 FROM reports;

Теперь для изменения значения с 0.05 на 0.07 необходимо найти каждое его вхождение в SQL-файлах, хранимых процедурах и коде приложения.

Зависть к функциям

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

Ява

// In ReportGenerator -- envious of Customer's data
double calculateCustomerRating(Customer customer) {
    return customer.getOrderCount() * customer.getAverageOrderValue()
           / customer.getDaysSinceRegistration();
}
// This logic belongs in Customer, not ReportGenerator

Мертвый код

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

обнаружениеИнструменты статического анализа, включая SonarQube, Knip (для TypeScript/JavaScript) и SMART TS XL Выявлять недоступные функции, невызванные методы и неиспользуемые переменные в коде.

Нарушения принципа DRY

Принцип «Не повторяйся» (DRY) гласит, что каждый фрагмент знаний должен иметь единственное, однозначное представление в системе. Нарушения принципа DRY являются первопричиной дублирования кода, скопления данных и многих ситуаций, требующих «хирургического вмешательства наугад». Когда бизнес-логика представлена ​​в нескольких местах, эти представления неизбежно расходятся. Принцип DRY — это суть; дублирование кода — это признак нарушения.

питон

# DRY violation: same validation logic in three places
def validate_email_in_registration(email):
    return "@" in email and "." in email

def validate_email_in_profile_update(email):
    return "@" in email and "." in email

def validate_email_in_checkout(email):
    return "@" in email and "." in email

# DRY-compliant: one function, three callers
def is_valid_email(email):
    return "@" in email and "." in email

Пороги обнаружения: когда код начинает пахнуть?

Для обнаружения «запахов кода» необходимы измеримые пороговые значения. Ниже приведены часто используемые метрики и значения, указывающие на наличие «запаха», требующего внимания:

МетрикаЧто он измеряетПорог предупрежденияКритический порог
Цикломатическая сложностьКоличество ветвей принятия решений в методеНад 10Над 20
Длина метода (в строках)Количество строк в методе/функцииНад 20Над 50
Количество параметровКоличество параметров, которые принимает метод.Над 4Над 7
Длина классаКоличество строк в классеНад 200Над 500
Коэффициент дублированияПроцент дублирующегося кодаВыше 3%Выше 10%
Когнитивная сложностьНасколько сложен для понимания этот кодНад 15Над 25
Афферентная связь (Ca)Количество классов, зависящих от этого классаНад 15Над 30
Эфферентная связь (Ce)Количество занятий, от которых зависит данный классНад 15Над 30

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

Инструменты обнаружения запаха кода

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

ИнструментОсновной языкЧто он обнаруживает
SonarQube / SonarCloudJava, Python, JS/TS, C# и многое другое.Полная таксономия запахов Фаулера, проблемные зоны безопасности, дублирование данных.
Checkstyle + PMDJavaНарушения стиля, дублирование, показатели сложности
ESLint + typescript-eslintJavaScript, TypeScriptДлинные функции, сложность, неиспользуемый код
Пилинт + РадонПитонИндекс сложности, стиля и ремонтопригодности
ReSharper / RiderC#Избыточный код, длинные методы, проблемы со связностью.
ClippyРжавчинаНарушения идиоматики, распространённые шаблоны, которые считаются «запахом кода» в Rust.
КодКлиматПоддержка Различных ЯзыковОценка сложности, дублирования и ремонтопригодности
SMART TS XLCOBOL, JCL, Java, Python, RPG, SQL, .NETДублирование между языками, мертвый код, взаимосвязь, дрейф зависимостей

Запахи кода в Rust В основном их выявляет Clippy, который обеспечивает соблюдение идиоматических шаблонов Rust. К наиболее распространенным специфическим для Rust «запахам» относятся ненужное клонирование и неправильное использование unwrap() в производственных путях, чрезмерно вложенных выражениях соответствия и функциях, которые должны возвращать Result но вместо этого используйте паники.

«Запахи кода» и технический долг: взаимосвязь.

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

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

Объясните технический долг с точки зрения «запахов кода» : если в кодовой базе 40% дублирования кода, каждое исправление ошибки обходится в 1.4 раза дороже, чем должно. Если основной класс обработки — это «класс-бог», от которого зависит всё остальное, каждое добавление функции требует понимания и тестирования всего класса. Если обработка ошибок непоследовательна, каждый инцидент в производственной среде требует больше времени на расследование, поскольку сигналы об ошибке ненадежны. Технический долг — это не абстракция, это сумма этих накапливающихся неэффективностей.

Исследование CISQ неизменно показывает, что разработчики тратят 30-40% своего времени на устранение технического долга, а не на создание нового функционала. Плотность «запахов кода» — наиболее прямой показатель того, сколько долга накопилось.

Как SMART TS XL Обнаруживает «запахи кода» в масштабах предприятия.

Отдельные инструменты, такие как SonarQube и Clippy, работают в рамках одного языка. В корпоративных средах, где программы на COBOL записывают данные в наборы данных, которые считываются сервисами на Java, где потоки заданий JCL вызывают программы на нескольких языках, и где одна и та же бизнес-логика была независимо продублирована в трех разных системах, написанных в три разных десятилетия, инструменты, работающие на одном языке, не могут увидеть полную картину.

SMART TS XLАвтора статический анализ кода Обнаруживает признаки «запахов кода» одновременно во всех языках среды: дублирование логики между копибуком COBOL и вспомогательным классом Java, мертвый код в программах RPG, который не вызывается ни одним заданием JCL, шаблоны «класса-бога» в программах COBOL, где один абзац выполняет работу пятидесяти, и непоследовательные шаблоны обработки ошибок на границе между языками.

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

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

Для команд, занимающихся модернизацией устаревших систем, анализ «запахов кода» является основой плана модернизации: мертвый код удаляется до начала миграции (что уменьшает ее масштаб), дублирующаяся логика объединяется в канонические реализации, компоненты с наибольшей степенью связанности модернизируются в последнюю очередь (после того, как будет решена проблема со всем, что от них зависит), а «классы-монстры» декомпозируются перед преобразованием в новый язык, поскольку преобразование «класса-монстра» в Java приводит к созданию «класса-монстра» в Java.

Устранение «запахов кода»: структура приоритезации

Не все «запахи кода» требуют немедленной рефакторизации. Правильный подход — это приоритизация на основе оценки рисков:

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

Приоритет 2. Проблемы на архитектурных границах. Классы-«боги» и компоненты с высокой степенью связанности, от которых зависит всё, наиболее опасны для изменения, но и наиболее дорогостоящи, если их не исправить. Перед рефакторингом необходимо провести тщательный анализ их влияния.

Приоритет 3: Дублирование кода в разных системах. Когда одна и та же бизнес-логика существует в нескольких системах, изменения необходимо координировать одновременно во всех копиях. Объединение этого дублирования снижает затраты на координацию и предотвращает расхождения.

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

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

Именно умение систематически, а не реактивно, реагировать на сбои в работе, выявлять, измерять и устранять «запахи кода» — вот что отличает команды разработчиков, поддерживающие высокую скорость разработки с течением времени, от тех, кто постепенно замедляет темпы по мере роста своих систем.