France IOI

Comment coder en évitant les bugs

Mathias Hiron pour www.france-ioi.org

Introduction

Face à un problème, la plupart des programmeurs passent un peu de temps à réfléchir, se lancent rapidement dans la programmation de leur idée, pressés de voir le résultat. Une fois le programme terminé, ils passent aux tests et à l'inévitable débuggage, sur lequel ils finissent souvent par passer plus de 80% du temps total de développement.

Le débuggage est la partie la plus improductive et la plus rébarbative du développement d'un programme. Il peut arriver de passer des heures à tester / modifier / bidouiller son programme pour trouver le bug, qui souvent, se ramène à la modification d'un simple caractère, un signe inversé, un <= qui aurait dû être un simple <, etc.

Le rêve du programmeur, c'est d'écrire du code qui marche du premier coup sans le moindre bug. Bien sûr, l'erreur est humaine et il est quasiment impossible d'écrire tous ses programmes sans bug du premier coup. Le débuggage n'est cependant pas une fatalité et s'il est impossible de s'en passer entièrement, il est possible de réduire considérablement le temps que l'on y passe. D'une part en réduisant le nombre de bugs initial de ses programmes, d'autre part en facilitant la recherche des bugs restants.

Pour éviter de passer 80% de son temps au débuggage, il n'y a pas de secret : il faut passer plus de temps sur la réflexion et sur l'écriture initiale du code. D'autres cours expliquent comment mieux réfléchir avant de passer à l'écriture du code lui-même.

Dans ce document, nous nous concentrons sur ce qu'il est possible de faire lors de l'écriture d'un programme, quel que soit le langage utilisé (y compris le pseudo-code), pour réduire au minimum les risques d'erreurs et faciliter le débuggage.

Les conseils que nous vous proposons ici sont basés sur quelques principes clés :

1) Votre priorité doit être d'écrire le code le plus simple possible

Inutile de chercher à optimiser chaque ligne, ou à gagner du temps dans l'écriture. Prenez le temps de trouver la manière la plus simple possible d'implémenter votre algorithme, cela vous en fera gagner beaucoup plus lors du débuggage.

2) Rendez votre code le plus facile à lire possible.

Un programme bien écrit doit pouvoir se lire et se comprendre comme du français (ou de l'anglais pour ceux qui préfèrent). Dans l'idéal, chaque ligne peut être comprise et vérifiée sans lire le reste du code.

3) Prenez de bonnes habitudes

Beaucoup de programmes ont des choses en commun. Pour éviter de vous reposer les mêmes questions à chaque fois, prenez des habitudes qui vous feront gagner du temps.

A. Bien nommer ses variables et fonctions

1) Nommer quoi et pourquoi

Pour éviter les erreurs dans un programme, il est important que celui-ci soit facile à comprendre. Pour comprendre une ligne de code, il faut pouvoir comprendre chacun de ses éléments. Si ceux-ci ne sont composés que de symboles ou d’identifiants arbitraires (i, j, k, ...) dont il faut retrouver ou mémoriser le rôle, déterminer précisément ce que fait cette ligne nécessite un effort qui diminue notre capacité à trouver les éventuelles erreurs.

Pour faciliter au maximum le débuggage du code, chaque ligne doit être la plus simple possible. L’idéal est une instruction mettant en oeuvre très peu d’éléments, chaque élément pouvant être compris au premier coup d’oeil.

Le meilleur moyen pour s’approcher de cet idéal, consiste à utiliser pour chacun des éléments principaux d’une instruction, un nom clair indiquant ce que cet élément représente. De nombreux éléments d'un programme peuvent être nommés, en suivant les conseils suivants :

  • Remplacer les expressions par des variables
  • Si par exemple une instruction utilise la distance entre deux points, on peut calculer cette distance séparément et stocker le résultat dans une variable bien nommée. La lecture de l’instruction se fait alors plus facilement, car elle ne nécessite pas de chercher à quoi correspond le calcul.

  • Remplacer les valeurs numériques par des constantes ou énumérations
  • Il est bien moins facile de comprendre à quoi correspond 0x8080FF que la constante nommée BLEU_FONCE. Le code sera beaucoup plus facile à lire si l’on identifie des directions par une énumération composée de Haut, Bas, Droite, Gauche, que si l’on se contente de valeurs comme 1, 2, 3 et 4.

  • Remplacer les groupes d’instructions par des fonctions
  • Lorsqu’un traitement demande plusieurs instructions, il vaut mieux le séparer du reste dans une fonction. Là où on l’utilise, on a alors un nom de fonction qui permet de voir au premier coup d’oeil ce que fait le traitement et de quoi il dépend. Evitez de faire des fonctions contenant plus d'une quinzaine d'instructions. Ce n'est pas une limite stricte, mais il est généralement préférable de les découper en plusieurs parties.

  • Remplacer certains tableaux par des structures
  • Par exemple, plutôt que d’utiliser un tableau de deux cases pour stocker les coordonnées d’un point, préférez une structure contenant deux champs nommés x et y. Il est plus facile de comprendre point.x et point.y que point[0] et point[1].

  • Ne pas trop compter sur les commentaires
  • Pour expliquer ce que fait un code, on peut ajouter des commentaires, mais cela a beaucoup d’inconvénients. Ils indiquent clairement ce que l’on voulait faire avec le code qui suit, mais pas ce que ce code fait réellement : le commentaire ou le code peuvent être faux, ou pas à jour. Mieux vaut écrire du code qui se comprend facilement sans commentaires. On peut éventuellement en mettre avant le code de chaque fonction.

2) Comment choisir un nom

Le nom d’une variable, d’une structure ou d’une fonction n’est pas à choisir à la légère. Plus il est clair, plus il aidera à comprendre rapidement toutes les instructions qui l’utilisent et à détecter les bugs. N’hésitez pas à passer du temps à chercher un bon nom, cela vous fera gagner du temps pour la suite.

Un identifiant doit permettre de comprendre au premier coup d’oeil ce qu’il représente. Pour le choisir, imaginez que vous avez à expliquer en français à quelqu’un ce que l’élément représente et tirez-en les mots les plus significatifs. Ne cherchez pas à économiser les caractères (sans tomber dans l'excès inverse).

Pour choisir un bon nom, posez-vous différentes questions, suivant le type d’élément :

  • pour une variable de stockage : que contient-elle ?
  • Essayez d’être très précis : s’il s’agit du résultat d’un calcul de distance, nommez la variable distance, plutôt que résultat.

  • pour un tableau : que contient une case ?
  • Evitez à tout prix d’appeler un tableau tab, simplement parce que c’est un tableau. La syntaxe d’utilisation (par exemple avec l’indice entre crochets après le nom, en C), suffit à voir que c’est un tableau. Concentrez-vous donc sur ce que représente la valeur stockée dans chaque case.

  • pour un indice : qu’est-ce qu’il énumère ?
  • Plutôt qu’un nom flou comme indice ou position, choisissez un nom qui met l’accent sur ce qui est énuméré. Par exemple ligne, colonne, temps, ou coup.

  • pour une fonction : que renvoie-t-elle ?
  • Lorsqu’une fonction renvoie une valeur, elle est susceptible d’être appelée au sein d’une expression qui fait intervenir le résultat. Nommer la fonction par ce que représente la valeur retournée permettra de mieux comprendre ce que fait l’instruction qui l’appelle.

  • pour une procédure : que fait-elle ?
  • S’il n’y a pas de résultat renvoyé, essayez de décrire en français ce que fait la procédure, puis faites-en un résumé de deux ou trois mots, éventuellement abrégés, pour l’utiliser comme nom.

3) Identifier clairement, pour ne pas confondre

Lors du choix d’un nom, et de son utilisation, il y a certaines précautions à prendre, pour que le fait que ce nom soit compréhensible au premier coup d’oeil ne se retourne pas contre vous :

  • une variable ne doit servir qu'à une chose
  • Si pour économiser le nombre de variables, vous décidez de réutiliser une variable qui a servi à autre chose un peu plus haut, vous prenez le risque de croire ensuite qu’elle contient toujours la valeur à laquelle elle était destinée au départ.

  • ne pas modifier les valeurs des paramètres des fonctions
  • Sauf dans le cas où un paramètre a pour unique but de fournir une valeur à l’appelant par un moyen différent de la valeur de retour, vous ne devez en aucun cas modifier sa valeur. Le nom de cette variable décrit l’information qui est transmise à la fonction et la variable ne doit jamais contenir autre chose, sinon vous pourriez utiliser la valeur modifiée en croyant qu’il s’agit de celle passée en paramètre.

  • éviter les noms trop proches les uns des autres
  • Ne nommez pas deux variables avec un nom proche visuellement, vous risqueriez de les confondre. Evitez par exemple d’utiliser ensemble les variables "ligne" et "lignes" pour représenter la ligne courante et le nombre de lignes. Préférez par exemple "curLigne" et "nbLignes".

B. Eviter les répétitions : factoriser au maximum

Pour gagner du temps, quand on a plusieurs traitements similaires à faire, on a parfois tendance à écrire une première version, puis la copier plusieurs fois et faire quelques modifications sur les copies pour gérer les différent cas.

Cette pratique est l'une des principales sources de bugs dans les programmes. Copier un code N fois, c'est multiplier par N les chances de bugs et multiplier par N le temps nécessaire à la maintenance de ce code. Les bugs peuvent venir de plusieurs sources :

  • Il y avait un bug dans l'original, ce bug est donc présent dans chaque copie. Si on corrige le bug dans l'une des copies, on risque d'oublier de le faire dans les autres.

  • Si une mise à jour est nécessaire dans l'une des copies, elle l'est probablement dans les autres. Faire toutes les modifications demande du temps, ce qui multiplie le risque d'introduire des erreurs et d'oublier certaines différences.

  • Lors de la relecture, on lit la première version, puis on voit que la suivante fait la même chose à peu de choses près, donc on la lit avec beaucoup moins d'attention et on laisse passer plus de bugs.

  • Les différences entre les copies ne sautent pas aux yeux car elles sont noyées dans du code identique. On peut donc se tromper facilement et oublier d'en faire certaines.

  • Avoir plusieurs fois le même code, cela veut dire avoir du code plus long que nécessaire. Plus le code est long, plus il y a de chances de laisser passer des bugs.

Pour toutes ces raisons, une règle s'impose lors de l'écriture d'un programme :

Le copier-coller est à éviter absolument !

Lorsqu'un programmeur a utilisé le copier-coller dans son programme, cela se voit en général de loin : on voit tout de suite que des blocs très similaires se succèdent. Même s'il y a des différences importantes entre les différentes versions, il s'agit toujours de copier-coller, donc c'est mal.

Bien sûr, éviter d'utiliser les raccourcis de copier-coller ne signifie pas que vous devez réécrire chaque version entièrement à la main : cela ne ferait qu'aggraver les choses. Il existe toujours une manière d'écrire le code qui permette d'éviter d'avoir plusieurs passages identiques, et fournit un code plus propre, plus facile à lire et à maintenir, donc contenant moins de bugs.

Voici les quatre principales manières d'éviter les répétitions de code :

1) Stocker le résultat d'un calcul dans une variable locale

Lorsqu'un traitement utilise plusieurs fois un petit calcul, un accès à un tableau, etc., il faut éviter de réécrire ce petit calcul ou cet accès plusieurs fois. En le stockant dans une variable locale, non seulement vous évitez la répétition du code, donc les risques d'erreurs, mais vous donnez également un nom au résultat, ce qui permet, s'il est bien choisi, de comprendre beaucoup plus vite les instructions qui l'utilisent.

Par exemple, si vous devez trouver le maximum sur une ligne d'un tableau, plutôt que d'écrire :

pour chaque ligne
   pour chaque colonne
      si (contenuTableau[ligne][colonne] > valeurMax)
         valeurMax = contenuTableau[ligne][colonne]

L'accès à un tableau 2D n'est pas trivial, mieux vaut ne le faire qu'une fois et stocker la valeur dans une variable locale :

pour chaque ligne
   pour chaque colonne
      valeurCourante = contenuTableau[ligne][colonne]
      si (valeurCourante > valeurMax)
         valeurMax = valeurCourante

On a ainsi un code un peu plus long, mais plus facile à lire et plus facile à modifier si les index devaient changer. Bien sûr, les variables doivent être nommées plus précisément que dans cet exemple et indiquer ce que contient le tableau.

2) Placer le code réutilisé dans une fonction

Lorsque les différentes versions d'un calcul ne sont pas strictement identiques, on ne peut pas se contenter de stocker le résultat du premier dans une variable et de le réutiliser ensuite. L'ensemble du calcul doit être refait, avec certaines différences.

En écrivant une fonction contenant la partie commune des différentes versions et en utilisant les paramètres pour décrire les parties qui peuvent varier, on évite de recopier l'ensemble du code, on met en avant les différences lors de l'appel et on donne un nom au calcul, ce qui permet d'identifier plus facilement ce qu'il fait.

Utiliser une fonction permet de plus de réduire la longueur du code qui l'appelle, et permet également de relire / tester / débugger le code de la fonction isolément, indépendamment du reste.

Ainsi, si l'on souhaite afficher une ligne de X puis une ligne de O, plutôt que d'écrire :

pour pos = 1 à 20
   afficher X
aller à la ligne

pour pos = 1 à 18
   afficher O
aller à la ligne

on préfèrera le code suivant :

fonction afficherLigne(caractère, nbColonnes)
   pour pos = 1 à nbColonnes
      afficher caractère
   aller à la ligne

afficherLigne(X, 20)
afficherLigne(O, 18)

3) Utiliser une boucle, en stockant les différences dans un tableau

Une autre possibilité pour éviter de recopier du code, avec quelques différences entre les différentes versions, consiste à stocker les différences dans un tableau (avec une ligne par version du code), et d'inclure le code réutilisé au sein d'une boucle, qui récupère les différences dans le tableau. Ceci peut être préférable à l'utilisation d'une fonction, lorsque le calcul répété est simple, et est au coeur de l'algorithme, donc doit être bien en évidence.

Par exemple, si l'on veut tester dans une grille, si l'une des cases adjacentes à une position contient une valeur, plutôt que d'écrire :

fonction caseAdjacenteContient(ligne, colonne, valeur)
   si (grille[ligne][colonne + 1]  = valeur)
      retourne vrai
   si (grille[ligne][colonne - 1]  = valeur)
      retourne vrai
   si (grille[ligne + 1][colonne]  = valeur)
      retourne vrai
   si (grille[ligne - 1][colonne]  = valeur)
      retourne vrai
   retourne faux

On pourra écrire :

fonction caseAdjacenteContient(ligne, colonne, valeur)
   constante nbDirections = 4
   structure { dLig, dCol } directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}

   pour curDir allant de 0 à nbDirections - 1
       adjLig = ligne + directions[curDir].dLig
       adjCol = colonne + directions[curDir].dCol
       si (grille[adjLig][adjCol] = valeur)
         retourne vrai
   retourne faux

Avec ce système, on peut vérifier sur une seule ligne (l'initialisation du tableau), que l'on a bien géré toutes les directions sans erreurs. On n'a plus à vérifier qu'une fois le code d'accès au tableau. De plus, si l'on devait passer à 8 directions, il n'y aurait que deux lignes à modifier.

4) Réorganiser le code

Parfois, une simple réorganisation des différents éléments du code permet d'éviter de répéter certaines choses. Il n'est alors pas nécessaire d'ajouter un tableau, une variable ou une fonction. La section suivante donne quelques conseils sur l'organisation du code.

Par exemple, si l'on effectue un test pour vérifier la validité de certains arguments avant d'appeler une fonction :

si ((v1 > 0) ET (v2 >0))
    uneFonction(v1 - 1, v2 -1)
si ((v1 > 0) ET (v2 < MAX_V2 - 1)
    uneFonction(v1 -1 , v2 + 1)
...
etc.

Plutôt que de répéter le même type de test à chaque appel de la fonction, on peut tout simplement déplacer le test à l'intérieur du code de la fonction et n'avoir ainsi à l'écrire qu'une fois :

fonction uneFonction(v1, v2)
   si ((v1 < 0) OU (v2 < 0) OU (v1 >= MAX_V1) OU (v1 >= MAX_V2))
      retour

Ceci n'est qu'un des nombreux exemples de manières possibles de réorganiser le code. Il est important de réfléchir à la position des différents traitements, car il y a très souvent moyen de faire plus simple que la première manière qui vient à l'esprit.

C. Bien organiser les différentes parties du code

Pour un même algorithme, il y a de nombreuses manières différentes d'écrire le code correspondant. On peut faire les traitements dans différents ordres et les regrouper de plusieurs manières en fonctions. Suivant les choix effectués, le code peut être plus moins facile à analyser, donc à débugger. Voici quelques conseils sur la manière dont vous pouvez structurer vos programmes :

1) Séparer les différents traitements en fonctions

L'intérêt des fonctions est multiple : éviter de répéter le même code à plusieurs endroits, découper le code en morceaux de petite taille, représenter un traitement par un nom, etc. Pour qu'une fonction soit facile à comprendre, à utiliser et à débugger, elle doit faire une chose bien précise, que l'on peut résumer en quelques mots. On utilisera ensuite ces mots pour choisir un bon nom de fonction.

Une fonction doit se contenter de faire les traitements correspondant à la phrase qui résume son objectif et ne doit rien faire d'autre. Même si à chaque fois que l'on doit effectuer le traitement dont s'occupe cette fonction, il se trouve que l'on doit aussi faire autre chose qui ne correspond pas directement à cet objectif, mieux vaut séparer le traitement dans une autre fonction. On ne doit ainsi pas avoir de "surprise" quand on lit le code, après avoir lu le nom de la fonction.

Dès qu'une fonction dépasse une quinzaine d'instructions, cela signifie qu'il est grand temps de la décomposer en sous-fonctions (à moins qu'il soit possible de la simplifier autrement). Bien sûr il ne faut pas la découper arbitrairement en deux, mais chercher des parties qui ont un rôle en elles-mêmes et peuvent donc faire l'objet d'une fonction.

2) Commencer par se débarrasser des problèmes

Souvent, l'idée d'un algorithme est simple, mais certains détails comme la manière dont sont fournies les données, le système de coordonnées utilisées, etc. peuvent rendre le code complexe.

Quand c'est le cas, essayez autant que possible de vous débarrasser de ces problèmes le plus tôt possible, pour vous ramener au cas où l'algorithme est simple à écrire. Si des données doivent être traduites, converties, translatées, etc. pour être utilisées dans l'algorithme, essayez autant que possible de faire entièrement cette traduction avant même le début de l'algorithme. Le code devient alors beaucoup plus simple que s'il contenait des instructions de traduction réparties un peu partout.

Avant de commencer à coder un algorithme, passez donc un peu de temps à chercher s'il est possible de modifier les données pour arriver à une version plus simple du problème.

3) Isoler les cas particuliers du cas général

La partie la plus complexe d'un code est en général le coeur de l'algorithme. Il faut donc tout faire pour la simplifier au maximum et mettre en avant ce qu'elle fait de plus important : le traitement du cas le plus général.

Si le code est parsemé de traitements de cas particuliers, la structure devient plus obscure et difficile à visualiser. Lorsque c'est possible, il faut faire en sorte d'éviter tout bonnement d'avoir à gérer des cas particuliers (voir la section dédiée à ce point). Si ce n'est pas possible, séparez-les autant que possible du traitement du cas général, pour mettre celui-ci bien en évidence et le rendre aussi simple possible.

4) Faire les tests au début d'une fonction plutôt qu'avant son appel

Il arrive souvent que l'on ait besoin de ne faire un traitement que lorsque l'on est dans certaines conditions. Par exemple, si l'on travaille sur une grille, un traitement peut n'avoir de sens que lorsque l'on ne se trouve pas sur les bords, voire même lorsque l'on n'est pas sorti des limites de cette grille.

Plutôt que de tester, à chaque appel, si l'on n'est pas sorti des conditions dans lesquelles l'appel est valide, mieux vaut placer le test au début de la fonction : la première chose que la fonction va faire consiste à vérifier que les paramètres sont à l'intérieur des limites, et quitter la fonction si ce n'est pas le cas (voir l'exemple dans la partie "réorganiser le code" de la section sur les répétitions).

Un inconvénient de ce système est que les tests sont faits à chaque appel, même lorsque l'on est dans des conditions telles que l'on sait sans aucun doute que l'on est à l'intérieur de certaines, voire toutes les limites. La perte de temps est cependant négligeable par rapport au gain en simplicité du code. Le choix est donc sans hésitation pour la simplicité.

5) Autres remarques

  • N'utilisez pas d'instruction goto, ou équivalent.
  • Cette instruction permet de passer d'un endroit du code à un tout autre endroit. Cela casse la structure du code, dont le déroulement ne suit alors plus l'organisation visuelle des différentes parties. Il en devient plus difficile à comprendre et débugger.

  • Déclarez les variables près du code
  • Pour éviter de réutiliser des variables lorsqu'elles ne sont plus valides et ne pas avoir à remonter pour savoir quel est leur type, si on les a déjà initialisées, etc., mieux vaut les déclarer et les initialiser aussi près que possible de l'endroit où elles sont utilisées, et si possible à l'intérieur d'un bloc qui limite leur portée.

    D. Eviter les cas particuliers

    Les bugs que l'on laisse dans un programme sont souvent ceux qui ne se produisent que dans certains cas : lorsque l'on atteint les limites, ou que l'on est face à un cas particulier du problème qui entraîne l'exécution d'un code différent du code du cas général.

    Tester spécifiquement le programme sur ces cas particuliers et cas limites est un bon moyen pour détecter ces bugs, mais le mieux est tout simplement de les éviter en faisant en sorte de ne pas avoir à gérer ces cas différemment du reste. Eviter d'écrire du code pour des cas particuliers permet également de simplifier l'ensemble du programme, ce qui est toujours une bonne chose.

    Plusieurs techniques peuvent être utilisées pour ramener les cas particuliers au cas classique :

    1) Inclure les cas limites dans les boucles (première/dernière itération)

    Dans certains cas, il vaut mieux utiliser une structure de type "faire instructions tant que condition", plutôt que "tant que condition, faire instructions" (souvent notées while / do while). La deuxième structure permet en effet d'éviter de mettre en dehors de la boucle le traitement pour une première itération, qui doit se faire avant que la condition ait du sens.

    Plus généralement, si un traitement doit être un peu différent pour la première ou dernière itération, essayez de faire en sorte que seule la partie qui diffère réellement soit en dehors de la boucle.

    2) Mettre des sentinelles autour des données (si cela simplifie)

    Plutôt que d'ajouter des tests pour vérifier si l'on n'est pas sorti des limites du problème, il est parfois plus intéressant d'ajouter des valeurs autour des données de départ, telles que l'algorithme n'essaiera jamais d'aller au delà de ces valeurs. Par exemple si l'on a des traitements divers à faire sur un labyrinthe, plutôt que de tester en permanence si l'on est sorti des limites, on peut ajouter une fois pour toutes des cases de murs tout autour du labyrinthe initial et ne plus se préoccuper des dépassements de limites, car ceux-ci sont devenus impossibles.

    Bien sûr il ne faut le faire que si les modifications simplifient plus qu'elles ne complexifient le programme.

    3) Utiliser +infini et -infini plutôt qu'une valeur arbitraire à tester

    Lorsque l'on vous demande par exemple de trouver le plus court chemin pour atteindre un but à partir d'une certaine position, vous êtes susceptibles de programmer une fonction qui retourne la distance au but à partir d'un point précis, tout en indiquant s'il est possible d'atteindre le but.

    Plutôt que d'utiliser une valeur spéciale, par exemple "-1", pour indiquer un chemin impossible, ce qui impliquerait un peu partout dans l'algorithme, de tester si la distance a été mise à -1, mieux vaut utiliser une valeur tellement grande qu'elle peut être assimilée à l'infini : quel que soit ce qu'on y ajoute ou retire, on obtient toujours une valeur plus grande que la longueur de n'importe quel chemin valide. De cette manière, on s'assure que les chemins impossibles ne seront jamais sélectionnés par l'algorithme, tout en n'ayant pas à écrire de tests spécifiques au cas impossible.

    Si vous travaillez avec des entiers, envisagez des valeurs comme un milliard pour représenter l'infini positif et moins un milliard pour un infini négatif. Il faut bien sûr que ces valeurs soient au delà de toute solution valide, tout en évitant les risques de débordements (si l'on ajoute deux valeurs infinies par exemple, il faut s'assurer que l'on ne dépasse pas la capacité de la variable entière).

    Ce conseil s'applique très souvent, tout particulièrement lorsque l'on vous demande de rechercher un minimum ou un maximum d'une propriété.

    4) S'il est impossible d'utiliser une valeur "infinie", utiliser une variable séparée.

    Il est important qu'une même variable n'ait jamais deux significations différentes suivant l'endroit où on l'utilise. L'utilisation de valeurs infinies pour représenter les cas invalides respecte cette règle. Si une telle valeur n'est pas utilisable, évitez d'utilisez une autre valeur, arbitrairement (comme -1, par exemple). Mieux vaut parfois utiliser une variable séparée si l'on dispose d'assez de mémoire (un tableau de booléens, par exemple), pour indiquer les cas particuliers d'une manière qui rende le code plus clair. A défaut, utilisez une constante nommée, plutôt que de mettre directement la valeur.

    E. Se limiter à un langage simple

    Certains langages contiennent de très nombreuses fonctionnalités, mots clefs, types de données, modificateurs, etc. Ceux-ci peuvent être utiles dans certains cas, mais le fait qu'ils fassent partie du langage ne veut pas dire que vous devez tous les utiliser. Vous n'êtes pas là pour prouver que vous connaissez tous les détails d'un langage. Voici quelques remarques à ce sujet :

    1) Faciliter le travail du lecteur, plus que celui du compilateur.

    Certains mots clefs permettent d'aider le compilateur à détecter certaines erreurs potentielles. Par exemple en C/C++, le mot clef "const" peut être utilisé entre autres pour indiquer que certains paramètres d'une fonction ne doivent pas être modifiés dans le corps de celle-ci. Cela peut aider à détecter certains bugs, mais on arrive rapidement à avoir des "const" parsemés dans le code, ce qui l'alourdit considérablement, et rend sa lecture (sans compter son écriture) plus difficile pour un être humain. Il y a des cas où ce mot clef est très utile, mais cela ne veut pas dire qu'il faut en mettre partout.

    En écrivant du code simple, réduit à l'essentiel, vous éviterez bien plus de bugs que ce que le compilateur pourra aider à trouver si vous le parsemez de divers mots clefs non essentiels.

    2) Réduire sa palette de types au minimum

    Certains langages proposent jusqu'à huit types différents pour déclarer une variable entière, permettant des valeurs plus ou moins grandes, signées ou non. Si vous choisissez pour chaque variable, le type qui correspond le plus précisément à ce que vous allez y stocker, vous obtiendrez du code dans lequel chaque variable a un comportement différent des autres, ce qui nécessite de vérifier, à chaque modification, que son type est toujours bien adapté.

    Dans la plupart des cas, le programme pourrait fonctionner parfaitement si l'on n'utilisait qu'un seul type d'entier : l'entier signé sur 32 bits. (int en C). Pourquoi chercher ailleurs quand il convient, si ce n'est pour rendre le programme plus difficile à lire ?

    Tant que c'est possible, contentez-vous d'un type d'entier, un type de réel, et un type de caractère. N'utilisez les autres que lorsque c'est réellement nécessaire, par exemple lorsque la réduction de l'espace mémoire utilisé est nécessaire, ou au contraire, lorsque la taille standard est insuffisante pour stocker toutes les valeurs possibles. Utiliser un type différent montre alors qu'il y a une raison particulière de le faire, qui mérite l'attention du lecteur.

    3) Ne pas retenir les priorités des opérateurs : utiliser des parenthèses

    Lorsque l'on écrit "3 + 5 * 4", tout le monde sait que la multiplication est effectuée avant l'addition. Passé les quatre opérations de base cependant, les choses sont beaucoup moins évidentes. Plutôt que d'essayer de retenir les priorités des différents opérateurs, mieux vaut utiliser des parenthèses, cela évitera tout risque d'erreur, et rendra les choses beaucoup plus claires. Inutile de perdre ne serait-ce que quelques secondes à bien vérifier qu'on ne s'est pas trompé dans les priorités.

    F. Simplifier les structures de données

    Le choix des structures de données influence considérablement le code qui utilise ces structures. Lorsque l'on a déjà écrit une partie du programme utilisant ces structures, les modifier entraîne souvent trop de changements pour qu'on puisse se le permettre. Il est donc important de bien les choisir dès le départ, et surtout de privilégier, ici encore, la simplicité.

    1) Eviter les pointeurs, préférer les tableaux aux structures dynamiques

    Les structures dynamiques composées de nombreux éléments alloués sur le tas, et reliés par des pointeurs, sont bien plus délicates à manipuler que des structures plus statiques. Si l'on n'a pas réellement besoin d'une structure dynamique, et c'est souvent le cas en algorithmique, mieux vaut l'éviter.

    Lorsque vous devez manipuler un ensemble d'éléments, cherchez d'abord à stocker ces éléments dans un tableau. N'envisagez une structure dynamique, comme une liste chaînée, que si c'est vraiment nécessaire.

    La manipulation de pointeurs est tout particulièrement dangereuse. Si vous devez en utiliser, contentez-vous du minimum : allocation, affectation, déréférencement. Evitez les additions, soustractions de pointeurs, dont la relecture est difficile.

    2) Si une structure dynamique est nécessaire, utiliser les bibliothèques standard

    Dans certains cas, on ne peut se passer de structures de données dynamiques. Pour réduire les erreurs, mieux vaut alors utiliser les bibliothèques toutes faites, même si celles-ci sont souvent moins performantes que ce que vous pourriez faire à la main. Les risques de bugs sont beaucoup plus faibles.

    3) Regrouper les données liées dans des tableaux, structures, ou classes

    Lorsque plusieurs données vont ensemble, mieux vaut les regrouper plutôt que d'utiliser plusieurs variables. On met ainsi en évidence le fait qu'elles sont liées, on donne un nom à l'ensemble (tableau, structures ou classes, suivant les cas), et on peut les transmettre en un seul paramètre.

    Lorsque l'on a un nombre fixe et réduit de données différentes, que l'on n'est pas susceptible d'énumérer dans une boucle, mieux vaut les regrouper dans une structure/classe plutôt que dans un tableau. On peut ainsi identifier par un nom ce que représente chaque variable. point.y est bien plus clair que point[1], par exemple.

    G. Bien présenter son code

    Deux versions d'un même programme, contenant exactement les mêmes variables, les mêmes instructions dans le même ordre, peuvent être présentées très différemment. Une mauvaise présentation peut rendre le code très difficile à lire, alors qu'une bonne présentation suffit parfois à mettre en évidence certains problèmes.

    La manière dont on présente son code est très importante, et ne doit pas être négligée. Il est assez facile de prendre quelques habitudes permettant de rendre le code plus facile à lire.

    1) Mettre la structure du code bien en évidence.

    Pour bien comprendre un programme, il est crucial d'en avoir une bonne vue d'ensemble. Les programmes sont généralement organisés en plusieurs fonctions, elles-mêmes décomposées en blocs, et sous-blocs : le corps d'une boucle, les groupes d'instructions exécutés selon une instruction conditionnelle, etc.

    Mettez en évidence cette structure en indentant les blocs (les décalant de 3 ou 4 espaces par rapport au bloc englobant), et en mettant en avant les symboles ou mots clefs définissant les débuts / fins de blocs quand il y en a. Vous pouvez également sauter des lignes pour bien séparer ces blocs, si cela semble nécessaire.

    2) Ne mettre qu'une seule instruction par ligne

    Lors de la relecture du code, il faut pouvoir se concentrer sur une seule chose à la fois. Vérifier une instruction entièrement, ou on simule de tête son comportement dans un cas donné, puis on passe à l'instruction suivante.

    Pour pouvoir faire ce travail de relecture efficacement, il faut que chaque instruction soit la plus simple possible à vérifier et éviter le risque d'oublier certaines instructions. Placer une seule instruction par ligne aide à se concentrer sur cette instruction, en la faisant apparaître plus clairement.

    Cela vaut par exemple pour les instructions conditionnelles : éviter d'écrire :

    si (curVal < minVal) minVal = curVal
    
    préférez les deux lignes suivantes, qui mettent mieux en évidence la structure du code :
    si (curVal < minVal)
       minVal = curVal
    
    Selon ce principe, évitez également de faire des calculs non triviaux lors du passage de paramètres à une fonction, ou pour indicer un tableau :
    calcul = maFonction(tab[val1 + val2] * tab2[maFonction2(2, val3)]);
    

    Mieux vaut faire les calculs un par un, en utilisant des variables locales bien nommées pour stocker les résultats intermédiaires. On peut ainsi relire plus facilement, et vérifier une chose à la fois.

    Dans certains cas, une fonction peut prendre de nombreux paramètres, et même si on se contente de passer directement des variables, mettre tout sur une même ligne peut résulter en une ligne qui dépasse la largeur de l'écran. Evitez d'écrire des lignes de plus de 80 caractères. Vous pouvez aller à la ligne entre les différents paramètres pour résoudre le problème, donc utiliser plusieurs lignes pour une même instruction.

    3) Bien choisir où mettre des espaces

    Une ligne de code où tous les éléments sont concaténés ne se comprend pas au premier coup d'oeil. Pensez à mettre des espaces autour des opérateurs pour bien les mettre en évidence, par exemple. Le plus important est de se fixer une règle, et de l'appliquer systématiquement. Un code où l'on change de style en permanence n'est pas agréable à lire.

    4) Eviter les commentaires au milieu du code

    Si l'on choisit bien les noms des identifiants, que l'on n'écrit que des lignes simples, en faisant usage de variables temporaires pour nommer les résultats intermédiaires, un code doit pouvoir se comprendre facilement. Ajouter des commentaires à chaque ligne, ou entre les lignes, ne fait alors que rendre plus difficile la lecture du code lui-même.

    Bien sûr vous pouvez commenter votre code, mais préférez des commentaires au début de la fonction, qui permettent de comprendre rapidement ce qu'elle fait, sans entrer dans les détails.

    H. Se fixer des conventions

    Un bon moyen pour réduire le nombre de bugs créés et gagner du temps lors de l'écriture même du programme, consiste à prendre de prendre de bonnes habitudes. Ecrivez les parties qui se ressemblent toujours de la même manière, pour éviter d'avoir à vous reposer certaines questions à chaque fois. Voici quelques exemples d'éléments où l'on peut se fixer des conventions.

    1) Ecrire ses boucles avec compteur toujours de la même manière

    Lorsque vous utilisez un compteur pour parcourir un tableau, ou un ensemble de valeurs possibles, vous avez plusieurs décisions à prendre : la valeur de départ du compteur, la condition de fin, et où placer l'incrémentation.

    Suivant le langage utilisé, on peut avoir diverses préférences, mais l'important est de faire toujours de la même manière lorsque c'est possible. Par exemple on peut décider de toujours initialiser le compteur à 0 et d'utiliser un inférieur strict sur le nombre d'éléments à énumérer pour la condition de fin : "si (compteur < nbElements)". Il faut alors faire de la même manière dans tous vos codes, lorsque c'est possible, et ne complique rien.

    2) Ecrire les algorithmes classiques toujours de la même manière

    Certains algorithmes, ou parties d'algorithmes apparaissent souvent au sein de programmes. Par exemple on peut souvent avoir besoin de chercher la valeur maximale d'un tableau. Cela peut aussi être des algorithmes plus évolués, comme un algorithme dynamique, un parcours en profondeur d'un graphe, etc. Dans chacun de ces cas, cherchez une manière simple et propre de le coder, retenez-la et réutilisez là telle quelle lorsque l'occasion se présente. Ceci inclut non seulement le code, mais les structures que vous utilisez.

    Bien sûr vous avec un peu d'expérience, vous pourrez trouver de nouvelles manières de programmer ces algorithmes, plus simples ou plus efficaces. Vous changerez alors vos futures implémentations, mais évitez d'écrire une version différente à chaque fois : vous aurez moins de chances de commettre des erreurs en reprenant un style déjà testé. Bien sûr cela ne vous dispense pas de relire votre code attentivement à chaque fois.

    Dans les corrections de ce site, nous vous proposerons souvent des conseils sur la manière d'écrire certains algorithmes. Vous pourrez alors vous en inspirer pour définir votre propre style.

    3) Se fixer des règles pour nommer ses identifiants

    Pour une même donnée, de nombreux noms de variables sont envisageables, tous aussi corrects les uns que les autres. On peut par exemple utiliser un nom français, ou anglais, séparer les mots par des '_' ou mettre la première lettre de chacun en majuscule. Ainsi, pour indiquer un nombre total de lignes, les noms "nbLines", "nbLignes", "nb_lignes", "lignes" sont tous aussi valides les uns que les autres.

    Il n'existe donc pas de "meilleur nom" pour une variable, mais si vous utilisez "nbLignes", n'utilisez pas "colonnes" un peu plus loin pour indiquer un nombre de colonnes : faites un choix une fois pour toutes et appliquez-le systématiquement.

    Attention : les choix ne sont pas indépendants les uns des autres. Si vous utilisez "ligne" pour représenter l'index de la ligne courante par exemple, évitez d'utiliser "lignes" pour représenter le nombre de lignes : les deux se ressemblent trop, il pourrait y avoir confusion. Faites donc un choix d'un ensemble cohérent de conventions sur les noms de variables et essayez de vous y tenir. Si vous choisissez d'utiliser l'anglais par exemple, utilisez-le partout.

    Bien sûr ceci est valable également pour les noms de constantes (que l'on peut par exemple mettre systématiquement en majuscules, pour les différencier), le nom des fonctions, etc.

    Conclusion

    Cette liste de conseils devrait pouvoir vous permettre d'écrire des programmes plus clairs, plus faciles à relire et dans lesquels les bugs se voient plus rapidement. Bien sûr ce n'est pas une méthode miracle et on peut toujours passer à côté des bugs, mais elle permet d'en éliminer une grande partie. Elle n'est pas non plus complète, le plus important est surtout de comprendre les principes de base : faire le plus simple possible, et le plus lisible possible, même si cela implique de passer plus de temps à l'écriture du programme.

    Tous les conseils donnés ici peuvent s'appliquer avant même l'écriture de la première ligne de code sur une machine : avant d'écrire votre programme, rappelez-vous qu'il vaut mieux en écrire une première version sur papier, en pseudo-code (voir le cours sur ce sujet). Ces conseils s'appliquent aussi bien sur ce pseudo-code que sur le programme final.

    Il n'est ainsi jamais trop tôt pour essayer de simplifier son programme, mais il n'est jamais trop tard non plus. Même après avoir terminé d'écrire un code et commencé à le tester, si l'on réalise qu'une partie peut être simplifiée, mieux vaut le faire plutôt que de perdre du temps à débugger la version plus compliquée. Le temps passé à simplifier sera très probablement inférieur au temps gagné lors du débuggage.

    Après avoir lu ce document, vous ne parviendrez probablement pas à appliquer pleinement tous les conseils dès vos prochains programmes. Revenez donc régulièrement sur ce document, et relisez-le en ayant devant vous les programmes que vous avez écrits récemment, cela vous aidera à voir quels points vous pouvez encore améliorer. Demandez-vous régulièrement si vous auriez pu faire plus simple, plus lisible.

    Pour vous aider, voici un résumé du document, ne contenant que les titres. Relisez les explications correspondantes si vous ne voyez plus très clairement le sens d'un de ces titres.

    Résumé des conseils

    A. Bien nommer ses variables et fonctions

    1. Nommer quoi et pourquoi
      • Remplacer les expressions par des variables.
      • Remplacer les valeurs numériques par des constantes ou énumérations
      • Remplacer les groupes d’instructions par des fonctions
      • Remplacer certains tableaux par des structures
      • Ne pas trop compter sur les commentaires
    2. Comment choisir un nom
      • pour une variable de stockage : que contient-elle ?
      • pour un tableau : que contient une case ?
      • pour un indice : qu’est-ce qu’il énumère ?
      • pour une fonction : que renvoie-t-elle ?
      • pour une procédure : que fait-elle ?
    3. Identifier clairement, pour ne pas confondre
      • une variable ne doit servir qu'à une chose
      • ne pas modifier les valeurs des paramètres des fonctions
      • éviter les noms trop proches les uns des autres

    B. Eviter les répétitions : factoriser au maximum

    Quatre principales manières d'éviter les répétitions de code :
    1. Stocker le résultat d'un calcul dans une variable locale
    2. Placer le code réutilisé dans une fonction
    3. Utiliser une boucle, en stockant les différences dans un tableau
    4. Réorganiser le code

    C. Bien organiser les différentes parties du code

    1. Séparer les différents traitements en fonctions
    2. Commencer par se débarrasser des problèmes
    3. Isoler les cas particuliers du cas général
    4. Faire les tests au début d'une fonction plutôt qu'avant son appel
    5. Autres remarques
      • N'utilisez pas d'instruction goto, ou équivalent.
      • Déclarez les variables près du code

    D. Eviter les cas particuliers

    1. Inclure les cas limites dans les boucles (première/dernière itération)
    2. Mettre des sentinelles autour des données (si cela simplifie)
    3. Utiliser +infini et -infini plutôt qu'une valeur arbitraire à tester
    4. S'il est impossible d'utiliser une valeur "infinie", utiliser une variable séparée.

    E. Se limiter à un langage simple

    1. Faciliter le travail du lecteur, plus que celui du compilateur.
    2. Réduire sa palette de types au minimum
    3. Ne pas retenir les priorités des opérateurs : utiliser des parenthèses

    F. Simplifier les structures de données

    1. Eviter les pointeurs, préférer les tableaux aux structures dynamiques
    2. Si une structure dynamique est nécessaire, utiliser les bibliothèques standard
    3. Regrouper les données liées dans des tableaux, structures, ou classes

    G. Bien présenter son code

    1. Mettre la structure du code bien en évidence.
    2. Ne mettre qu'une seule instruction par ligne
    3. Bien choisir où mettre des espaces
    4. Eviter les commentaires au milieu du code

    H. Se fixer des conventions

    1. Ecrire ses boucles avec compteur toujours de la même manière
    2. Ecrire les algorithmes classiques toujours de la même manière
    3. Se fixer des règles pour nommer ses identifiants