Каталог "запахов" кода

Введение
Задаётесь ли вы вопросом, насколько хорошо написан ваш код? Какие в нем есть недостатки: архитектурные, стилистические, форматирования?
Если вас беспокоит "здоровье" вашего кода, то данная статья поможет подсветить в нем скрытые от вашего взора недостатки. В ней описываются так называемые "запахи" кода, которые негативно сказываются на его качестве, а также способы их устранения.
Запахи в коде (code smells) — недостатки в коде, которые мешают его читабельности и ухудшают поддерживаемость.
Code smell — это не ошибка в коде, влияющая на выполнение программы, а признак его запутанности и избыточности.
Для устранения таких недостатков используется рефакторинг — процесс улучшения структуры кода без изменения его функциональности. Цель рефакторинга — сделать код понятнее и легче в сопровождении.
Так как программная индустрия за долгие десятилетия накопила большой список "запахов", данная статья периодически будет пополняться новыми их видами и техниками устранения.
1. Guard Clauses
Guard Clauses (охранные выражения) — это подход, при котором в коде в первую очередь записывают проверки, исключающие его дальнейшее выполнение при ложном результате. Это избавляет от необходимости создавать вложенные if-else и лишние ветки с else, делая код "плоским" и легким для восприятия.
1
Для освоения данного способа рефакторинга рассмотрим метод compare(), сравнивающий два числа. Сам метод работает корректно, но из-за вложенных if-else его сложно читать и понимать.
Integer compare(Integer a, Integer b) {
    if (a != null && b != null) {
        if (!a.equals(b)) {
            if (a > b) {
                return 1;
            } else if (a < b) {
                return -1;
            }
        }
        return 0;
    } else {
        throw new IllegalArgumentException("Arguments must not be null");
    }
}
Метод возвращает:
  • 0, если числа равны
  • 1, если a > b
  • -1, если a < b
Также он выкидывает исключение, если параметры равны null
Выполним рефакторинг, применив Guard Clauses:
Integer compare(Integer a, Integer b) {
    if (a == null || b == null) {
        throw new IllegalArgumentException("Arguments must not be null");
    }
    if (a.equals(b)) return 0;
    if (a > b) return 1;
    return -1;
}
Что сделали:
  • Избавились от вложенных проверок и лишних веток
  • Убрали явную проверку a < b, применив метод исключения
  • Инвертировали проверку на null
Можно еще упростить код, заменив две последние строки на тернарный оператор:
Integer compare(Integer a, Integer b) {
    if (a == null || b == null) {
        throw new IllegalArgumentException("Arguments must not be null");
    }
    if (a.equals(b)) return 0;
    return a > b ? 1 : -1;
}
2
Guard Clauses применяется не только для вложенных if-else. Его можно использовать с целью избавления от лишних веток, руководствуясь правилом:
Если имеется ветка else (else-if), идущая после return, break, yield, throw или continue, то находящийся в ней код необходимо вынести из нее, а ветку удалить
Код с "запахом":
if (b != 0) {
    result = a / b;   
} else {
    throw new IllegalArgumentException("Деление на ноль запрещено");
}
Исправленный вариант:
if (b == 0) {
    throw new IllegalArgumentException("Деление на ноль запрещено");
} 
result = a / b;
Что сделали:
  • Убрали лишнюю ветку else
  • Инвертировали проверку на 0
3
Когда использовать Guard Clauses?
  • Метод перегружен вложенными проверками
  • Перед else стоят return, break, yield, throw или continue
2. Magic Number
В данном разделе мы рассмотрим проблему использования в коде чисел, смысл которых не очевиден без дополнительных пояснений. Такие числа называются магическими, так как они могут означать что угодно. Подобное явление в коде ухудшает его понимание и усложняет модификацию.
1
Как вы думаете, что означает число 10 в следующем коде?
if (bookCount < 10) {
...
}
Количество полок в шкафу, число книг на полке, количество книг, которые нужно прочитать за месяц?
Чтобы не гадать о назначении данного числа, придадим ему больше смысла, поместив в переменную. При этом важно придумать для нее хорошее имя, информирующее, что она хранит.
Исправленный код:
int maxBooksOnShelf = 10;
if (bookCount < maxBooksOnShelf) {
...
}
Благодаря переменной стало понятно, что число 10 обозначало максимально допустимое количество книг на полке.
2
Недостатки магических чисел
  • Код сложнее понимать и рефакторить
  • Есть вероятность совершить ошибку при изменении значения, когда числа используются в коде более одного раза
  • При модификации одного магического числа можно ошибочно изменить другое с таким же значением
3
Не все числа магические
Следует отличать магические числа от чисел, которые таковыми не являются, так как их назначение понятно из контекста:
for (i = 0; i < size - 1; i++) {
Из данного кода любому программисту понятно, что счётчик цикла инициализируется 0, а 1 уменьшает количество итераций на одну. Избыточно помещать эти значения в переменные.
Еще пример:
if (num % 2 == 0) {
Цифра 2 используется для проверки числа на четность или нечетность и не требует отдельной переменной.
4
Константы
Константы в Java — это статические финальные поля, содержимое которых неизменно.
Если магическое число никогда не меняется, то его необходимо сделать константой.
Пример кода с константой:
static final int PLAYER_COUNT = 3;
...

for (int i = 0; i < PLAYER_COUNT; i++) {
    print("Введите имя игрока: ");
Достоинства констант:
  • Константы защищают свои значения от случайных изменений
  • Хорошее имя константы является своего рода документацией, которая раскрывает смысл хранимого в ней значения
  • Изменить значение константы в одном месте проще, чем делать это по всей кодовой базе, рискуя совершить ошибку
  • Константы устраняют дублирование. Это особенно актуально, когда значение является сложным или длинным (например, 3.1415926535)
  • Наличие одного и того же значения во множестве мест приведет к дублированию комментариев или их отсутствию. При наличии константы документацию к ней достаточно написать только в месте ее объявления
Использование магических чисел в коде является bad practice. Их стоит всячески избегать, помещая значения в локальные переменные либо константы.
Заключение
Из данной статьи вы узнали, что запахи кода — это признаки потенциальных проблем, которые могут повлиять на качество и поддерживаемость программ. Используйте данный каталог как ориентир для рефакторинга, чтобы сделать ваш код понятным и эффективным. Регулярная работа над устранением запахов кода поможет предотвратить накопление технического долга и упростить дальнейшую разработку.
Автор: Чимаев Максим
Оцените статью, если она вам понравилась!