Перейти к основному содержимому

Введение в правила качества кода с помощью FindBugs и PMD

· 6 мин. чтения

1. Обзор

В этой статье мы расскажем о некоторых важных правилах, используемых в инструментах анализа кода, таких как FindBugs, PMD и CheckStyle.

2. Цикломатическая сложность

2.1. Что такое цикломатическая сложность?

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

CheckStyle известен своей способностью анализировать код в соответствии со стандартами кодирования и правилами форматирования. Однако он также может обнаруживать проблемы в разработке классов/методов, вычисляя некоторые метрики сложности .

Одним из наиболее важных измерений сложности, представленных в обоих инструментах, является CC (Cyclomatic Complexity).

Значение CC можно рассчитать, измерив количество независимых путей выполнения программы.

Например, следующий метод даст цикломатическую сложность 3:

public void callInsurance(Vehicle vehicle) {
if (vehicle.isValid()) {
if (vehicle instanceof Car) {
callCarInsurance();
} else {
delegateInsurance();
}
}
}

CC учитывает вложенность условных операторов и логических выражений, состоящих из нескольких частей.

Вообще говоря, код со значением CC выше 11 считается очень сложным, его трудно тестировать и поддерживать.

Некоторые общие значения, используемые инструментами статического анализа, показаны ниже:

  • 1-4: низкая сложность — легко тестировать
  • 5-7: умеренная сложность – терпимо
  • 8-10: высокая сложность — следует рассмотреть возможность рефакторинга для облегчения тестирования.
  • 11 + очень высокая сложность — очень сложно протестировать

Уровень сложности также влияет на тестируемость кода: чем выше CC, тем сложнее реализовать соответствующие тесты . На самом деле значение цикломатической сложности точно показывает количество тестовых случаев, необходимых для достижения 100%-го показателя покрытия ветвей.

Блок-схема, связанная с методом callInsurance() :

./9320851a18abc57c33bd3b5396d88628.png

Возможные пути выполнения:

  • 0 => 3
  • 0 => 1 => 3
  • 0 => 2 => 3

С математической точки зрения CC можно рассчитать по следующей простой формуле:

CC = E - N + 2P
  • E: Общее количество ребер
  • N: общее количество узлов
  • P: Общее количество точек выхода

2.2. Как уменьшить цикломатическую сложность?

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

  • Избегайте написания длинных операторов switch , используя шаблоны проектирования, например, шаблоны построителя и стратегии могут быть хорошими кандидатами для решения проблем размера и сложности кода.
  • Напишите многократно используемые и расширяемые методы, разделив структуру кода на модули и реализуя принцип единой ответственности .
  • Соблюдение других правил размера кода PMD может иметь прямое влияние на CC , например, правило чрезмерной длины метода, слишком много полей в одном классе, список чрезмерных параметров в одном методе и т. д.

Вы также можете рассмотреть следующие принципы и шаблоны, касающиеся размера и сложности кода, например, принцип KISS (Keep It Simple and Stupid) и DRY (Don't Repeat Yourself) .

3. Правила обработки исключений

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

PMD и FindBugs предлагают несколько наборов правил, касающихся исключений. Вот наш выбор того, что может считаться критическим в программе Java при обработке исключений.

3.1. Не выбрасывать исключение в finally

Как вы, возможно, уже знаете, блок finally{} в Java обычно используется для закрытия файлов и освобождения ресурсов, его использование для других целей можно рассматривать как запах кода .

Типичная подверженная ошибкам процедура выдает исключение внутри блока finally{} :

String content = null;
try {
String lowerCaseString = content.toLowerCase();
} finally {
throw new IOException();
}

Этот метод должен генерировать NullPointerException , но, как ни странно, он генерирует IOException , что может ввести вызывающий метод в заблуждение и заставить его обрабатывать неправильное исключение.

3.2. Возвращение в блок finally

Использование оператора return внутри блока finally{} может сбивать с толку. Причина, по которой это правило так важно, заключается в том, что всякий раз, когда код генерирует исключение, оно отбрасывается оператором return .

Например, следующий код выполняется без каких-либо ошибок:

String content = null;
try {
String lowerCaseString = content.toLowerCase();
} finally {
return;
}

Исключение NullPointerException не было перехвачено, но все же отброшено оператором return в блоке finally .

3.3. Невозможно закрыть поток при исключении

Закрытие потоков — одна из основных причин, почему мы используем блок finally , но это не тривиальная задача, как кажется.

Следующий код пытается закрыть два потока в блоке finally :

OutputStream outStream = null;
OutputStream outStream2 = null;
try {
outStream = new FileOutputStream("test1.txt");
outStream2 = new FileOutputStream("test2.txt");
outStream.write(bytes);
outStream2.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outStream.close();
outStream2.close();
} catch (IOException e) {
// Handling IOException
}
}

Если инструкция outStream.close() выдает исключение IOException , то outStream2.close() будет пропущен.

Быстрым решением будет использование отдельного блока try/catch для закрытия второго потока:

finally {
try {
outStream.close();
} catch (IOException e) {
// Handling IOException
}
try {
outStream2.close();
} catch (IOException e) {
// Handling IOException
}
}

Если вам нужен хороший способ избежать последовательных блоков try/catch , проверьте метод IOUtils.closeQuiety из Apache commons, он упрощает обработку закрытия потоков без создания IOException .

5. Плохая практика

5.1. Класс определяет compareto() и использует Object.equals()

Всякий раз, когда вы реализуете метод compareTo() , не забудьте сделать то же самое с методом equals() , иначе результаты, возвращаемые этим кодом, могут сбивать с толку:

Car car = new Car();
Car car2 = new Car();
if(car.equals(car2)) {
logger.info("They're equal");
} else {
logger.info("They're not equal");
}
if(car.compareTo(car2) == 0) {
logger.info("They're equal");
} else {
logger.info("They're not equal");
}

Результат:

They're not equal
They're equal

Чтобы устранить путаницу, рекомендуется убедиться, что Object.equals() никогда не вызывается при реализации Comparable, вместо этого вы должны попытаться переопределить его примерно так:

boolean equals(Object o) { 
return compareTo(o) == 0;
}

5.2. Возможное разыменование нулевого указателя

NullPointerException (NPE) считается наиболее часто встречающимся исключением в программировании на Java, и FindBugs жалуется на разыменование Null PointeD, чтобы избежать его выбрасывания.

Вот самый простой пример броска NPE:

Car car = null;
car.doSomething();

Самый простой способ избежать NPE — выполнить нулевую проверку:

Car car = null;
if (car != null) {
car.doSomething();
}

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

Итак, вот некоторые методы, используемые для избежания NPE без нулевых проверок:

  • Избегайте ключевого слова null при кодировании : это простое правило, избегайте использования ключевого слова null при инициализации переменных или возврате значений .
  • Используйте аннотации @NotNull и @Nullable
  • Используйте java.util.Необязательно
  • Реализуйте шаблон нулевого объекта

6. Заключение

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

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