Vous lisez un extrait de mon livre sur le code propre, « Washing your code ». Disponible au format PDF, EPUB et en édition papier et Kindle. Obtenez votre exemplaire maintenant.
Un code intelligent est quelque chose que nous pouvons voir dans les questions d'entretien d'embauche ou les quiz linguistiques, lorsqu'ils s'attendent à ce que nous sachions comment fonctionne une fonctionnalité linguistique, que nous n'avons probablement jamais vue auparavant. Ma réponse à toutes ces questions est : "il ne passera pas la révision du code".
Certaines personnes confondent brèveté avec clarté. Le code court (brèveté) n’est pas toujours le code le plus clair (clarté), c’est souvent le contraire. S'efforcer de rendre votre code plus court est un objectif noble, mais cela ne doit jamais se faire au détriment de la lisibilité.
Il existe de nombreuses façons d'exprimer la même idée dans le code, et certaines sont plus faciles à comprendre que d'autres. Nous devons toujours viser à réduire la charge cognitive du prochain développeur qui lira notre code. Chaque fois que nous tombons sur quelque chose qui n’est pas immédiatement évident, nous gaspillons les ressources de notre cerveau.
Info : J'ai « volé » le nom de ce chapitre du livre de Steve Krug sur la convivialité du Web du même nom.
Regardons quelques exemples. Essayez de couvrir les réponses et devinez ce que font ces extraits de code. Ensuite, comptez combien vous avez deviné correctement.
Exemple 1 :
const percent = 5; const percentString = percent.toString().concat('%');
Ce code ajoute uniquement le signe % à un nombre et doit être réécrit comme :
const percent = 5; const percentString = `${percent}%`; // → '5%'
Exemple 2 :
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
Le symbole ~ est appelé l'opérateur NOT au niveau du bit. Son effet utile ici est qu'il renvoie une valeur fausse uniquement lorsque indexOf() renvoie -1. Ce code doit être réécrit comme :
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
Exemple 3 :
const value = ~~3.14;
Une autre utilisation obscure de l'opérateur NOT au niveau du bit consiste à supprimer la partie fractionnaire d'un nombre. Utilisez Math.floor() à la place :
const value = Math.floor(3.14); // → 3
Exemple 4 :
if (dogs.length + cats.length > 0) { // Something fishy here… }
Celui-ci est compréhensible au bout d'un moment : il vérifie si l'un des deux tableaux contient des éléments. Cependant, il vaut mieux que ce soit plus clair :
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
Exemple 5 :
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
Celui-ci m’a mis du temps à comprendre. Imaginez que nous ayons une partie d'une URL, telle que filename="pizza". Tout d'abord, nous divisons la chaîne par = et prenons la deuxième partie, "pizza". Ensuite, on tranche le premier et le dernier personnages pour obtenir une pizza.
J'utiliserais probablement une expression régulière ici :
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
Ou, mieux encore, l'API URLSearchParams :
const header = 'filename="pizza.rar"'; const filename = new URLSearchParams(header) .get('filename') .replaceAll(/^"|"$/g, ''); // → 'pizza'
Ces citations sont cependant bizarres. Normalement, nous n'avons pas besoin de guillemets autour des paramètres d'URL, donc parler au développeur backend pourrait être une bonne idée.
Exemple 6 :
const percent = 5; const percentString = percent.toString().concat('%');
Dans le code ci-dessus, on ajoute une propriété à un objet lorsque la condition est vraie, sinon on ne fait rien. L'intention est plus évidente lorsque nous définissons explicitement les objets à déstructurer plutôt que de nous appuyer sur la déstructuration de fausses valeurs :
const percent = 5; const percentString = `${percent}%`; // → '5%'
Je préfère généralement que les objets ne changent pas de forme, je déplacerais donc la condition à l'intérieur du champ de valeur :
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
Exemple 7 :
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
Ce merveilleux one-liner crée un tableau rempli de nombres de 0 à 9. Array(10) crée un tableau avec 10 éléments vides, puis la méthode keys() renvoie les clés (nombres de 0 à 9) en tant qu'itérateur, que nous convertissons ensuite en un tableau simple en utilisant la syntaxe spread. Emoji tête qui explose…
On peut le réécrire en utilisant une boucle for :
const value = ~~3.14;
Autant j'aime éviter les boucles dans mon code, autant la version boucle est plus lisible pour moi.
Quelque part au milieu, on utiliserait la méthode Array.from() :
const value = Math.floor(3.14); // → 3
Le Array.from({length: 10}) crée un tableau avec 10 éléments non définis, puis en utilisant la méthode map(), nous remplissons le tableau avec des nombres de 0 à 9.
Nous pouvons l'écrire plus court en utilisant le rappel de carte d'Array.from() :
if (dogs.length + cats.length > 0) { // Something fishy here… }
Explicit map() est légèrement plus lisible, et nous n'avons pas besoin de nous rappeler ce que fait le deuxième argument de Array.from(). De plus, Array.from({length: 10}) est légèrement plus lisible que Array(10). Mais seulement légèrement.
Alors, quel est votre score ? Je pense que le mien serait vers 3/7.
Certains modèles se situent à la frontière entre intelligence et lisibilité.
Par exemple, utiliser Boolean pour filtrer les éléments de tableau faux (null et 0 dans cet exemple) :
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
Je trouve ce modèle acceptable ; même si cela nécessite un apprentissage, c'est mieux que l'alternative :
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
Cependant, gardez à l'esprit que les deux variantes filtrent les valeurs fausses, donc si les zéros ou les chaînes vides sont importants, nous devons filtrer explicitement les valeurs indéfinies ou nulles :
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
Quand je vois deux lignes de code délicat qui semblent identiques, je suppose qu'elles diffèrent d'une certaine manière, mais je ne vois pas encore la différence. Sinon, un programmeur créerait probablement une variable ou une fonction pour le code répété au lieu de le copier-coller.
Par exemple, nous avons un code qui génère des identifiants de test pour deux outils différents que nous utilisons sur un projet, Enzyme et Codeception :
const header = 'filename="pizza.rar"'; const filename = new URLSearchParams(header) .get('filename') .replaceAll(/^"|"$/g, ''); // → 'pizza'
Il est difficile de repérer immédiatement les différences entre ces deux lignes de code. Vous vous souvenez de ces paires d'images où il fallait trouver dix différences ? C'est ce que ce code fait au lecteur.
Bien que je sois généralement sceptique quant au SÉCHAGE extrême du code, c'est un bon cas en ce sens.
Info : Nous parlons davantage du principe Ne vous répétez pas dans le chapitre Diviser pour mieux régner, ou fusionner et détendre.
const percent = 5; const percentString = percent.toString().concat('%');
Maintenant, il ne fait aucun doute que le code des deux identifiants de test est exactement le même.
Regardons un exemple plus délicat. Supposons que nous utilisions différentes conventions de dénomination pour chaque outil de test :
const percent = 5; const percentString = `${percent}%`; // → '5%'
La différence entre ces deux lignes de code est difficile à remarquer, et nous ne pouvons jamais être sûrs que le séparateur de nom (- ou _) est la seule différence ici.
Dans un projet avec une telle exigence, ce motif apparaîtra probablement à de nombreux endroits. Une façon de l'améliorer est de créer des fonctions qui génèrent des identifiants de test pour chaque outil :
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
C'est déjà bien mieux, mais ce n'est pas encore parfait : le code répété est encore trop volumineux. Réparons cela aussi :
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
Il s'agit d'un cas extrême d'utilisation de petites fonctions, et j'essaie généralement d'éviter autant de diviser le code. Cependant, dans ce cas, cela fonctionne bien, surtout s'il existe déjà de nombreux endroits dans le projet où nous pouvons utiliser la nouvelle fonction getTestIdProps().
Parfois, un code qui semble presque identique présente des différences subtiles :
const value = ~~3.14;
La seule différence ici est le paramètre que nous passons à la fonction avec un nom très long. Nous pouvons déplacer la condition à l'intérieur de l'appel de fonction :
const value = Math.floor(3.14); // → 3
Cela élimine le code similaire, ce qui rend l'extrait entier plus court et plus facile à comprendre.
Chaque fois que nous rencontrons une condition qui rend le code légèrement différent, nous devons nous demander : cette condition est-elle vraiment nécessaire ? Si la réponse est « oui », nous devrions nous poser à nouveau la question. Souvent, il n’y a pas de réel besoin pour une condition particulière. Par exemple, pourquoi devons-nous même ajouter séparément les ID de test pour différents outils ? Ne pouvons-nous pas configurer l’un des outils pour utiliser les identifiants de test de l’autre ? Si nous creusons suffisamment, nous découvrirons peut-être que personne ne connaît la réponse ou que la raison initiale n'est plus pertinente.
Considérez cet exemple :
if (dogs.length + cats.length > 0) { // Something fishy here… }
Ce code gère deux cas extrêmes : lorsque AssetsDir n'existe pas et lorsque AssetsDir n'est pas un tableau. De plus, le code de génération d'objet est dupliqué. (Et ne parlons pas des ternaires imbriqués…) On peut se débarrasser de la duplication et d’au moins une condition :
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
Je n'aime pas que la méthode castArray() de Lodash encapsule undéfini dans un tableau, ce qui n'est pas ce à quoi je m'attendais, mais le résultat est quand même plus simple.
CSS a des propriétés abrégées et les développeurs en abusent souvent. L’idée est qu’une seule propriété peut définir plusieurs propriétés en même temps. Voici un bon exemple :
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
Ce qui équivaut à :
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
Une ligne de code au lieu de quatre, et ce qui se passe est toujours clair : nous définissons la même marge sur les quatre côtés d'un élément.
Maintenant, regardez cet exemple :
const percent = 5; const percentString = percent.toString().concat('%');
Pour comprendre ce qu'ils font, nous devons savoir que :
Cela crée une charge cognitive inutile et rend le code plus difficile à lire, à modifier et à réviser. J'évite de tels raccourcis.
Un autre problème avec les propriétés abrégées est qu'elles peuvent définir des valeurs pour des propriétés que nous n'avions pas l'intention de modifier. Prenons cet exemple :
const percent = 5; const percentString = `${percent}%`; // → '5%'
Cette déclaration définit la famille de polices Helvetica, la taille de police de 2rem, et rend le texte en italique et en gras. Ce que nous ne voyons pas ici, c'est que cela modifie également la hauteur de la ligne à la valeur par défaut, normale.
Ma règle générale est d'utiliser les propriétés abrégées uniquement lors de la définition d'une seule valeur ; sinon, je préfère les propriétés longues.
Voici quelques bons exemples :
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
Et voici quelques exemples à éviter :
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
Bien que les propriétés abrégées rendent effectivement le code plus court, elles le rendent souvent beaucoup plus difficile à lire, alors utilisez-les avec prudence.
Éliminer les conditions n’est pas toujours possible. Cependant, il existe des moyens de faciliter la détection des différences dans les branches de code. L'une de mes approches préférées est ce que j'appelle le codage parallèle.
Considérez cet exemple :
const value = ~~3.14;
C'est peut-être une bête noire personnelle, mais je n'aime pas lorsque les déclarations de retour sont à des niveaux différents, ce qui les rend plus difficiles à comparer. Ajoutons une instruction else pour résoudre ce problème :
const value = Math.floor(3.14); // → 3
Maintenant, les deux valeurs de retour sont au même niveau d'indentation, ce qui les rend plus faciles à comparer. Ce modèle fonctionne lorsqu'aucune des branches de condition ne gère les erreurs, auquel cas un retour anticipé serait une meilleure approche.
Info : Nous parlons des retours anticipés dans le chapitre Conditions à éviter.
Voici un autre exemple :
if (dogs.length + cats.length > 0) { // Something fishy here… }
Dans cet exemple, nous avons un bouton qui se comporte comme un lien dans le navigateur et affiche un modal de confirmation dans une application. La condition inversée pour la prop onPress rend cette logique difficile à voir.
Rendons les deux conditions positives :
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
Maintenant, il est clair que nous définissons soit des accessoires onPress, soit des liens en fonction de la plate-forme.
Nous pouvons nous arrêter ici ou aller plus loin, en fonction du nombre de conditions Platform.OS === 'web' dans le composant ou du nombre d'accessoires que nous devons définir de manière conditionnelle
Nous pouvons extraire les accessoires conditionnels dans une variable distincte :
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
Ensuite, utilisez-le au lieu de coder en dur l'intégralité de la condition à chaque fois :
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
J'ai également déplacé l'accessoire cible vers la branche Web car il n'est de toute façon pas utilisé par l'application.
Quand j’avais la vingtaine, me souvenir des choses n’était pas vraiment un problème pour moi. Je pouvais me souvenir des livres que j'avais lus et de toutes les fonctions d'un projet sur lequel je travaillais. Maintenant que j’ai la quarantaine, ce n’est plus le cas. J'apprécie désormais le code simple qui n'utilise aucune astuce ; J'apprécie les moteurs de recherche, l'accès rapide à la documentation et les outils qui m'aident à raisonner sur le code et à naviguer dans le projet sans tout garder en tête.
Nous ne devrions pas écrire du code pour nous-mêmes aujourd’hui, mais pour ce que nous serons dans quelques années. Réfléchir est difficile et la programmation en demande beaucoup, même sans avoir à déchiffrer un code délicat ou peu clair.
Commencez à penser à :
Si vous avez des commentaires, mastodontez-moi, tweetez-moi, ouvrez un ticket sur GitHub ou envoyez-moi un e-mail à artem@sapegin.ru. Obtenez votre exemplaire.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!