Introduction aux règles de qualité du code avec FindBugs et PMD

1. Vue d'ensemble

Dans cet article, nous mettrons en évidence certaines des règles importantes présentées dans les outils d'analyse de code tels que FindBugs, PMD et CheckStyle.

2. Complexité cyclomatique

2.1. Qu'est-ce que la complexité cyclomatique?

La complexité du code est une métrique importante, mais difficile à mesurer. PMD propose un ensemble solide de règles dans sa section Règles de taille de code, ces règles sont conçues pour détecter les violations concernant la taille des méthodes et la complexité de la structure.

CheckStyle est connu pour sa capacité à analyser le code par rapport aux normes de codage et aux règles de formatage. Cependant, il peut également détecter des problèmes dans la conception de classes / méthodes en calculant certaines métriques de complexité.

L'une des mesures de complexité les plus pertinentes des deux outils est la CC (Cyclomatic Complexity).

La valeur CC peut être calculée en mesurant le nombre de chemins d'exécution indépendants d'un programme.

Par exemple, la méthode suivante donnera une complexité cyclomatique de 3:

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

CC prend en compte l'imbrication des instructions conditionnelles et des expressions booléennes en plusieurs parties.

De manière générale, un code avec une valeur supérieure à 11 en termes de CC est considéré comme très complexe et difficile à tester et à maintenir.

Certaines valeurs courantes utilisées par les outils d'analyse statique sont présentées ci-dessous:

  • 1-4: faible complexité - facile à tester
  • 5-7: complexité moyenne - tolérable
  • 8-10: complexité élevée - la refactorisation doit être envisagée pour faciliter les tests
  • 11 + très haute complexité - très difficile à tester

Le niveau de complexité affecte également la testabilité du code, plus le CC est élevé, plus la difficulté à mettre en œuvre des tests pertinents est élevée . En fait, la valeur de complexité cyclomatique indique exactement le nombre de cas de test nécessaires pour obtenir un score de couverture de 100% des branches.

Le graphe de flux associé à la méthode callInsurance () est:

Les chemins d'exécution possibles sont:

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

Mathématiquement parlant, CC peut être calculé en utilisant la formule simple suivante:

CC = E - N + 2P
  • E: nombre total d'arêtes
  • N: nombre total de nœuds
  • P: nombre total de points de sortie

2.2. Comment réduire la complexité cyclomatique?

Afin d'écrire du code nettement moins complexe, les développeurs peuvent avoir tendance à utiliser différentes approches, selon la situation:

  • Évitez d'écrire de longues instructions de commutation en utilisant des modèles de conception, par exemple, le constructeur et les modèles de stratégie peuvent être de bons candidats pour gérer les problèmes de taille et de complexité du code
  • Écrire des méthodes réutilisables et extensibles en modularisant la structure du code et en implémentant le principe de responsabilité unique
  • Suivre d'autres règles de taille de code PMD peut avoir un impact direct sur CC , par exemple règle de longueur de méthode excessive, trop de champs dans une seule classe, liste de paramètres excessive dans une seule méthode… etc.

Vous pouvez également considérer les principes et modèles suivants concernant la taille et la complexité du code, par exemple le principe KISS (Keep It Simple and Stupid) et DRY (Don't Repeat Yourself).

3. Règles de gestion des exceptions

Les défauts liés aux exceptions peuvent être habituels, mais certains d'entre eux sont largement sous-estimés et doivent être corrigés pour éviter un dysfonctionnement critique du code de production.

PMD et FindBugs offrent tous deux un ensemble de règles concernant les exceptions. Voici notre sélection de ce qui peut être considéré comme critique dans un programme Java lors de la gestion des exceptions.

3.1. Ne lancez pas d'exception enfin

Comme vous le savez peut-être déjà, le bloc finally {} en Java est généralement utilisé pour fermer des fichiers et libérer des ressources, son utilisation à d'autres fins peut être considérée comme une odeur de code .

Une routine typique sujette aux erreurs lève une exception à l'intérieur du bloc finally {} :

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

Cette méthode est censée lever une NullPointerException , mais étonnamment, elle lève une IOException , ce qui peut induire en erreur la méthode appelante pour gérer la mauvaise exception.

3.2. De retour dans le Final Block

L'utilisation de l'instruction return dans un bloc finally {} peut être déroutante. La raison pour laquelle cette règle est si importante, c'est que chaque fois qu'un code lève une exception, elle est rejetée par l' instruction return .

Par exemple, le code suivant s'exécute sans aucune erreur:

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

Une NullPointerException n'a pas été interceptée, mais elle est toujours rejetée par l'instruction return dans le bloc finally .

3.3. Échec de la fermeture du flux en cas d'exception

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

Les vérifications nulles peuvent éviter les NPE, mais lorsqu'elles sont utilisées intensivement, elles affectent certainement la lisibilité du code.

Voici donc quelques techniques utilisées pour éviter les NPE sans vérification nulle:

  • Évitez le mot-clé null lors du codage : cette règle est simple, évitez d'utiliser le mot-clé null lors de l'initialisation des variables ou du renvoi de valeurs
  • Utilisez les annotations @NotNull et @Nullable
  • Utilisez java.util.Optional
  • Implémenter le modèle d'objet nul

6. Conclusion

Dans cet article, nous avons effectué un aperçu général de certains des défauts critiques détectés par les outils d'analyse statique, avec des instructions de base pour résoudre de manière appropriée les problèmes détectés.

Vous pouvez parcourir l'ensemble complet des règles pour chacune d'elles en visitant les liens suivants: FindBugs, PMD.