Licence CC BY

Des constantes sans gâchis

Parfois, les « valeurs magiques » sont une bonne idée

« Extrayez cette valeur dupliquée n fois dans une constante », « Assignez cette valeur magique à une constante bien nommée et utilisez la constante à la place »… si vous avez fait du développement, vous avez sans doute reçu ces injonctions un jour ou l’autre, de la part de votre IDE, d’un collègue, d’un outil d’analyse statique de code ou autre.

Mais est-ce que ces conseils devraient s’appliquer à l’instant, de façon systématique et sans réfléchir ? Évidemment, non1. Mais se pencher dessus va nous permettre de voir ce qu’est une constante créée de façon intelligente.

Une constante utile c’est une constante bien nommée

… et une constante bien nommée, c’est une constante dont le nom est fonctionnel. Qui indique à quoi sert la constante, et ne décrit pas son contenu. Pour savoir son contenu, il suffit d’aller voir sa valeur, merci.

C’est hélas un problème que l’on rencontre très souvent, soit par ignorance, soit par manque d’idée jamais corrigé.

Par exemple, ceci est une constante mal nommée. Elle ne nous apprends rien de plus que sa valeur et ne rends pas le code plus lisible quand elle est utilisée :

const val COLON = ":"

Pire, si sa valeur doit être changée, on peut se retrouver avec ce genre d’aberration.

const val COLON = ";"

Ce qui donne toute une catégorie de bugs très pénibles à détecter, parce le code ne correspond plus à ce qu’il prétend être.

Un meilleur nommage serait que la constante indique ce à quoi sert sa valeur :

const val LINUX_PATH_SEPARATOR = ":"

Ce qui a plusieurs avantages : on sait à quoi sert la valeur quand elle est utilisée dans le code (c’est une forme d’auto-documentation – d’ailleurs, il faudra qu’on parle de la documentation un jour…), et surtout ça permet d’éviter des réutilisations ou des modifications hors de propos. Dans ce cas, un développeur du projet sous Windows évitera de remplacer sauvagement cette valeur par ; pour que le projet tourne chez lui, parce que la constante indique explicitement que le cas d’utilisation est Linux2.

D’ailleurs, en parlant de réutilisation sauvage…

Ces deux valeurs identiques ont-elles besoin d’être une même constante ?

C’est très fréquent que les outils d’analyse de code indiquent qu’une valeur est dupliquée et devrait être factorisée dans une constante unique. Mais ces valeurs sont-elles un réel duplicata, ou est-ce une coïncidence qui ne devrait pas être factorisée ?

Vérifiez toujours avant de regrouper des valeurs, sous peine de faire comme ce gros éditeur3 qui avait une seule clé de traduction pour l’anglais « second », qui peut se traduire en français comme « seconde » (si on parle de l’unité temporelle ») ou « deuxième » (si c’est le nombre ordinal) ; et donc qui affichait :

Machine virtuelle démarrée depuis 0 deuxième(s)

Notez que les constantes bien nommées aident beaucoup à éviter ce genre de cas, parce qu’elles produisent du code absurde à la relecture :

// Toutes les constantes contiennent le caractère ':'
// Ça a l’air légitime, mais la constante est mal nommée
val csvEntries = csvLine.split(COLON)

// On renomme la constante par ailleurs...
val csvEntries = csvLine.split(LINUX_PATH_SEPARATOR)
// Oups.

// Avec une nouvelle constante bien nommée elle aussi :
val csvEntries = csvLine.split(CSV_ENTRIES_SEPARATOR)

Cette valeur est-elle réellement une « valeur magique »

Certains outils d’analyse statique – et, plus gênant, certains humains – demandent à l’extraction dans une constante de toute valeur non « triviale »4

Mais est-ce que l’extraction dans une constante va réellement améliorer la lisibilité et la maintenance du code ? Ça n’est pas garanti du tout.

Prenons par exemple cette classe de mapping de configuration. Les valeurs en dur dans le code sont les valeurs par défaut quand il n’y a aucune configuration particulière fournie, et ceci est un fait bien connu des développeurs qui ont accès à cette classe.

@ConfigurationProperties("fr.spacefox.example.data")
public class SampleDataConfiguration {

    private @NotNull Duration refreshRate = Duration.ofMinutes(5);
}

Et-ce vraiment utile d’écrire ceci pour satisfaire un outil ou un collègue mal lunés ?

@ConfigurationProperties("fr.spacefox.example.data")
public class SampleDataConfiguration {

    // On est d’accord pour dire que la syntaxe de déclaration de constantes en Java est complètement nulle ?
    private static final int DEFAULT_REFRESH_RATE_IN_MINUTES = 5;

    private @NotNull Duration refreshRate = Duration.ofMinutes(DEFAULT_REFRESH_RATE_IN_MINUTES);
}

… et des exemples du même genre, il y en a plein.

Et je ne parle même pas d’absurdités comme celles-ci (tirée d’exemples réels…), où la constante est à la fois mal nommée et moins lisible que la valeur qu’elle remplace :

@ConfigurationProperties("fr.spacefox.example.data")
public class SampleDataConfiguration {

    private static final int EMPTY_STRING = "";

    private @NotNull String defaultName = EMPTY_STRING;
}

Bonus : où ranger ses constantes ?

C’est une bonne question à laquelle il n’y a pas de réponse universelle. Certains les rangent dans d’immenses classes de constantes, d’autres les éparpillent au besoin ; certains ne jurent que par les interface de constantes, d’autres par les class de constantes, etc.

La seule vraie règle est : soyez cohérents. N’utilisez qu’un seul système au sein du même projet.


  1. Si vous avez lu les autres billets de la « série » en cours, vous avez sans doute déjà compris que la réponse à cette question est toujours « non ». Dans le cas contraire, lisez « La « configuration » par langage dédié (DSL), une invention de Satan », « Une erreur de configuration = un plantage », « Vous n’avez (probablement) pas besoin de cette interface » et « Ne générez pas vos getters et setters ! », chez le même éditeur, pas cher.
  2. On me rétorquera que le développeur capable de changer la valeur d’une constante de façon à ce qu’elle ne corresponde plus à son nom ignorera tout aussi bien les indications données par ce nom… ce qui est hélas probable, en effet. Mais si ça n’arrête pas le mauvais développeur, ça aidera tout de même à la correction du problème engendré par icelui.
  3. VMware, avec vSphere
  4. En général la chaine vide et les nombres 0 et 1 sont considérés comme acceptables en dur dans le code, le reste est considéré comme devant être remplacé par « une constante bien nommée ».


Les constantes, dans l’ensemble, c’est bien, ça apporte plein de facilités, de sûreté et d’auto-documentation au code, et même parfois un peu de performances (même si ça ne devrait pas être le but premier).

Vous devriez apporter un soin tout particulier au nommage de vos constantes, sans quoi elles deviendront des boulets plutôt que des aides. De plus, ça n’est pas parce qu’on vous dit « mettez une constante ici » qu’il faut le faire sans réfléchir. Peut-être que ça n’est pas utile. Peut-être que ça n’est pas la constante que vous pensiez réutiliser en premier lieu.


Licence de l’icône : un truc aussi simple n’a rien d’une œuvre de l’esprit et ne peut donc pas prétendre à un quelconque copyright ou une quelconque forme de licence. C’est donc du domaine public.

6 commentaires

J’aime beaucoup la licence de l’icône …qui me fait penser à un certain tableau (voir la majorité d'une certaine expo) incidemment refait(e) par d’autres avant et après.

Fin de digression, on peut revenir à nos constantes. Il me semble qu’il y a des trucs prévus comme File.pathSeparatorChar qui évite de devoir passer par des COLON et autres joyeusetés.

+0 -0

qui évite de devoir passer par des COLON

à part une sonde, je vois pas…

Blague à part, l’aspect "magique" d’une constante est toujours un peu litigieux. Surtout avec les vieilles API de java. Prenons l’exemple de l’API de gestion d’image. Elle te retourne un tableau de byte. pixel[0] est toujours rouge, pixel[1] est toujours vert, pixel[2] est toujours bleu et quand tu le demandes pixel[3] est toujours le canal alpha.

Les outils de vérification statiques du code te disent que 2 et 3 sont des constantes magiques. Alors certes, on peut tout à fait créer une constante BLUE_CANAL_INDEX et ALPHA_CANAL_INDEX mais du coup, pour cohérence on doit aussi le faire pour red et green. Et la plus value en terme de lisibilité est minime, je dirais même pire, on rallonge alors que le contrat était très clair à la base. La constante est "presque magique" mais tellement connue qu’elle s’apparente à la variable "i" dans les boucles for.

Les outils de vérification statiques du code te disent que 2 et 3 sont des constantes magiques. Alors certes, on peut tout à fait créer une constante BLUE_CANAL_INDEX et ALPHA_CANAL_INDEX mais du coup, pour cohérence on doit aussi le faire pour red et green. Et la plus value en terme de lisibilité est minime, je dirais même pire, on rallonge alors que le contrat était très clair à la base. La constante est "presque magique" mais tellement connue qu’elle s’apparente à la variable "i" dans les boucles for.

Je pense que c’est un cas où justement cela me semble pertinent. C’est je pense assez facile de se tromper à la lecture ou à l’écriture en cas de fatigue, mais aussi quand tu débutes un peu et que tu n’as pas ce réflexe, cela ajoute de la complexité à la tâche.

Cela me fait penser aux permissions UNIX, 0x4 = lecture, 0x2 = écriture, 0x1 = exécution à décliner en trois exemplaires. Quand tu as l’habitude, ces nombres sont automatiques et tu t’en sors bien. Mais ce n’est pas le cas de tout le monde et tu peux vite introduire une erreur.

C’est d’ailleurs un sujet récurrent dans le noyau : utiliser des macros pour cacher ces nombres ou utiliser les valeurs en dur directement ? Exemple : https://lwn.net/Articles/696227/

Mais c’est pareil parfois avec les protocoles où des champs ont une valeur particulière ou des valeurs de registres. Quand tu as l’habitude que tu connais le truc par coeur, avoir la valeur directement visible dans le code peut faire gagner du temps. Mais cela rend la tâche pour ceux qui passe derrière plus difficile. D’autant plus que parfois une erreur se glisse et n’est pas vue car une constante en dur n’a pas de signification justement. Et si le cerveau passe outre, ça se passe mal.

Cela m’arrive d’avoir des constantes en dur qui ne sont pas 0 ou 1, mais très très rarement et il faut vraiment que cela ne pose pas de problème.

+0 -0

Pour avoir bossé avec du code antique qui utilisait massivement des tableaux ou des listes d’objets là où il y aurait dû avoir des objets ad hoc pour stocker les données, je dirais que l’exemple de l’API de gestion d’images est pour moi plutôt le genre de cas qui mériterait des constantes. Ou, plus exactement, qui mériterait un objet du type record Color(byte red, byte green, byte blue, byte alpha) mais ça n’existait pas à l’époque et ça peut poser des problèmes pour des raisons techniques et/ou de performances.

On peut s’en sortir avec des constantes « locales » avec des noms simplifiés : RED, GREEN, BLUE, ALPHA, ou même R, G, B, A – si on peut les mettre dans une classe bien nommée qui permet de les préfixer si le contexte est douteux.

Mais là on est dans une API standard et plutôt connue, c’est le genre de cas qui se gère a la merge request, en fonction de l’équipe, de comment l’API est utilisée, etc.

Le vrai danger (rencontré dans du code en production) c’est quand on a ce genre de montage sur du code custom, qui vit et qui bouge, et qu’après on se retrouvre avec des appels de méthodes du genre :

Object[] data = loadFromDatabase([...]);
return new Dto(data[1], data[3], data[4], data[0], data[2]);

Inversement j’ai vu ce genre de chose (relativement courant dans les API qui gèrent des listes mais dont on sait qu’elles ne contiennent qu’une valeur, ou dont on ne veut gérer que la première valeur) :

List<String> values = computeValues();
String firstValue = null;
if (values.isEmpty()) {
    singleValue = values.get(0);
    // Commentaire de MR : mettre 0 dans une constante !
}

Une catégorie de constante inutile que j’ai oubliée dans ce billet, c’est les messages de logs déplacés dans une constante :

const val SOME_LOG_MESSAGE = "Whatever and a value: {}"

// [... snip a lot of lines ...]

LOGGER.log(SOME_LOG_MESSAGE, aValue)

Avec évidemment la constante qui n’est réutilisée nulle part. Ça n’apporte rien en performances et pourrit la lisiblité et la maintenance et obligeant à aller chercher la constante pour savoir ce qui est vraiment loggué, ou à chercher l’usage de la constante quand on cherche à retrouver d’où vient le log (il peut ne pas y avoir de numéro de ligne correct).

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte