- Même lorsqu’un code est excellent sur le plan logique, il peut devenir pénible à lire dans la durée ; cet article attribue cette fatigue à la complexité visible à l’œil nu
- Les Halstead Complexity Metrics comptent le nombre d’opérateurs et d’opérandes pour calculer le Volume et la Difficulty, et considèrent qu’une implémentation comportant davantage de variables et d’opérateurs est plus difficile, même à comportement identique
- La Cognitive Complexity de SonarSource évalue la charge liée aux instructions abrégées, aux ruptures de flux linéaire et aux flux de contrôle imbriqués, notamment les conditions, boucles, exceptions, combinaisons d’opérateurs logiques, la récursion et
goto - Du côté des variables, le masquage de variables (variable shadowing), les noms similaires, les longues durées de vie des variables et les usages inhabituels augmentent le coût de suivi du flux de données pour le lecteur
- Petites fonctions, motifs familiers, regroupement des longues chaînes, conditions simples, usage limité de
goto, imbrication faible, noms de variables distincts et durées de vie courtes des variables constituent des critères de lisibilité applicables au-delà des langages et du formatage
Limites et objectifs des métriques de lisibilité du code
- Il n’existe pas de métrique unique, largement utilisée et consensuelle pour la lisibilité du code
- Les références disponibles sont surtout des articles académiques peu utilisés en pratique ou des opinions ; il fallait donc des outils de discussion plus concrets, directement utilisables en revue de code
- L’objectif n’est pas d’inventer une nouvelle métrique, mais de rassembler des motifs visuels que chacun peut utiliser lorsqu’il s’agit de discuter de la facilité de lecture d’un code
- Les mesures ou idées examinées devaient répondre aux critères suivants
- Être applicables à un fragment de code source ou à une fonction unique
- Ne pas se concentrer uniquement sur la complexité essentielle, difficile à séparer de l’algorithme implémenté lui-même, comme la Cyclomatic Complexity
- Ne pas se limiter à des éléments de style superficiels comme la longueur des noms de variables, les espaces, l’indentation ou le placement des parenthèses
Halstead Complexity Metrics
- Maurice Halstead a proposé à la fin des années 1970 les Halstead Complexity Metrics afin de créer des mesures empiriques du code source
- Ces métriques peuvent s’appliquer au-delà des langages et des plateformes, et se concentrent davantage sur la forme dans laquelle le code est écrit que sur l’algorithme implémenté lui-même
- Les mesures de base sont quatre décomptes fondés sur les opérateurs et les opérandes
- nombre d’opérateurs distincts
n1 - nombre d’opérandes distincts
n2 - nombre total d’opérateurs
N1 - nombre total d’opérandes
N2
- nombre d’opérateurs distincts
- À partir de cela, Halstead a créé des métriques associées comme la length, le volume et la difficulty, et a même tenté d’en déduire une estimation du nombre de bugs dans une implémentation
- Intuitivement, plus il y a d’opérateurs, plus il faut prendre en compte d’interactions potentielles ; plus il y a d’opérandes, plus il devient difficile de comprendre les flux de données possibles
-
Exemple JavaScript
- Même pour une fonction de test pair/impair, une implémentation simple utilisant
ifetreturncontient peu d’opérateurs et d’opérandes - 4 opérateurs distincts, 7 opérateurs au total
- 5 opérandes distincts, 6 opérandes au total
Volume33,30,Difficulty2,50- Une implémentation utilisant un tableau,
Number, une expression de comparaison et un index comporte davantage d’opérateurs et d’opérandes - 7 opérateurs distincts, 10 opérateurs au total
- 9 opérandes distincts, 12 opérandes au total
Volume71,35,Difficulty3,75- La première implémentation paraît aussi plus simple à l’œil, et les valeurs de
Volumeet deDifficultyde Halstead le confirment - L’inconvénient est qu’il n’est pas toujours évident, dans tous les langages, de déterminer ce qui doit être considéré comme operator ou operand ; pour mesurer, il vaut mieux choisir un outil ou une implémentation précise et l’utiliser de manière cohérente
- Même pour une fonction de test pair/impair, une implémentation simple utilisant
-
Motifs pratiques tirés de Halstead
- Plus une fonction est petite et contient peu de variables, plus elle est généralement facile à lire
- Les opérateurs propres à un langage et le sucre syntaxique ajoutent une charge supplémentaire pour le lecteur ; mieux vaut donc éviter d’en abuser
- Enchaîner longuement des composants fonctionnels comme
map,reduce,filter, des lambdas, des itérateurs ou des compréhensions peut réduire la lisibilité, même si le code est concis - Ces longues chaînes peuvent se rencontrer plus souvent en JavaScript et en Rust, ou dans du code Python très poussé avec
itertools
La difficulté de lecture selon la Cognitive Complexity
- La Cognitive Complexity, créée par SonarSource, est une métrique destinée à mieux saisir ce qui rend le code difficile à lire
- L’idée centrale de cette métrique repose sur trois points
- Les syntaxes abrégées qui combinent des instructions réduisent la difficulté
- Chaque écart par rapport au flux linéaire augmente la difficulté
- Les flux de contrôle imbriqués augmentent la difficulté
- Son nom peut être critiqué parce qu’il sonne comme une mesure scientifique ou objective, mais, en pratique, on peut y voir une heuristique efficace
-
Le problème de densité des syntaxes abrégées
- Une syntaxe abrégée comme
MyObj myObj = a?.myObj;est plus courte et demande moins de temps de lecture queif (a != null) { myObj = a.myObj; } - Cependant, ces deux morceaux de code peuvent ne pas être totalement équivalents en pratique
- Dans le premier,
myObjdevienta.myObjounull - Dans le second,
myObjdevienta.myObjouundefined - Même les langages à typage fort comme TypeScript ou Rust ne font que réduire la probabilité d’oubli ; ils ne garantissent pas que tous les cas soient correctement traités
- Avec un langage comme JavaScript standard, où la vérification de types est limitée, ces cas limites ont davantage de chances de ne pas être traités
- Les syntaxes abrégées peuvent être faciles à écrire et à lire, mais il existe un compromis entre concision et densité
- Une syntaxe abrégée comme
-
Éléments qui rompent le flux linéaire
- Un code linéaire sans condition est plus facile à parcourir qu’un code avec conditions
- La Cognitive Complexity considère comme facteurs d’augmentation de la difficulté non seulement les conditions, les boucles et
goto, mais aussi les macros conditionnelles,try/except, les séquences d’opérateurs logiques et la récursion switchest compté comme un seul groupe, tandis qu’une chaîne deelse-ifest considérée comme plus difficile à chaqueelse-ifsupplémentaire- Cela tient au fait que chaque branche
else-ifpeut effectuer deux comparaisons ou plus - Toutefois, le fall-through d’un
switchet lesbreakmanquants peuvent aussi accroître la difficulté de lecture - Les cas où l’on enchaîne le même opérateur logique dans une condition et ceux où l’on mélange
&&,||et!présentent des niveaux de difficulté différents debug || verbose || consoleModeest une condition simpledebug || (verbose && consoleMode)se lit plus difficilement à cause du mélange d’opérateursdebug || !(verbose && consoleMode)devient encore plus complexe avec l’ajout de la négation
-
Exceptions et goto
try/catchaugmente la difficulté dans la Cognitive Complexity, mais plusieurs blocscatchne sont pas considérés comme plus difficiles qu’un seulcatch, tandis quetryetfinallysont ignorés- Le simple fait de lancer une exception peut aussi créer un coût de lecture
- Lorsque la gestion des exceptions franchit les frontières d’une fonction, la complexité des fonctions concernées s’entremêle
- Le lecteur doit chercher où cette exception est interceptée
gotoest généralement compté comme un élément qui augmente la difficulté- Cependant, certains experts estiment qu’une forme comme
goto outougoto done, qui libère des ressources dans un cas d’erreur avant de quitter la fonction, peut être utile - À l’inverse, un
gotoqui franchit les limites d’une boucle d’une manière impossible à exprimer aveccontinueoubreakimpose au lecteur de reconstruire un nouveau flux de contrôle, ce qui représente une charge de lecture importante
Imbrication et forme des fonctions
- Si une condition est en soi difficile à lire, une condition imbriquée l’est encore davantage
- La Cognitive Complexity ajoute, au score propre des conditions et boucles, une difficulté supplémentaire pour chaque niveau d’imbrication
- Cette idée est aussi appelée “Level of Indentation” ou “Bumpy Road”
- Lorsque l’imbrication dépasse deux niveaux, la lecture devient particulièrement difficile ; un code qui réduit l’imbrication grâce à des retours anticipés se lit de manière plus plate
- Cette métrique ne reflète pas directement la longueur d’une fonction, mais, toutes choses égales par ailleurs, une fonction longue demande plus d’effort de lecture qu’une fonction courte
Noms de variables, durée de vie et motifs familiers
-
Des noms distincts et explicites
- Les noms explicites sont importants pour comprendre ce que le code cherche à faire, tandis que les noms redondants ou cryptiques produisent l’effet inverse
- Le masquage de variables (variable shadowing) est dangereux
- Il faut éviter les situations où le lecteur doit raisonner sur les règles de portée pour distinguer quelle variable est utilisée
- Il faut aussi éviter les identifiants visuellement similaires
- Des noms faciles à confondre visuellement, comme
ietj, ouitemetitems, peuvent provoquer des erreurs - Un code qui utilise dans une même fonction plusieurs variantes d’un même nom de variable, comme
node,_node,thisNode, est une forme propice aux bugs
-
Durée de vie courte des variables
- L’analyse des variables vivantes (live variable analysis) considère la plage qui va du premier endroit où une variable est utilisée jusqu’au dernier endroit où elle peut l’être
- Lorsque la durée de vie d’une variable est longue, le lecteur doit garder davantage de variables et de valeurs possibles en tête
- Déclarer les variables juste avant leur utilisation effective peut réduire leur durée de vie, par rapport à une déclaration de toutes les variables en haut de la fonction
- Le pire cas est celui d’une variable qui vit à travers plusieurs fonctions et est utilisée à plusieurs endroits
- Si plusieurs variables ont en pratique la même durée de vie, un objet peut être plus approprié
- Si un objet ne convient pas, il vaut mieux minimiser le nombre de fonctions et de lignes que le lecteur doit lire pour comprendre les valeurs
-
Longues chaînes et variables intermédiaires
- Le style de programmation fonctionnelle raccourcit la durée de vie des variables, mais des chaînes trop longues ou une imbrication de callbacks peuvent créer une charge de lecture
- Découper les longues chaînes de fonctions en petits groupes, et utiliser des variables intermédiaires ou des fonctions d’aide bien nommées, peut réduire la charge cognitive du lecteur
- La version avec variables intermédiaires peut être légèrement moins efficace
- Tant qu’un outil de performance n’indique pas que cette ligne constitue réellement un goulot d’étranglement, ces micro-différences d’efficacité n’ont pas d’importance
-
Réutilisation de motifs de code familiers
- Réutiliser des formes de code et de variables familières permet au lecteur de reconnaître des motifs qu’il connaît déjà, ce qui rend la lecture moins fatigante
- Cela rejoint le Principle of Least Surprise
- Dans une codebase, il vaut mieux conserver de manière cohérente les formes récurrentes, comme la manière d’écrire les conditions
- Lorsqu’il faut s’écarter d’un motif, les noms de variables ou les commentaires peuvent mettre en évidence la différence
- En poussant cette idée jusqu’au bout, on en vient à utiliser des fonctions template ou génériques pour que le lecteur n’ait pas à reconnaître à nouveau des motifs répétés
8 motifs visuels pour améliorer la lisibilité
- Line/Operator/Operand count : des fonctions plus petites et moins de variables et d’opérateurs sont plus faciles à lire
- Novelty : éviter la nouveauté dans la forme des fonctions, les opérateurs et le sucre syntaxique, et réutiliser les motifs communs de la codebase
- Grouping : diviser les longues chaînes de fonctions, itérateurs et compréhensions en groupes logiques au moyen de fonctions d’aide ou de variables intermédiaires
- Conditional simplicity : garder les conditions aussi courtes que possible et, dans une même condition, préférer une séquence du même opérateur logique plutôt que mélanger différents opérateurs
- Gotos : ne pas utiliser
goto, sauf lorsqu’il suit un motif particulier de gestion d’erreur et que les alternatives sont moins bonnes - Nesting : minimiser la logique imbriquée et les grands changements d’indentation ; si une imbrication profonde est nécessaire, l’extraire dans une fonction distincte plutôt que l’enfouir dans une grande fonction
- Variable distinction : utiliser des noms de variables explicites et visuellement distincts, et éviter le masquage de variables
- Variable liveness : garder des durées de vie de variables courtes, en faisant particulièrement attention aux longues durées de vie qui traversent les frontières de fonctions
Problèmes observés dans une vraie codebase
- Les codebases qui généraient une forte fatigue mentale combinaient plusieurs anti-patterns
- Concrètement, elles comportaient de longues fonctions, un mélange de nombreuses constructions du langage et beaucoup de chaînes de fonctions qui auraient dû être séparées dans des fonctions d’aide
- Résultat : dans de grandes fonctions, la complexité d’imbrication et les longues durées de vie des variables augmentaient ensemble
- Malgré la grande qualité du code et de ses auteurs, au moins un bug critique a été découvert
- L’un d’eux était un bug facile à voir, mais il était probablement passé inaperçu parce qu’il se trouvait au milieu d’une fonction longue et complexe, ce qui le rendait difficile à raisonner
- Dans un mois, la personne la plus susceptible de relire votre code sera vous-même ; écrire sous une forme facile à lire est donc directement lié aux coûts pratiques du travail
1 commentaires
Avis de Hacker News
L’affirmation de l’article selon laquelle des chaînes map/reduce/filter longues ou multiples nuisent à la lisibilité ne découle absolument pas du reste
On dirait une critique courante glissée discrètement parce que ce n’est pas familier, alors qu’avec un peu d’habitude, c’est souvent bien plus facile à lire et à écrire que les alternatives
Par exemple, je vois mal quel code serait plus lisible que
books.filter(book => book.pageCount > 1000).map(book => book.author).distinct()Même d’après les métriques de complexité, ce genre de code est meilleur que presque n’importe quelle autre approche, et il faut apprendre au moins les bases de la programmation fonctionnelle. Pas besoin d’aller jusqu’à expliquer les monades, mais il faut être assez à l’aise pour ne pas dénigrer aveuglément
mapetfilterSelon moi, cela commence à devenir difficile à lire à partir d’une chaîne d’environ 5 appels, et l’exemple de l’article était de cette longueur
« multiple » désignait les cas où il y a des chaînes imbriquées ou où le type manipulé change, ce qui ralentit la lecture
Les composants fonctionnels peuvent être élégants, mais on peut aussi en abuser
Quand la chaîne devient trop longue, que les conditions se complexifient et qu’il y a beaucoup de listes/variables, cela devient difficile à comprendre d’un seul coup
Dans une boucle procédurale, on peut donner un nom aux résultats intermédiaires, et ce nom permet d’oublier le traitement précédent pour se concentrer sur l’étape suivante
SELECT DISTINCT author FROM books WHERE pageCount > 1000;Personnellement, j’ai tendance à découper les chaînes pour donner un nom aux résultats intermédiaires, comme si les noms de variables servaient de commentaires
var longBooks = books.filter(book => book.pageCount > 1000)var authorsOfLongBooks = longBooks.map(book => book.author).distinct()?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).Selon la structure de la base de données Prolog, cela peut même être plus court
?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).Je pense qu’un bon code comporte fondamentalement une part assez importante qualitative et littéraire
Les programmeurs ou universitaires habitués à la pensée mathématique, qui veulent des réponses quantitatives, sont souvent mal à l’aise avec cet aspect
J’aime à la fois Dostoïevski et Wodehouse : les deux sont excellents, mais de manière très différente
Même si le code n’est pas un champ aussi ouvert, j’ai travaillé sur de bonnes bases de code qui donnaient des impressions qualitativement très différentes, et il faut souvent du temps pour « sentir » le style d’une codebase, comme lorsqu’on s’habitue au style d’un nouvel auteur
Cela voulait dire que les fonctions étaient ordonnées de façon à ce qu’en lisant le fichier de haut en bas, le récit se déroule naturellement, avec une implémentation déclarative qui semblait s’adresser au lecteur
Je suis le paradigme de la programmation fonctionnelle pure, et je trouve qu’il se prête mieux à ce style plus narratif
Les dépendances/entrées d’une fonction se limitent à ses arguments ou à d’autres fonctions pures, et sa sortie n’est contenue que dans son type de retour, ce qui facilite le guidage progressif à travers la complexité
Contrairement à d’autres paradigmes qui comportent des complexités comme l’état caché, je pense ironiquement que le paradigme le plus mathématiquement précis est aussi celui qui convient le mieux à un style plus narratif
Peu importe sa forme : si l’on ne peut pas comprendre ce que fait une fonction dans un délai raisonnable, sans longue expérience de développement, c’est tout simplement du mauvais code
Par exemple, l’opérateur ternaire cité dans l’article,
return n % 2 === 0 ? 'Even' : 'Odd';, se lit à l’envers pour le cerveau humain, et convient davantage au compilateur pour traiter l’arbre syntaxique qu’à une personneUn mathématicien humain écrirait plutôt une fonction définie par morceaux, du type : si
n mod 2 = 0alors'Even', sinon'Odd', ce qui est bien plus clairElles aident les nouveaux membres de l’équipe à assimiler un style cohérent, et maintiennent aussi le style de l’équipe dans une cohérence raisonnable
Le commentaire sur
.editorconfigmérite aussi d’être consulté : https://news.ycombinator.com/item?id=43333011Cela réduit les débats sur les détails de style dans les pull requests
L’article est bon, mais je trouve qu’il passe à côté de l’élément le plus mentalement fatigant quand on lit du code : la mutabilité
Pouvoir « figer » une seule fois le sens d’une variable en lisant une méthode, puis le conserver tel quel pendant qu’on déduit le reste, c’est un vrai cadeau
La compréhension d’une méthode devrait progresser de façon monotone de 0 % à 100 %, sans qu’on ait besoin de redémarrer mentalement la méthode parce qu’on ne sait plus comment le corps de la boucle a modifié l’accumulateur à telle itération
La vraie raison pour laquelle GOTO est nuisible est là aussi. Le problème n’est pas qu’il soit difficile de déplacer mentalement le pointeur d’instruction dans une méthode, mais qu’avec GOTO il devient difficile de connaître l’état des variables mutables
Les variables mutables comme immuables peuvent aider ou gêner ce déplacement ; tout dépend de la netteté avec laquelle le code correspond à cet espace
Les variables immuables ont un petit avantage tactique, puisqu’on n’a pas à craindre que leur valeur change ou change d’une manière trompeuse, mais d’après mon expérience ce n’est pas assez important pour en faire une règle du type « utilisez toujours l’immutabilité »
Parfois, la mutabilité permet de représenter cet espace d’information de façon beaucoup plus propre
Du point de vue de l’appelé, et non du site d’appel, si quelqu’un peut sauter vers une ligne donnée, on ne peut pas retracer ce qui s’est passé avant. Comme cela peut venir de n’importe où, il faut une analyse globale du programme, pas une analyse locale
Si la mutabilité était la vraie source de la complexité de GOTO, les instructions
ifet les bouclesfordevraient poser le même problèmeJe suis d’accord pour dire que la mutabilité et l’état créent directement de la complexité, mais je pense que GOTO appartient à une catégorie complètement différente et bien plus nuisible
Un pattern que je n’aime personnellement pas est celui qui consiste à retourner directement dans un
ifet à laisser le reste comme chemin par défaut impliciteif (n % 2 === 0) return "Even"; return "Odd";est certes plus court, mais je préfère largementif ... return "Even"; else return "Odd";La raison est que le premier donne une impression d’asymétrie.
"Even"et"Odd"sont des choix symétriques, donc la version avecelseest plus intuitivereturn (n % 2 === 0) ? "Even" : "Odd";est ce qu’il y a de plus lisible, parce que c’est celle qui contient le moins de boilerplateDans un langage doté d’un opérateur ternaire, cela devrait être facile à reconnaître
D’expérience,
elseajoute de l’imbrication et pousse facilement à continuer d’évaluer des conditions limites ou des variables au-delà de la portée immédiate du chemin normal. Il faut garder tout ce contexte en têteOn voit visuellement le retour avec un seul niveau d’indentation juste sous le nom de la fonction, et s’il n’y a pas de sortie anticipée, cela donne l’impression qu’un résultat est garanti
Quand le retour est enfoui plus bas, je trouve ça un peu bizarre
return "Odd";soit visuellement séparé duif, et si le langage le permet, j’ajouterais aussi des accolades au corps duifIl y a des cas où
elsese justifie, mais c’est généralement lorsqu’il y a des effets de bord ; le plus souvent, refactorer jusqu’à le supprimer rend le code plus clairIl est courant qu’un code complexe se transforme en séquence de gardes qui sortent par ordre d’importance ou de coût d’exécution, ce qui sépare la logique réelle de la fonction/méthode des conditions de terminaison
La première approche s’écoule vers le bas et exécute le second ensemble d’instructions.
returnest un opérateur spécial qui interrompt le flot de contrôle ; le flot de contrôle ordinaire de la première approche ne représente donc pas correctement la complétude des deux casEn Rust idiomatique, on n’utilise pas
returnsauf dans les cas exceptionnels qui rompent le flot de contrôle d’une méthode, et le second exemple apparaît généralement plus souvent sans instruction de retourEn Python aussi, on fait généralement des retours anticipés au début en cas d’arguments ou d’état invalides, puis le retour en position finale devient la vraie valeur de retour
À cause de ces conventions, casser une structure
if-elsecomplète donne à unreturnindenté l’apparence d’une situation exceptionnelle. Si l’on suit cette convention, les instructionsreturnfinissent naturellement par sembler redondantes sauf lorsqu’elles interrompent le flot de contrôle, et la convention de Rust devient compréhensible. Dans tous les langages,returnest une instruction équivalente àbreakPeut-être que je suis le seul, mais TypeScript rend parfois le code difficile à lire
Tant qu’on garde le modèle de données relativement « atomique » et que les développeurs déclarent réellement les types et les documentent avec sérieux, ça va
Mais dès que des types commencent à être dérivés d’autres types via des types utilitaires, et qu’on omet les types explicites pour s’appuyer sur l’inférence, les choses se défont rapidement
Dans une pile profonde, avec 4 ou 5 niveaux d’indirection de types, il devient très difficile de suivre l’origine d’un champ. Certains sont inférés, d’autres explicites, certains sont des types dérivés, et des alias de champs s’y mêlent aussi
Avec de grands modèles de données et des piles d’appels profondes, une forme comme
function checkDogs(dogs: Dog[]) { ... }sans type de retour explicite devient totalement inutilisable et vraiment exaspéranteLa raison principale est de forcer tous les chemins de retour de cette fonction à respecter ce type
J’ai vu beaucoup de régressions causées par l’ajout d’une nouvelle condition qui renvoyait un type légèrement différent de celui des autres branches
En revanche, je ne vois pas beaucoup d’intérêt à annoter les déclarations de variables
Dans l’exemple,
const checkedDoggos = checkDogs([])est très bien, et il suffit de laissercheckedDoggoshériter du type de la fonctionJe travaille sur une base de code où le linter impose
const checkedDoggos: DogBreedAndSize[] = checkDogs([]), et c’est assez ridicule, avec peu de valeur ajoutéeEn JavaScript, on ne peut pas être sûr, donc il faut sans cesse monter et descendre dans la pile et garder tout ça en tête
Il faut se méfier de l’affirmation selon laquelle « les petites fonctions avec peu de variables sont généralement plus faciles à lire »
Je n’aime pas que les discussions sur la lisibilité se concentrent uniquement sur la lisibilité microscopique. Cela conduit facilement à découper le code de façon excessive, sous l’hypothèse erronée que la lisibilité microscopique serait plus importante que la lisibilité macroscopique
Ce genre de dogmatisme produit des programmeurs qui voient les arbres sans voir la forêt, et engendre du code excessivement inefficace ou difficile à déboguer
Les langages de la famille APL sont à l’extrême opposé, mais le véritable optimum se situe probablement quelque part au milieu, et varie sans doute beaucoup selon les personnes
Dans du code qu’on ne connaît pas, devoir aller à la définition 3 ou 4 fois suffit déjà à devenir pénible ; c’est peut-être moi, mais j’ai du mal à imaginer que la plupart des gens s’en sortent beaucoup mieux que moi
Dans la culture .NET, en particulier autour de la « clean architecture », ce problème atteint un niveau choquant. Quand on veut modifier une fonctionnalité ou remonter la piste d’un problème, c’est réparti sur 4 couches et 15 fichiers, dont certains sont composés à plus de 60 % de mots-clés
Je ne sais pas où tracer la limite, mais je préfère en général une longue fonction qui suit les autres bonnes pratiques et se lit séquentiellement, plutôt qu’un code tellement fragmenté qu’il faut faire défiler vers le haut et vers le bas toutes les 5 lignes
C’est pareil pour les types/classes : inutile de mettre dans un autre fichier un enum de 4 valeurs qui n’est utilisé que dans ce DTO
Article intéressant, mais pas entièrement satisfaisant
Il saute trop vite aux conclusions et revient à la préférence personnelle. Je suis d’accord avec plusieurs préférences, mais l’article lui-même cherchait explicitement à dépasser les préférences
Dire que « les opérateurs propres à un langage ou le sucre syntaxique font peser une charge sur le lecteur et doivent donc être évités » ne découle pas des métriques. Si une fonction contient 3 opérateurs différents, et qu’un opérateur propre au langage peut remplacer les trois d’un coup, alors « l’effort » de la fonction diminue
Des composants comme
map/reduce/filter, bien utilisés, peuvent aussi remplacer d’autres opérateurs et réduire le « volume » ; cela peut donc aller dans un sens comme dans l’autreL’exemple de
?.semble être un diagnostic très spécifique à JavaScript, qui pointe surtout une conception du langage difficile à lire. Dans beaucoup de langages,nulletundefinedne sont pas séparés, et on parle souvent de null-safe operator« Le masquage de variables (variable shadowing) est horrible » et « une durée de vie plus longue oblige à garder davantage de variables en tête » peuvent entrer en conflit
Dans certains contextes, j’aime beaucoup le masquage de variables, parce qu’il retire l’ancienne instance du scope au lieu de la laisser accessible en permanence
Il existe un excellent plugin pour VS Code appelé Highlight
Il permet d’appliquer différentes couleurs au code avec des expressions régulières personnalisées, et l’usage courant est sans doute de mettre
//TODOen jauneJe m’en sers pour estomper les logs, parce qu’à force d’en mettre un peu partout, ils génèrent beaucoup de bruit visuel
La bibliothèque que je maintiens utilise des logs comme
this.logger?.info('Some logs here');, et j’y applique une opacité de 0,4 pour les repousser à l’arrière-planIls restent visibles, mais au premier coup d’œil, la logique métier ressort davantage
La configuration peut être adaptée comme suit :
"highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }https://marketplace.visualstudio.com/items?itemName=fabiospa...
Je ne suis pas d’accord avec l’idée selon laquelle découper de longues chaînes de fonctions ou de callbacks en petits groupes avec des variables bien nommées serait un peu moins efficace
Les deux versions peuvent être tout aussi efficaces
Dans les deux cas, les mêmes objets sont alloués, stockés sur le tas et soumis au ramasse-miettes. La différence d’efficacité dépend du compilateur
Dans la deuxième version, le compilateur devrait pouvoir voir que chaque variable n’est utilisée qu’immédiatement après sa déclaration, et traiter ces objets comme hors scope, comme dans un appel chaîné
À condition, bien sûr, de laisser le type de la variable être inféré
Le coût qu’on observe réellement apparaît quand on matérialise explicitement les valeurs intermédiaires pour les voir dans le débogueur. Par exemple, les transformer en liste introduit des allocations évitables, donc un coût
Vouloir quantifier la « lisibilité » est une bonne chose. Il nous faut beaucoup plus d’approches de ce genre
Aujourd’hui, la définition la plus courante de la lisibilité ressemble surtout à « ce que moi je trouve facile à lire »
On pourrait peut-être découvrir les dimensions réelles de la lisibilité en montrant du code à un très grand nombre de personnes, en leur demandant de choisir une phrase décrivant ce que fait ce code, et en chronométrant le tout
Les exercices que le plus grand nombre de personnes réussissent avec le temps moyen le plus court deviendraient des exemples de code lisible dans le monde réel et, plus important encore, aideraient à identifier les pratiques réellement difficiles à lire
Les répondants se regrouperaient probablement selon des axes comme « expérience en programmation » ou « compréhension du paradigme X », et les résultats pourraient évoluer avec le temps à mesure que les modes changent
Ce qu’on a appris à lire et à écrire façonne ce que l’on perçoit comme facile à lire
Ce qu’on essaie de faire, les personnes avec qui l’on travaille, ce que l’on savait faire avant de coder, les autres langages que l’on connaît, etc., influencent beaucoup les choses
Une fois cueillis les fruits les plus bas — par exemple, au-delà du simple fait d’éviter de donner aux variables des noms arbitraires, sans rapport ou trompeurs —, beaucoup de problèmes de « lisibilité » pourraient finalement relever de la construction d’un consensus
Il n’existe peut-être pas de bonne réponse qui transcende le groupe précis de programmeurs avec lequel on cherche à travailler
La lisibilité du code ressemble à la lisibilité d’une langue : c’est surtout un problème pour les personnes qui ne connaissent pas bien cette langue, et cela se résout avec du temps
Le vrai problème en programmation, c’est la complexité du code, et on ne peut pas l’évaluer uniquement avec des métriques portant sur des fragments de code isolés
Le problème se situe dans les relations entre les fonctions, plus que dans les choix d’implémentation à l’intérieur du corps d’une fonction
Comprendre ce que fait le code est généralement facile ; ce qui est difficile, c’est de le modifier ou d’ajouter une fonctionnalité
Cette difficulté vient du fait que plusieurs couches d’abstraction masquent la manière dont elles sont reliées entre elles