10 распространённых кодовых запахов и как от них избавиться

Кодовый запах — это фрагмент кода или общая схема программирования, которая намекает на более глубокую проблему в архитектуре или структуре проекта. Запах не обязательно означает баг: часто код работает, но его трудно поддерживать и расширять. В этой статье рассмотрены 10 самых распространённых кодовых запахов, признаки их наличия и стратегии «дезодорации» — то есть безопасного рефакторинга.
Важно: цели статьи — практическая помощь при рефакторинге и предотвращении деградации качества кода. Если вы начинающий разработчик, избегая этих ошибок, вы заметно улучшите качество своего кода.
Как пользоваться этой статьёй
- Прочитайте TL;DR и основные критерии приёмки перед началом рефакторинга.
- Используйте чек-листы и ролевые руководства при планировании изменений.
- Для быстрого принятия решения примените дерево решений Mermaid.
1. Сильная связанность
Проблема
Сильная связанность (tight coupling) возникает, когда два объекта зависят друг от друга настолько, что изменение одного требует изменения другого. Это повышает риск внесения багов при доработках и делает код хрупким.
Признаки
- Класс сразу инстанцирует конкретные реализации других классов.
- Изменение внутренней реализации одного класса приводит к правкам в нескольких местах.
- Невозможность легко заменить зависимость на другую реализацию.
Пример (сохранён оригинальный код):
class Worker {
Bike bike = new Bike();
public void commute() {
bike.drive();
}
}Почему это плохо
Worker напрямую создаёт Bike — зависимость жёсткая. Если нужен Car вместо Bike, придётся править Worker.
Как исправить
Добавьте уровень абстракции (интерфейс/абстрактный класс) и внедряйте зависимость извне (Dependency Injection). Это ослабит связь и упростит тестирование.
Рефакторинг (пример):
class Worker {
Vehicle vehicle;
public void changeVehicle(Vehicle v) {
vehicle = v;
}
public void commute() {
vehicle.drive();
}
}
interface Vehicle {
void drive();
}
class Bike implements Vehicle {
public void drive() {
}
}
class Car implements Vehicle {
public void drive() {
}
}Когда компромисс допустим
Для очень маленьких скриптов или временных прототипов жёсткая связность может быть оправдана ради простоты. Но не переносите такое решение в продакшен.
Критерии приёмки
- Код покрыт тестами, подтверждающими заменяемость реализации.
- Снижение количества прямых зависимостей от конкретных классов.
2. «Бог-объекты»
Проблема
«Бог-объект» — класс или модуль, который делает слишком много: содержит много полей, методов и отвечает за разные аспекты системы. Он «знает слишком много» и «делает слишком многое».
Признаки
- Один класс содержит логику, относящуюся к аутентификации, профилю, платежам и т. п.
- Изменение одного аспекта функциональности требует открытия большого монолитного файла.
Пример (упрощённый исходный код):
class User {
public String username;
public String password;
public String address;
public String zipcode;
public int age;
...
public String getUsername() {
return username;
}
public void setUsername(String u) {
username = u;
}
}Как исправить
Разбейте класс на более мелкие сущности по ответственности: Credentials, Profile, Address и т. д. Используйте композицию.
Рефакторинг (пример):
class User {
Credentials credentials;
Profile profile;
...
}
class Credentials {
public String username;
public String password;
...
public String getUsername() {
return username;
}
public void setUsername(String u) {
username = u;
}
}Когда не стоит дробить
Если объект является естественной агрегирующей сущностью в предметной области и все поля логически связаны, дробление надо делать аккуратно, чтобы не создать чрезмерно мелкие объекты.
Критерии приёмки
- Каждый класс решает одну чёткую задачу.
- Логика, относящаяся к разным бизнес-проблемам, вынесена в отдельные классы.
3. Длинные функции
Проблема
Функция выросла и выполняет слишком много действий. Она тяжела для понимания и тестирования.
Признаки
- Функция занимает десятки или сотни строк.
- Внутри много вложенных блоков и комментариев типа “тут много деталей”.
Как исправить
Выделите подзадачи в отдельные функции, каждая из которых делает одну вещь. Главная функция превращается в последовательность вызовов с понятными именами.
Характеристика
Длинные функции часто идут вместе с глубокими вложениями и множеством параметров.
Критерии приёмки
- Функция не превышает разумного размера для вашей команды (обычно 20–40 строк).
- Каждая подфункция покрывается юнит-тестами.
4. Чрезмерное количество параметров
Проблема
Функция или конструктор требует слишком многих параметров — это делает использование и тестирование неудобным и часто является признаком слишком широкой ответственности.
Признаки
- Список параметров длиннее трёх—пяти элементов.
- Частые изменения сигнатуры вызывают массовые правки в вызывающем коде.
Как исправить
- Группируйте параметры в объект (Parameter Object) или структуру.
- Разделяйте функцию на несколько функций с меньшим набором параметров.
Правило-практика
Остерегайтесь функций с более чем 3 параметрами; рефакторите при первой возможности.
Критерии приёмки
- Публичные методы принимают ограниченное и понятное количество аргументов.
- Когда параметры связаны по смыслу, они собраны в отдельный объект.
5. Плохо названные идентификаторы
Проблема
Имена переменных, функций и классов не отражают их назначение: однобуквенные имена, внедрение типа в имя (booleanFlag), несоблюдение соглашений о стиле.
Признаки
- Невозможно понять цель переменной без чтения реализаций.
- Смешивание camelCase и snake_case в одном кодбэйзе.
Как исправить
Выработайте и применяйте единый стиль (style guide). Имена должны быть короткими, но описательными; функции — содержать глагол; классы — существительное.
Критерии приёмки
- Код читаем при первом просмотре без запуска.
- Имена согласованы с существующим стилевым руководством.
6. Магические числа
Проблема
Жёстко закодированные числовые константы, смысл которых неочевиден. Человек не понимает, откуда взялось число.
Признаки
- В коде встречаются числа вроде 7, 3600, 42 без пояснений.
Как исправить
Вынесите числа в именованные константы или перечисления. Комментарий помогает, но лучше дать семантическое имя.
Пример
Вместо if (x > 3600) использовать:
private static final int SECONDS_IN_HOUR = 3600;
if (x > SECONDS_IN_HOUR) { ... }Критерии приёмки
- В коде нет неочевидных «магических» чисел — все важные числа имеют имена.
7. Глубокая вложенность
Проблема
Код содержит многоуровневые вложенные циклы и условные конструкции, что усложняет чтение и понимание логики.
Признаки
- Тройная и более вложенность for/if/while.
- Множественные уровни отступов и флаги-состояния.
Как исправить
- Сведите глубину вложенности, вынеся обработку в отдельные функции или методы.
- Для сложной логики рассмотрите применение паттернов State или Strategy.
Специфика
Глубокая вложенность часто встречается у начинающих разработчиков и при обработке многомерных данных.
Критерии приёмки
- Максимальная глубина вложенности ограничена (например, 2–3 уровня).
- Логика декомпозирована на понятные функции.
8. Непроработанные исключения
Проблема
Игнорирование или некорректная обработка исключений скрывает ошибки и затрудняет дебаг.
Признаки
- Пустые catch-блоки или логирование без контекста.
- Перехват общих исключений (catch Exception) без нужды.
Как исправить
- Ловите конкретные исключения и обрабатывайте их адекватно.
- При логировании включайте стектрейсы и контекст.
- Не позволяйте программе «молчать» при критических причинах падения.
Критерии приёмки
- Исключения логируются с контекстом и стектрейсом.
- Нелокальные обработчики превращают исключения в понятные пользовательские ошибки или повторяют попытки в контролируемом виде.
9. Дублирование кода
Проблема
Одиначная логика реализована в нескольких местах. При правках некоторые копии остаются, и это приводит к рассинхронизации.
Признаки
- Один и тот же фрагмент структуры циклов или условий повторяется.
Пример исходного кода:
String queryUsername = getSomeUsername();
boolean isUserOnline = false;
for (String username : onlineUsers) {
if (username.equals(queryUsername)) {
isUserOnline = true;
}
}
if (isUserOnline) {
...
}Как исправить
Вынесите общую логику в функцию:
public boolean isUserOnline(String queryUsername) {
for (String username : onlineUsers) {
if (username.equals(queryUsername)) {
return true;
}
}
return false;
}Критерии приёмки
- Повторяющийся код заменён общими методами или утилитами.
- Новая реализация покрыта тестами.
10. Отсутствие комментариев и документации
Проблема
Код полностью лишён комментариев и документации. Даже хорошо написанный код иногда требует пояснений «почему» принято то или иное решение.
Признаки
- Нет документации на публичные API.
- Алгоритмы без пояснения причин выбора.
Как исправить
Пишите осмысленные комментарии, которые объясняют мотивацию решения, а не повторяют очевидное. Документируйте публичные интерфейсы.
Критерии приёмки
- Публичные методы и модули имеют краткую документацию, объясняющую контракт и ограничения.
- Необходимые сложные алгоритмы снабжены пояснением «почему».
Как писать код, от которого не веет запахом
Большинство запахов обусловлено нарушением базовых принципов проектирования: SRP (принцип единственной ответственности), DRY (не повторяйся), инверсия зависимостей и др. Регулярный код-ревью, автоматический статический анализ и наличие тестов значительно уменьшают вероятность появления запахов.
Ниже — практическое руководство для рефакторинга и сопровождения кода.
Быстрый чек-лист перед рефакторингом
- Есть ли тесты, покрывающие поведение, которое будете менять?
- Можно ли ввести инъекцию зависимостей без больших интеграционных изменений?
- Приводит ли рефакторинг к уменьшению ответственности класса/функции?
- Изменится ли публичный API и как это повлияет на потребителей?
- Планируется ли поэтапный рефакторинг с возможностью отката?
Ролевые чек-листы
Разделите обязанности при рефакторинге:
Разработчик
- Написать юнит-тесты для текущего поведения.
- Прописать план рефакторинга и ожидаемые изменения API.
- Выполнить локальный рефакторинг и прогнать тесты.
Ревьювер
- Проверить, что логика не изменилась без необходимости.
- Оценить влияние на производительность и обратную совместимость.
- Убедиться в наличии документации и примеров использования.
Архитектор
- Утвердить изменения архитектурных границ.
- Понять влияние на модульные и интеграционные тесты.
Мини-методология рефакторинга (пошагово)
- Зафиксируйте текущее поведение с помощью тестов.
- Выделите минимальный безопасный шаг по улучшению (малый коммит).
- Прогоните тесты и статический анализ.
- Повторяйте шаги 2–3, пока не достигнете желаемой структуры.
- Обновите документацию и миграционные заметки.
Дерево решений для диагностики запаха (Mermaid)
flowchart TD
A[Начало: заметили проблемный код?] --> B{Это баг или дизайн-проблема?}
B -- Баг --> C[Починить тестом и фиксом]
B -- Дизайн --> D{Поведение корректно, но трудно поддерживать?}
D -- Да --> E{Повышенная связанность?}
E -- Да --> F[Добавить абстракцию / DI]
E -- Нет --> G{Много ответственности в одном классе?}
G -- Да --> H[Разделить на понятные сущности]
G -- Нет --> I{Дублирование или магические числа?}
I -- Дублирование --> J[Вынести в функцию/модуль]
I -- Магические числа --> K[Переместить в именованные константы]
D -- Нет --> L[Оставить, но пометить для дальнейшего мониторинга]
C --> M[Завершено]
F --> M
H --> M
J --> M
K --> M
L --> M(Используйте дерево для быстрого выбора стратегии рефакторинга)
Шаблоны и чек-листы для распространённых случаев
Шаблон: PR для рефакторинга с минимальными изменениями
- Заголовок: “refactor: <<короткое описание>> — без изменения поведения”
- Описание: кратко — зачем, что меняется и как это тестируется
- Тесты: список новых/изменённых тестов
- Планы отката: шаги для возврата к исходной реализации
- Требования к ревью: отметить архитекторов и владельцев модуля
Шаблон: Parameter Object
- Создать новый класс/структуру, инкапсулирующую связанные параметры
- Обеспечить переходную версию метода (старый метод вызывает новый)
- Постепенно обновлять все вызовы
Примеры когда рефакторинг может навредить (контрпримеры)
- Экстренные исправления в продакшене: риск > выгода, приоритет — исправление работы.
- Очень простые одноцелевые скрипты с ограниченным сроком жизни: время рефакторинга не окупится.
- Невозможность покрыть тестами: без тестов риск регрессий повышается.
Критерии приёмки после рефакторинга
- Все существующие тесты проходят.
- Добавлены тесты на изменённые/новые случаи.
- Изменения задокументированы и не нарушают публичный контракт без явной миграции.
- Производительность не ухудшена (проверить ключевые SLI при необходимости).
Тестовые случаи и приёмочные сценарии
- Поведение до рефакторинга и после — эквивалентность для основных входных данных.
- Граничные значения и исключительные ситуации проверены.
- Покрытие кода для изменённых модулей увеличено или сохранено.
Глоссарий (1 строка на термин)
- Кодовый запах: структура кода, которая указывает на потенциальную проблему с дизайном.
- DI (Dependency Injection): способ передачи зависимостей в объект извне.
- SRP: принцип единственной ответственности.
- DRY: не повторяйся, избегать дублирования логики.
- God object: класс, выполняющий слишком много задач.
Матрица рисков и смягчения (кратко)
- Высокий риск: рефакторинг публичных API. Смягчение: версионирование API и миграционные заметки.
- Средний риск: изменение критичных алгоритмов. Смягчение: детальные тесты на производительность и точность.
- Низкий риск: внутренние утилиты с покрытием тестами. Смягчение: incremental PR и CI.
Короткая сводка
- Выделите время на регулярное рефакторинг и ревью — это инвестиция в скорость разработки.
- Начинайте рефакторинг с покрытия тестами и делайте изменения малыми шагами.
- Используйте паттерны проектирования и простые абстракции для снижения связанности.
Спасибо за прочтение. Какие запахи вы чаще всего встречаете в своей работе? Поделитесь в комментариях.
Изображение: SIphotography / Depositphotos
Похожие материалы
Троян Herodotus: как он работает и как защититься
Включить новое меню «Пуск» в Windows 11
Панель полей PivotTable в Excel — руководство
Включить новый Пуск в Windows 11 — инструкция
Как убрать дубликаты Диспетчера задач Windows 11