Un antipattern est une attitude que l'on doit absolument éviter lorsque l'on écrit du code-source.
Il s'agit donc de mauvaises pratiques à éviter.
En anglais, on parle aussi de code smell, i.e. d'indices au niveau du code qui indiquent un problème structurel plus important.
L'abus du static en Java
Evitons static
- Le langage Java propose le mot-clé static pour désigner des membres de classes (par opposition aux membres d'instance)
- Une méthode ou champ statique est donc propre à la classe elle-même et non à l'une de ces instances
- static doit être utilisé avec parcimonie
-
Dans le cas extrême d'une utilisation systématique de static, on n'utilise plus les capacités objet de Java
- On reproduit donc le mode d'écriture d'un programme utilisant un langage procédural (C, Pascal...)
Quelques usages légitimes de static
Expression de constantes
public class MathElements { /** Une constante immutable */ public static final double APPROX_PI = 3.14; /** * Utiliser final avec un type mutable apporte un faux sentiment de sécurité * car on peut le modifier (seule la référence est finale) */ public static final int[] FIBO_SEQUENCE_START = new int[]{1, 1, 2, 3, 5, 8, 11}; /** Avec List.of, on obtient une liste immutable, * toute tentative de changement occasionnera une exception */ public static final List<Integer> FIBO_SEQUENCE_START_2 = List.of(1, 1, 2, 3, 5, 8, 11); }
Champs mutable utiles pour la classe
Quelques exemples de cas où un champ statique mutable peut être utile :
- Compteur d'instance (numéro de série)
- Conservation d'une unique instance singleton
- Conservation d'un cache partagé par toutes les instances
- ...
Cas des classes contenant que des membres statiques
Deux possibilités :
-
Soit la classe ne contient pas de champs (n'a donc pas d'état) mais que des méthodes statiques
- C'est possible pour implanter des classes contenant des méthodes utilitaires (car par exemple de la classe Collections de l'API standard qui propose des méthodes utilitaires travaillant sur les collections comme Collections.shuffle(...) pour mélanger une liste ou Collections.binarySearch(...) pour réaliser une recherche dichotomique sur une liste
-
Si la classe contient aussi des champs statiques (pas uniquement des méthodes), alors il est recommandé d'utiliser plutôt un singleton
- Avec un singleton, toutes les méthodes et champs deviennent non-statiques, mais on n'autorise qu'une seule instance de la classe
-
Seul un champ et une méthode sont statiques :
- private static final MyClass instance; pour stocker l'instance unique
- public static final MyClass getInstance() pour récupérer l'instance unique (et l'instancier si ce n'est déjà fait)
Certains langages orientés objet n'utilisent pas le concept de membres static :
- C'est le cas de Kotlin qui utilise à la place des objets singleton (avec des companion object pouvant accompagner des classes)
Objet divin et code ravioli
- Un god object est un objet qui regroupe trop de champs et méthodes et possède trop de responsabilités dans le programme
- Dans le cas extrême, une seule classe regroupe tout le code : on perd le bénéfice de la programmation objet
- Un god object est souvent le résultat d'un développement itératif : on commence le développement sans vraiment avoir en tête un objectif, et on rajoute du code par petites touches dans la même classe
- Il faut diviser pour règner, i.e. découper les différentes responsabilités et les confier à des objets différentes
- Il faut éviter aussi les god methods, méthodes volumineuses qui font trop de choses
- Le code ravioli est l'inverse du god object : il s'agit d'un code avec trop de petites classes et/ou petites méthodes
- Le code ravioli est difficilement compréhensible car il faut réunir de nombreux morceaux de code à des endroits différents pour obtenir une vision globale du programme
- On parle également de code spaghetti (nombreuses classes avec des interdépendances complexes), de code lasagne (code avec de nombreuses couches liées rendant difficile la maintenance)...
Paramètres trop nombreux
- Il faut éviter d'écrire des constructeurs ou méthodes demandant trop de paramètres
- L'usage de tels constructeurs ou méthodes devient difficile pour un humain qui ne peut mémoriser les paramètres ou leur ordre
- Cela est encore plus problématique en Java où les paramètres par défaut n'existent pas
-
Comment éviter trop de paramètres ?
- Pour un constructeur, on peut reporter la fixation de certains champs avec l'usage de setters (et emploi d'une valeur par défaut dans le constructeur)
- Pour une méthode, on peut fixer un état dans l'objet (champs de la classe) qui sera utilisé par la méthode
- Si l'on doit construire des objets avec beaucoup de paramètres, l'usage d'une fabrique ou builder peut aider
Redéfinition de méthodes
- En Java, il est possible de redéfinir une méthode d'une classe ancêtre dans une classe dérivée
- Mais ce n'est pas toujours une bonne idée
-
Il est préférable de n'utiliser que deux types de méthodes :
- des méthodes final qui ne peuvent être redéfinies dans les classes dérivées (plus performant)
- des méthodes abstract non encore implantées dont la redéfinition sera obligatoire dans une classe dérivée
Duplication de code
- La duplication de code intervient lorsque l'on copie-colle du code à différents endroits d'un projet
- Ce code dupliqué pourrait souvent être factorisé dans une classe ou méthode
- Il existe des outils pour rechercher du code dupliqué (Duplo, CPD...)
-
Garder du code dupliqué rend le code difficilement maintenable :
- le volume de code du projet est plus important
- Si un bug est détecté dans une version du code, les autres versions dupliquées doivent aussi être corrigées (et la probabilité est grande de les oublier)
-
La présence de code dupliqué de grande taille est souvent le signe :
- soit que l'on pourrait externaliser ce code dans une nouvelle bibliothèque
- soit qu'une bibliothèque existe déjà pour faire la même chose (à rechercher sur Internet)
- Il ne faut pas non plus tomber dans l'excès inverse cherchant à factoriser au maximum le code et à utiliser des bibliothèques externes pour tout et n'importe quoi
- Ce phénomène est courant avec JavaScript et les blbiothèques npm :
- Si un problème peut être traité avec moins d'une dizaine de lignes de code, utiliser une blbliothèque externe est contre-productif
Valeurs en dur dans le code
- Il est préférable de regrouper toutes les constantes dans un lieu unique du code sous forme de champs public static final en Java
- Il ne faut jamais insérer dans le code des valeurs littérales : les modifier devient ensuite difficile car il faut reconstituer la logique du code et retrouver tous les exemplaires de ces valeurs
-
Il faut bien discriminer constantes et paramètres :
- Une constante ne sera jamais modifiée quelles que soient les circonstances (par exemple PI ne change pas)
-
En revanche un paramètre peut changer (même si ce changement peut être peu fréquent)
- Un paramètre ne doit donc pas être déclaré comme une constante (sinon cela oblige la recompilation en cas de changement)
-
Un paramètre doit :
- soit être communiqué dans la ligne de commande et récupéré par le tableau args de main
- soit être passé dans une variable d'environnement
- soit être déclaré dans un fichier de configuration chargé au démarrage du programme
Maintien de données redondantes
- Stocker des données est soit utile, soit inutile
-
Conserver des données redondantes (e.g. en plusieurs exemplaires dans la mémoire ou sur le disque) est utile uniquement si cela bénéficie aux performances
- Le cas typique est l'utilisation de caches qui permettent de conserver en mémoire avec accès rapide des informations autrement présentes sur disque ou récupérables à distance
- Conserver des informations redondantes présente un danger : il vaut veiller que toutes les versions soient synchronisées
Mauvaise utilisation des exceptions
- Le mécanisme d'exceptions présent notamment en Java permet de traiter des erreurs inusuelles qui stoppent le flot d'exécution du code
- Les exceptions doivent être utilisées avec parcimonie et ne pas se substituer au type de retour
- Il ne faut jamais capturer une exception avec un bloc catch vide de code : l'exception est rendue silencieuse et le code devient difficile à débugguer
Confiance aveugle en les entrées
- Il ne faut jamais accorder la moindre confiance aux entrees d'un programme provenant de l'extérieur (requêtes venant du réseau, paramètres passé par l'utilisateur...)
-
Un utilisateur malicieux peut fabriquer une entrée dans l'optique :
-
de créer un déni de service (i.e. saturer la mémoire, le disque, le processeur de la machine)
- Exemple #1 : envoi d'un fichier compressé de quelques octets se décompressant en un fichier énorme de centaines de gigaoctets
- Exemple #2 : ralentissement du fonctionnement d'une HashMap en envoyant des données avec collisions sur la valeur de hachage (évité en randomisant le hachage)
-
de réaliser une action non autorisée sur le système
- Exemple #1 : injection de commandes SQL pour altérer la base de données (injection évitée en utilisant des requêtes paramétrées)
- Exemple #2 : exploitation d'une faille ne contrôlant pas la longueur d'une entrée pour réaliser un buffer overflow et exécuter du code arbitraire (en C, pas en Java)
-
de faire fuiter une information personnelle
- Exemple #1 : révélation de la présence d'une adresse email en base sur un formulaire de déclaration de perte de mot de passe (faille fonctionnelle non liée au code lui-même)
-
de créer un déni de service (i.e. saturer la mémoire, le disque, le processeur de la machine)