Introduzione alle regole di qualità del codice con FindBugs e PMD

1. Panoramica

In questo articolo evidenzieremo alcune delle regole importanti presenti negli strumenti di analisi del codice come FindBugs, PMD e CheckStyle.

2. Complessità ciclomatica

2.1. Cos'è la complessità ciclomatica?

La complessità del codice è importante, ma difficile da misurare. PMD offre un solido set di regole nella sezione Code Size Rules, queste regole sono progettate per rilevare le violazioni relative alle dimensioni dei metodi e alla complessità della struttura.

CheckStyle è noto per la sua capacità di analizzare il codice rispetto a standard di codifica e regole di formattazione. Tuttavia, può anche rilevare problemi nella progettazione di classi / metodi calcolando alcune metriche di complessità.

Una delle misurazioni della complessità più rilevanti presenti in entrambi gli strumenti è la CC (Cyclomatic Complexity).

Il valore CC può essere calcolato misurando il numero di percorsi di esecuzione indipendenti di un programma.

Ad esempio, il seguente metodo produrrà una complessità ciclomatica di 3:

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

CC prende in considerazione la nidificazione di istruzioni condizionali ed espressioni booleane in più parti.

In generale, un codice con un valore superiore a 11 in termini di CC, è considerato molto complesso e difficile da testare e mantenere.

Di seguito sono riportati alcuni valori comuni utilizzati dagli strumenti di analisi statica:

  • 1-4: bassa complessità - facile da testare
  • 5-7: complessità moderata - tollerabile
  • 8-10: alta complessità - il refactoring dovrebbe essere considerato per facilitare i test
  • 11 + complessità molto elevata - molto difficile da testare

Il livello di complessità influisce anche sulla testabilità del codice, maggiore è il CC, maggiore è la difficoltà di implementare i test pertinenti . In effetti, il valore della complessità ciclomatica mostra esattamente il numero di casi di test necessari per ottenere un punteggio di copertura dei rami del 100%.

Il grafico di flusso associato al metodo callInsurance () è:

I possibili percorsi di esecuzione sono:

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

Matematicamente parlando, CC può essere calcolato utilizzando la seguente semplice formula:

CC = E - N + 2P
  • E: numero totale di bordi
  • N: numero totale di nodi
  • P: numero totale di punti di uscita

2.2. Come ridurre la complessità ciclomatica?

Per scrivere codice sostanzialmente meno complesso, gli sviluppatori possono tendere a utilizzare approcci diversi, a seconda della situazione:

  • Evita di scrivere lunghe istruzioni di commutazione utilizzando modelli di progettazione, ad esempio il builder e i modelli di strategia possono essere buoni candidati per affrontare problemi di dimensione e complessità del codice
  • Scrivere metodi riutilizzabili ed estensibili modularizzando la struttura del codice e implementando il principio di responsabilità unica
  • Seguire altre regole di dimensione del codice PMD può avere un impatto diretto su CC , ad esempio regola di lunghezza del metodo eccessiva, troppi campi in una singola classe, elenco di parametri eccessivi in ​​un unico metodo ... ecc.

Puoi anche considerare i seguenti principi e schemi riguardanti la dimensione e la complessità del codice, ad esempio il principio KISS (Keep It Simple and Stupid) e DRY (Don't Repeat Yourself).

3. Regole per la gestione delle eccezioni

I difetti relativi alle eccezioni potrebbero essere normali, ma alcuni di essi sono enormemente sottovalutati e dovrebbero essere corretti per evitare disfunzioni critiche nel codice di produzione.

PMD e FindBugs offrono entrambi una manciata di regole riguardanti le eccezioni. Ecco la nostra selezione di ciò che può essere considerato critico in un programma Java quando si gestiscono le eccezioni.

3.1. Infine, non inserire eccezioni

Come forse già saprai, il blocco finalmente {} in Java viene generalmente utilizzato per chiudere file e rilasciare risorse, utilizzarlo per altri scopi potrebbe essere considerato come un odore di codice .

Una tipica routine soggetta a errori lancia un'eccezione all'interno del blocco finalmente {} :

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

Questo metodo dovrebbe generare un'eccezione NullPointerException , ma sorprendentemente genera un'eccezione IOException , che potrebbe fuorviare il metodo chiamante per gestire l'eccezione sbagliata.

3.2. Tornando nel fine blocco

L'uso dell'istruzione return all'interno di un blocco finalmente {} può essere nient'altro che confuso. Il motivo per cui questa regola è così importante, è perché ogni volta che un codice genera un'eccezione, viene scartata dall'istruzione return .

Ad esempio, il codice seguente viene eseguito senza errori di sorta:

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

Un NullPointerException non è stato catturato, eppure, ancora scartato dalla dichiarazione di ritorno nel fine blocco.

3.3. Impossibile chiudere lo streaming in caso di eccezione

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

I controlli nulli possono evitare gli NPE, ma se utilizzati ampiamente, influiscono sicuramente sulla leggibilità del codice.

Quindi ecco alcune tecniche utilizzate per evitare NPE senza controlli nulli:

  • Evita la parola chiave null durante la codifica : questa regola è semplice, evita di utilizzare la parola chiave null durante l'inizializzazione delle variabili o la restituzione di valori
  • Usa le annotazioni @NotNull e @Nullable
  • Usa java.util.Optional
  • Implementa il pattern di oggetti nulli

6. Conclusione

In questo articolo, abbiamo dato uno sguardo generale ad alcuni dei difetti critici rilevati dagli strumenti di analisi statica, con linee guida di base per affrontare in modo appropriato i problemi rilevati.

Puoi sfogliare il set completo di regole per ciascuna di esse visitando i seguenti link: FindBugs, PMD.