Comment ne pas tuer un projet avec de mauvaises pratiques de codage ?
Bartosz Slysz
Software Engineer
De nombreux programmeurs en début de carrière considèrent que la question de la dénomination des variables, des fonctions, des fichiers et d'autres composants n'est pas très importante. En conséquence, leur logique de conception est souvent correcte - les algorithmes s'exécutent rapidement et produisent l'effet désiré, tout en étant à peine lisibles. Dans cet article, j'essaierai de décrire brièvement ce qui devrait nous guider pour nommer les différents éléments du code et comment ne pas passer d'un extrême à l'autre.
Pourquoi le fait de négliger l'étape du nommage prolongera (dans certains cas - énormément) le développement de votre projet ?
Supposons que vous et votre équipe s'emparent de la code d'autres programmeurs. Les projet you inherit a été développé sans aucun amour - il fonctionnait très bien, mais chacun de ses éléments aurait pu être écrit d'une bien meilleure manière.
En ce qui concerne l'architecture, l'héritage de code déclenche presque toujours la haine et la colère des programmeurs qui l'ont obtenu. Cela est parfois dû à l'utilisation de technologies mourantes (ou éteintes), parfois à une mauvaise façon de penser l'application au début du développement, et parfois simplement au manque de connaissances du programmeur responsable.
Quoi qu'il en soit, au fur et à mesure que le projet avance, il est possible d'arriver à un point où les programmeurs sont furieux contre les architectures et les technologies. Après tout, chaque application nécessite la réécriture de certaines parties ou simplement des changements dans des parties spécifiques après un certain temps - c'est naturel. Mais le problème qui fera grisonner les programmeurs est la difficulté à lire et à comprendre le code dont ils ont hérité.
Dans les cas extrêmes, lorsque les variables sont nommées à l'aide d'une seule lettre dépourvue de sens et que les fonctions sont le fruit d'une soudaine poussée de créativité, sans aucune cohérence avec le reste de l'application, vos programmeurs risquent de devenir fous furieux. Dans ce cas, toute analyse de code qui pourrait s'exécuter rapidement et efficacement avec un nommage correct nécessite une analyse supplémentaire des algorithmes responsables de la production du résultat de la fonction, par exemple. Et une telle analyse, bien que discrète, fait perdre énormément de temps.
L'implémentation de nouvelles fonctionnalités dans différentes parties de l'application implique de passer par le cauchemar de l'analyse du code. Après un certain temps, il faut revenir au code et l'analyser à nouveau parce que ses intentions ne sont pas claires et que le temps passé à essayer de comprendre son fonctionnement a été perdu parce que l'on ne se souvient plus de son objectif.
C'est ainsi que nous sommes aspirés dans une tornade de désordre qui règne sur l'application et consume lentement tous les participants à son développement. Les programmeurs détestent le projet, les chefs de projet détestent expliquer pourquoi son temps de développement commence à augmenter constamment, et le client perd confiance et s'énerve parce que rien ne se passe comme prévu.
Comment l'éviter ?
Soyons réalistes : certaines choses ne peuvent pas être ignorées. Si nous avons choisi certaines technologies au début du projet, nous devons être conscients qu'avec le temps, elles cesseront d'être prises en charge ou que de moins en moins de programmeurs maîtriseront les technologies d'il y a quelques années qui deviennent lentement obsolètes. Certaines bibliothèques, dans leurs mises à jour, nécessitent des changements plus ou moins importants dans le code, ce qui entraîne souvent un tourbillon de dépendances dans lequel vous pouvez vous retrouver encore plus coincé.
D'un autre côté, ce n'est pas un scénario aussi noir ; bien sûr, les technologies vieillissent, mais le facteur qui ralentit définitivement le temps de développement des projets qui les impliquent est en grande partie la laideur du code. Et bien sûr, nous devons mentionner ici le livre de Robert C. Martin - il s'agit d'une bible pour les programmeurs, dans laquelle l'auteur présente un grand nombre de bonnes pratiques et de principes qui devraient être suivis pour créer un code qui vise la perfection.
L'essentiel, lorsque l'on nomme des variables, est de transmettre clairement et simplement leur intention. Cela semble assez simple, mais c'est parfois négligé ou ignoré par de nombreuses personnes. Un bon nom précisera ce que la variable est censée stocker ou ce que la fonction est censée faire. Il ne faut pas que le nom soit trop générique, mais il ne faut pas non plus qu'il devienne une longue limace dont la simple lecture représente un véritable défi pour le cerveau. Après un certain temps passé avec un code de bonne qualité, nous expérimentons l'effet d'immersion, où nous sommes capables d'organiser inconsciemment le nommage et le passage des données à la fonction de telle sorte que l'ensemble ne laisse aucune illusion quant à l'intention qui l'anime et au résultat attendu en l'appelant.
Un autre élément que l'on peut trouver dans JavaScriptest, entre autres, une tentative de sur-optimisation du code, ce qui le rend souvent illisible. Il est normal que certains algorithmes nécessitent une attention particulière, ce qui reflète souvent le fait que l'intention du code peut être un peu plus alambiquée. Néanmoins, les cas dans lesquels nous avons besoin d'optimisations excessives sont extrêmement rares, ou du moins ceux dans lesquels notre code est sale. Il est important de se rappeler que de nombreuses optimisations liées au langage ont lieu à un niveau d'abstraction légèrement inférieur ; par exemple, le moteur V8 peut, avec suffisamment d'itérations, accélérer de manière significative les boucles. Il convient de souligner que nous vivons au XXIe siècle et que nous n'écrivons pas de programmes pour la mission Apollo 13. Nous disposons d'une marge de manœuvre beaucoup plus grande en ce qui concerne les ressources - elles sont là pour être utilisées (de préférence de manière raisonnable :>).
Parfois, la décomposition du code en plusieurs parties apporte beaucoup. Lorsque les opérations forment une chaîne dont le but est d'effectuer des actions responsables d'une modification spécifique des données, il est facile de se perdre. C'est pourquoi, de manière simple, au lieu de tout faire dans une seule chaîne, vous pouvez décomposer les différentes parties du code qui sont responsables d'une chose particulière en éléments individuels. Cela permettra non seulement de clarifier l'intention des opérations individuelles, mais aussi de tester des fragments de code qui ne sont responsables que d'une seule chose et qui peuvent être facilement réutilisés.
Quelques exemples pratiques
Je pense que la représentation la plus précise de certaines des déclarations ci-dessus sera de montrer comment elles fonctionnent en pratique - dans ce paragraphe, j'essaierai d'exposer quelques mauvaises pratiques de code qui peuvent plus ou moins être transformées en bonnes pratiques. Je soulignerai ce qui perturbe la lisibilité du code à certains moments et comment l'éviter.
Le fléau des variables à lettre unique
Une pratique terrible qui est malheureusement assez courante, même dans les universités, consiste à nommer les variables avec une seule lettre. Il est difficile de ne pas admettre qu'il s'agit parfois d'une solution pratique - nous évitons de réfléchir inutilement à la manière de déterminer l'objet d'une variable et, au lieu d'utiliser plusieurs caractères pour la nommer, nous nous contentons d'une seule lettre - par exemple, i, j, k.
Paradoxalement, certaines définitions de ces variables sont assorties d'un commentaire beaucoup plus long, qui détermine ce que l'auteur avait à l'esprit.
Un bon exemple serait de représenter l'itération sur un tableau à deux dimensions qui contient les valeurs correspondantes à l'intersection de la colonne et de la ligne.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]] ;
// plutôt mauvais
for (let i = 0 ; i < array[i] ; i++) {
for (let j = 0 ; j < array[i][j] ; j++) {
// voici le contenu, mais à chaque fois que i et j sont utilisés, je dois revenir en arrière et analyser à quoi ils servent.
}
}
// toujours mauvais mais amusant
let i ; // ligne
let j ; // colonne
for (i = 0 ; i < array[i] ; i++) {
for (j = 0 ; j < array[i][j] ; j++) {
// voici le contenu, mais à chaque fois que i et j sont utilisés, je dois revenir en arrière et vérifier dans les commentaires à quoi ils servent.
}
}
// beaucoup mieux
const rowCount = array.length ;
for (let rowIndex = 0 ; rowIndex < rowCount ; rowIndex++) {
const row = array[rowIndex] ;
const columnCount = row.length ;
for (let columnIndex = 0 ; columnIndex < columnCount ; columnIndex++) {
const column = row[columnIndex] ;
// est-ce que quelqu'un a des doutes sur ce qui est quoi ?
}
}
Sur-optimisation sournoise
Un beau jour, je suis tombé sur un code très sophistiqué écrit par un ingénieur logiciel. Cet ingénieur avait compris que l'envoi de permissions d'utilisateurs sous forme de chaînes de caractères spécifiant des actions spécifiques pouvait être grandement optimisé à l'aide de quelques astuces au niveau des bits.
Une telle solution serait probablement acceptable si la cible était le Commodore 64, mais le but de ce code était une simple application web, écrite en JS. Le temps est venu de surmonter cette bizarrerie : Disons qu'un utilisateur n'a que quatre options dans tout le système pour modifier le contenu : créer, lire, mettre à jour, supprimer. Il est assez naturel d'envoyer ces permissions sous forme JSON en tant que clés d'un objet avec des états ou d'un tableau.
Cependant, notre ingénieur astucieux a remarqué que le chiffre quatre est une valeur magique dans la présentation binaire et a trouvé la solution suivante :
Le tableau des capacités comporte 16 lignes, mais je n'en ai cité que 4 pour illustrer l'idée de la création de ces autorisations. La lecture des autorisations se fait comme suit :
Ce que vous voyez ci-dessus n'est pas Code WebAssembly. Je ne veux pas être mal compris ici - de telles optimisations sont normales pour les systèmes où certaines choses doivent prendre très peu de temps ou de mémoire (ou les deux). Les applications web, en revanche, ne sont certainement pas un endroit où de telles sur-optimisations ont un sens total. Je ne veux pas généraliser, mais dans le travail des développeurs frontaux, des opérations plus complexes atteignant le niveau d'abstraction des bits sont rarement effectuées.
Il est tout simplement illisible, et un programmeur capable d'analyser un tel code se demandera sûrement quels sont les avantages invisibles de cette solution et ce qui peut être endommagé lorsque le équipe de développement veut le réécrire pour en faire une solution plus raisonnable.
De plus, je soupçonne que l'envoi des permissions en tant qu'objet ordinaire permettrait à un programmeur de lire l'intention en 1 ou 2 secondes, alors que l'analyse de l'ensemble du code depuis le début prendra au moins quelques minutes. Il y aura plusieurs programmeurs dans le projet, chacun d'entre eux devra tomber sur ce morceau de code - ils devront l'analyser plusieurs fois, parce qu'après un certain temps, ils oublieront la magie qui s'y passe. Cela vaut-il la peine de sauvegarder ces quelques octets ? À mon avis, non.
Diviser pour mieux régner
Développement web se développe rapidement et rien n'indique que les choses vont bientôt changer à cet égard. Nous devons admettre que la responsabilité des développeurs frontaux a récemment augmenté de manière significative - ils ont pris en charge la partie de la logique responsable de la présentation des données dans l'interface utilisateur.
Parfois, cette logique est simple et les objets fournis par l'API ont une structure simple et lisible. Parfois, cependant, ils nécessitent différents types de mappage, de tri et d'autres opérations pour les adapter à différents endroits de la page. Et c'est là que nous pouvons facilement tomber dans le marécage.
Souvent, je me suis surpris à rendre les données des opérations que j'effectuais pratiquement illisibles. Malgré l'utilisation correcte des méthodes de tableaux et le nommage adéquat des variables, les chaînes d'opérations ont, à certains moments, presque perdu le contexte de ce que je voulais réaliser. De plus, certaines de ces opérations devaient parfois être utilisées ailleurs, et parfois elles étaient suffisamment globales ou sophistiquées pour nécessiter l'écriture de tests.
Je sais, je sais - il ne s'agit pas d'un morceau de code trivial qui illustre facilement ce que je veux transmettre. Je sais aussi que les complexités informatiques des deux exemples sont légèrement différentes, mais dans 99% des cas, nous n'avons pas à nous en soucier. La différence entre les algorithmes est simple : ils préparent tous deux une carte des emplacements et des propriétaires d'appareils.
Le premier prépare cette carte deux fois, alors que le second ne la prépare qu'une seule fois. Et l'exemple le plus simple qui nous montre que le second algorithme est plus portable réside dans le fait que nous devons modifier la logique de création de cette carte pour le premier algorithme et, par exemple, exclure certains lieux ou d'autres choses bizarres appelées logique d'entreprise. Dans le cas du deuxième algorithme, nous ne modifions que la manière d'obtenir la carte, tandis que toutes les autres modifications de données intervenant dans les lignes suivantes restent inchangées. Dans le cas du premier algorithme, nous devons modifier chaque tentative de préparation de la carte.
Et ce n'est qu'un exemple - dans la pratique, il existe de nombreux cas où nous devons transformer ou remanier un certain modèle de données pour l'ensemble de l'application.
La meilleure façon d'éviter de suivre les divers changements commerciaux est de préparer des outils globaux qui nous permettent d'extraire des informations d'intérêt d'une manière assez générique. Même au prix de ces 2-3 millisecondes que nous pourrions perdre au prix d'une désoptimisation.
Résumé
Le métier de programmeur est une profession comme une autre : chaque jour, nous apprenons de nouvelles choses et commettons souvent de nombreuses erreurs. La chose la plus importante est d'apprendre de ces erreurs, de devenir meilleur dans sa profession et de ne pas répéter ces erreurs à l'avenir. Vous ne pouvez pas croire au mythe selon lequel le travail que nous effectuons sera toujours parfait. Vous pouvez cependant, en vous basant sur l'expérience des autres, réduire les défauts en conséquence.
J'espère que la lecture de cet article vous aidera à éviter au moins quelques-uns des problèmes suivants mauvaises pratiques de codage que j'ai expérimentées dans mon travail. Pour toute question concernant les meilleures pratiques en matière de code, vous pouvez contacter The Codest équipage pour consulter vos doutes.