Code smell

Un code smell est un indice de la qualité déficiente d’un code. Des code smells se présentent quand une impression désagréable se dégage à l’examen d’un code. Nous exposons les code smells connus et vous expliquons comment s’en débarrasser.

Qu’est-ce qu’un code smell ?

Un code smell (traduit littéralement par « mauvaise odeur ») est un aspect du code qui insuffle aux programmeurs expérimentés l’impression immédiate que le code n’est « pas propre ». Avoir un bon « nez » pour détecter les code smells est comparable au talent d’un maître artisan. S’il observe des câbles enchevêtrés ou des conduites mal posées, il comprend immédiatement que les choses ont été mal faites. À la différence près que ces motifs évocateurs se présentent autrement dans un code source.

Le célèbre programmeur britannique Martin Fowler impute la création du terme à son collègue Kent Beck. Sur son propre blog, Fowler fournit la définition suivante :

Citation

« A Code Smell is a surface indication that usually corresponds to a deeper problem in the system. » – Source : https://martinfowler.com/bliki/CodeSmell.html
Traduction : « Un code smell est un indice superficiel, qui s’accompagne généralement d’un problème plus profond dans le système. » (traduit par IONOS)

Un code smell signale l’existence d’un problème systématique. Soit le code a été écrit par un programmeur présentant un manque de compétence ou par une équipe de programmation à forte rotation. Dans ce dernier cas, le degré variable de compétence, de connaissance du codebase et de connaissance des directives et des normes débouche sur la qualité déficiente du code. Le code commence à sentir mauvais.

Un code smell ne doit pas être confondu avec des déficits ponctuels parsemant inévitablement chaque codebase. À première vue, un code smell n’est rien d’autre qu’un simple indice. Quand un programmeur perçoit un code smell, il est important de vérifier si un problème systématique existe réellement.

Si le code semble ainsi déficient, il existe différentes façons de « nettoyer » le code. On parle souvent à ce stade d’approches de refactoring ou de « réusinage » du code en français, qui servent à améliorer la structure du code tout en conservant sa fonctionnalité. Selon la portée et la complexité du code, il peut toutefois s’avérer impossible de supprimer les code smells. Seule la « réécriture » du code, c’est-à-dire tout reprendre de zéro, peut résoudre ce problème.

Quels sont les types de code smells ?

Le code est un support qui se déploie sur différents niveaux d’abstraction. Prenons l’exemple d’une application. Elle se compose d’un certain nombre d’éléments, qui sont tous définis dans le code : il s’agit des variables, fonctions, classes, modules. Pour examiner les différents code smells, nous distinguons les niveaux d’abstraction :

  1. code smells généraux
  2. code smells au niveau de la fonction
  3. code smells au niveau de la classe
  4. code smells au niveau de l’application

Code smells généraux

Les code smells généraux se retrouvent à tous les niveaux d’une application. Ils ne constituent pas d’erreurs à part entière la plupart du temps, mais ce sont des schémas qui exercent une influence négative. Une règle de base dans la programmation stipule que le code devrait être écrit principalement pour des observateurs humains. Même les programmeurs inexpérimentés écrivent souvent du code qui est à même de fournir le résultat désiré, mais qui n’en demeure pas moins incompréhensible.

Dénomination incorrecte des structures de code

Nommer les structures de code individuelles, telles que les variables et les fonctions, est un grand art. Malheureusement, le code est souvent rempli de noms dénués de sens, absurdes, voire contradictoires. Un parfait exemple en sont les noms de variable constitués d’une seule lettre : x, i, n, etc. Comme ces noms ne précisent aucun contexte, leur but et leur signification restent un mystère. Il est préférable d’utiliser des noms expressifs. Ceci permet de lire le code comme un langage naturel et il est plus facile de suivre le flux du programme et d’identifier les incohérences logiques. Voici le même code avec un nom bien choisi :

Pas de style de codage uniforme

Un code correctement construit se lit d’une seule traite : on remarque immédiatement que le code a été créé en respectant certaines règles. Si une telle rigueur fait défaut, nous sommes en présence d’un code smell. Les divergences observées dans la dénomination indique que plusieurs personnes ont écrit des parties du code chacune de leur côté. Quand il s’agit d’une équipe, ceci peut être dû à un manque de spécification uniforme.

Conseil

Avons-nous éveillé votre curiosité ? Consultez également notre article « Le Clean Code, c’est quoi ? ».

Variables non définies

Certains langages, tels que C++, Java et JavaScript, vous permettent de déclarer une variable sans spécifier de valeur initiale. La variable existe à partir de la déclaration, mais n’est pas définie. Cela peut entraîner des bogues subtils. Ce code smell a des implications particulièrement fatales dans des langages au typage flou. Car en l’absence de définition initiale, il est impossible d’identifier le type attendu pour la variable.

Valeurs codées en dur

Les programmeurs inexpérimentés commettent souvent cette erreur : pour comparer une variable avec une valeur spécifique, ils entrent la valeur directement. On parle ici de « valeurs codées en dur ». Les valeurs codées en dur sont problématiques dès lors qu’elles apparaissent à plusieurs endroits du programme. Les copies individuelles ont tendance à muter indépendamment les unes des autres au fil du temps. Il est préférable de définir une constante pour la valeur. On établit ainsi une Single Source of Truth (SSOT, « source unique de vérité ») : tout code nécessitant la valeur accède à la seule et même constante définie à un seul endroit.

Nombres magiques dans le code

Un nombre magique ou magic number est un cas spécial de valeurs codées en dur. Imaginons que notre code utilise une valeur limitée à 24 bits. 24 bits correspondent à 16 777 216 valeurs possibles ou aux nombres de 0 à 16 777 215.

En l’absence de commentaire, il sera malheureusement difficile de cerner la provenance de cette valeur à une date ultérieure : un nombre magique est né. Le manque de connaissances sur l’origine de la valeur augmente le risque de modification accidentelle.

On peut éviter ces sources d’erreur en incluant la définition de la valeur comme une expression dans l’affectation de la constante.

Instructions de contrôle profondément imbriquées

Dans la plupart des langages, aucune limite n’est imposée dans la profondeur des structures de code implantées. Malheureusement, cela augmente également la complexité, ce qui complique la lecture et la compréhension du code. Un code smell très courant est l’imbrication profonde des instructions de contrôle dans des boucles et des ramifications. Plusieurs approches existent pour aplanir l’imbrication, par exemple recourir à un opérateur booléen AND afin de tenir compte des deux conditions dans une instruction IF. Une autre approche qui fonctionne avec autant de conditions que souhaité fait appel à des clauses de garde.

Note

Évaluer un code comme « profondément imbriqué » tient lieu du jugement subjectif. Ce critère s’applique d’une manière générale : les instructions de contrôle devraient être imbriquées sur un maximum de trois niveaux, ou quatre à titre exceptionnel. Il n’est pratiquement jamais recommandé ni nécessaire d’imbriquer le code à des niveaux plus profonds. Si la tentation de réaliser des imbrications plus profondes se présente, il est temps de refactoriser le code.

Code smells au niveau de la fonction

Dans la plupart des langages, les fonctions sont l’unité d'exécution de base du code. Il existe des sections de code plus petits, comme les variables et les expressions, mais ceux-ci sont autonomes. Écrire proprement des fonctions est l’apanage d’une expérience affirmée en matière de programmation. Nous exposons quelques code smells parmi les plus courants ayant trait à la fonction.

Lacunes dans le traitement des exceptions

Les fonctions contiennent des valeurs d’entrée sous la forme d’arguments. Dans de nombreux cas de figure, seules certaines valeurs ou plages de valeurs sont valides. Il incombe au responsable de la programmation de vérifier la validité des valeurs d’entrée et de gérer les exceptions en conséquence.

Accès à la variable à partir d’un domaine de validité parent dans la fonction

Les domaines de validité sont une caractéristique fondamentale de la plupart des langages de programmation. Elles spécifient les noms définis à des endroits précis du code. Ce faisant, les domaines de validité enfant héritent des domaines parent. Quand on accède à une variable définie en externe au sein d’une fonction, nous sommes en présence d’un code smell. Car la valeur peut avoir changé entre la définition de la variable et l’appel de la fonction.

L’idéal serait d’accéder à des valeurs qui sont passées en tant qu’arguments au sein des fonctions. Pour éviter de transmettre la même valeur à plusieurs reprises, il suffit de définir des paramètres par défaut.

Note

Accéder à une variable située dans le domaine de validité d’une fonction enveloppante génère une closure ou « fermeture » en français. Ceci ne correspond pas à un code smell. Les closures sont un concept important dans la programmation JavaScript.

Code smells au niveau de la classe

La programmation orientée objet (OOP) peut concourir à augmenter la réutilisation du code et à réduire la complexité. Malheureusement, la conception de classe est sujette à de multiples erreurs, qui prendront ultérieurement la forme d’un code smell.

L’un des codes smells les plus courants au niveau de la classe est le God object ou la « classe dieu » correspondante. Un God object combine toutes sortes de fonctionnalités éparses et viole ainsi le principe de la « séparation des préoccupations » ou Separation of Concerns en anglais.

Les code smells sont récurrents dans le contexte de la hiérarchie d’héritage. Parfois, le code est inutilement distribué sur plusieurs niveaux d’héritage. L’erreur de recourir aux relations d’héritage au lieu de la composition comme approche principale pour assembler des objets est souvent commise.

Dans la même veine, l’utilisation par inadvertance de « Data Classes » est également considérée comme un code smell. Il s’agit ici de classes qui n’implémentent pas leur propre comportement. Si aucune méthode autre que les getters et setters génériques n’est définie, envisagez d’utiliser une structure de données plus simple, telle qu’un dict ou une struct.

Code smells au niveau de l’application

C’est le cauchemar de toute personne amenée à programmer : on est chargé(e) de travailler sur une application existante, et le premier examen du code montre que l’application entière réside dans un énorme fichier unique. La manière dont les éléments constitutifs du code interagissent est confuse. Comme sur une assiette de nouilles, tout est sens dessus dessous dans un « code spaghetti ».

Toute forme d’abstraction fait défaut, il n’existe aucune répartition en classes, et tout au mieux peu de fonctions. Il existe toutes sortes de codes redondants, ce qui entraîne des bogues subtils. On observe de même une prolifération de variables globales.

L’utilisation de variables globales est un code smell particulièrement répandu. Comme les variables globales peuvent être potentiellement modifiées n’importe où dans le code, la porte est grande ouverte pour disséminer des erreurs difficiles à corriger. Lors du débogage, on se voit obligé de parcourir tout le code pour dénicher les bogues.

Un autre code smell spécifique à l’application est le manque de séparation des préoccupations. Il est aisé de mélanger le balisage, la fonctionnalité et le rendu dans un seul fichier, particulièrement dans le cadre des projets PHP. Cela rend difficile la réutilisation et le refactoring du code. Il est préférable de viser une nette séparation des préoccupations, par exemple en s’appuyant sur le modèle MVC (Model View Controller).

Comment supprimer un code smell ?

En soi, il est préférable de veiller à la propreté du code de manière permanente. La meilleure façon de procéder est d’adopter le schéma suivant :

  1. Prototype : créer un brouillon
  2. Test : tester la fonctionnalité
  3. Refactoriser : nettoyer le code
  4. Ship : livrer le code à l’environnement de production

Malheureusement, le troisième point est souvent ignoré, surtout lorsque la décision revient à un ou une responsable sans expérience du codage. Le raisonnement est le suivant : le code fonctionne, alors pourquoi y investir encore des efforts ? Au fil du temps, le code commence à « puer » ; des dettes techniques s’accumulent.

Si un codebase existant contient des code smells, il n'est pas possible de tout recommencer à zéro. Il faut alors réusiner le code. Il est préférable d’adopter une approche incrémentielle et de délimiter les zones à l’intérieur du codebase qui sont relativement faciles à nettoyer. Vous séparez les composantes opérationnelles ou pouvant être améliorées des « coins sombres », qu’il est préférable de laisser en paix.

Délimiter les zones de code les moins affectées des zones les plus affectées par les code smells réduit la complexité. Cela facilite le test du code et l’exécution des étapes de débogage. Des outils de revue de code automatisée qui traquent les code smells et proposent des suggestions et/ou des aides au nettoyage peuvent faciliter ce processus.

Les approches de refactoring sont efficaces pour éliminer les code smells. Il s’agit d’encapsuler ou d’éclater la fonctionnalité dans des fonctions. La suppression des sections de code inutilisées et l’amélioration du nommage éclaircissent le codebase. On peut s’appuyer ici sur les principes de base « Don’t Repeat Yourself » (DRY) ou « ne pas se répéter » et « You ain’t gonna need it » (YAGNI) ou « Tu n’en auras pas besoin ».