Въведение в правилата за качество на кода с FindBugs и PMD

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 взема предвид влагането на условни изрази и многоделни булеви изрази.

Най-общо казано, код със стойност, по-висока от 11 по отношение на CC, се счита за много сложен и труден за тестване и поддръжка.

Някои често срещани стойности, използвани от инструментите за статичен анализ, са показани по-долу:

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

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

Графиката на потока, свързана с метода callInsurance () , е:

Възможните пътища за изпълнение са:

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

Математически казано, CC може да се изчисли, като се използва следната проста формула:

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

2.2. Как да намалим цикломатичната сложност?

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

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

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

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

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

PMD и FindBugs предлагат и набор от правила по отношение на изключенията. Ето нашия избор на това, което може да се счита за критично в Java програма при обработка на изключения.

3.1. Не хвърляйте изключение накрая

Както може би вече знаете, блокът konačno {} в Java обикновено се използва за затваряне на файлове и освобождаване на ресурси, като използването му за други цели може да се счита за миризма на код .

Типична склонна към грешки рутина е хвърляне на изключение в блока накрая {} :

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

Този метод трябва да хвърли NullPointerException , но изненадващо хвърля IOException , което може да заблуди извикващия метод за обработка на грешно изключение.

3.2. Връщайки се в най- накрая Блок

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

Например следният код се изпълнява без никакви грешки:

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

А NullPointerException не е заловен, но все пак, все още изхвърли от отчета за връщане в крайна сметка блок.

3.3. Неуспешно затваряне на потока при изключение

Closing streams is one of the main reasons why we use a finally block, but it's not a trivial task as it seems to be.

The following code tries to close two streams in a finally block:

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 } }

If the outStream.close() instruction throws an IOException, the outStream2.close() will be skipped.

A quick fix would be to use a separate try/catch block to close the second stream:

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

If you want a nice way to avoid consecutive try/catch blocks, check the IOUtils.closeQuiety method from Apache commons, it makes it simple to handle streams closing without throwing an IOException.

5. Bad Practices

5.1. Class Defines compareto() and Uses Object.equals()

Whenever you implement the compareTo() method, don't forget to do the same with the equals() method, otherwise, the results returned by this code may be confusing:

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"); }

Result:

They're not equal They're equal

To clear confusions, it is recommended to make sure that Object.equals() is never called when implementing Comparable, instead, you should try to override it with something like this:

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

5.2. Possible Null Pointer Dereference

NullPointerException (NPE) is considered the most encountered Exception in Java programming, and FindBugs complains about Null PointeD dereference to avoid throwing it.

Here's the most basic example of throwing an NPE:

Car car = null; car.doSomething();

The easiest way to avoid NPEs is to perform a null check:

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

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

Ето някои техники, използвани за избягване на NPE без нулеви проверки:

  • Избягвайте ключовата дума null по време на кодиране : Това правило е просто, избягвайте да използвате ключовата дума null при инициализиране на променливи или връщане на стойности
  • Използвайте @NotNull и @Nullable пояснения
  • Използвайте java.util. Незадължително
  • Внедрете Null Object Pattern

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

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

Можете да разгледате пълния набор от правила за всяко едно от тях, като посетите следните връзки: FindBugs, PMD.