Bonnes pratiques de programmation

Lorsque l'on programme, on passe un certain temps à écrire du code, mais on passe beaucoup plus de temps à lire du code que soi, ou d'autres, ont écrit.

On peut facilement faire l'analogie entre l'écriture du code et l'écriture d'un texte. Lorsque le texte est mal écrit, qu'il contient des fautes d'orthographe, que les phrases sont mal structurées et que les idées ne sont pas organisées, ce texte est très difficile à lire et donc à comprendre. Il en va de même pour le code : un code brouillon est très difficile et fatiguant à comprendre... de plus, les bugs s'y cachent beaucoup plus facilement.

Observez par exemple le code ci-dessous :

def k(i: int) -> int:
    # rxkfghh = rxkfgh2 sauf si i = 1
    rxkfghh="gh"
    if i==1:
        p=7
        a=p+2
        rxkfgh=1
        rxkfghh="temporaire"
        print(rxkfghh, rxkfgh)
    else:
        rxkfghh2="tempoaussi"
        rxkfgh = i*k(i-1)
        print(rxkfghh, rxkfgh)
    return rxkfgh

Reconnaissez-vous cet algorithme ? C'est celui de factorielle. Ce serait plus simple à comprendre si l'algorithme était bien écrit non ?

Il est donc très important de respecter certaines bonnes pratiques de programmation (qui s'appliquent naturellement à la rédaction d'algorithmes) pour rendre le plus agréable possible la lecture de code.

Ce chapitre n'a pas vocation à vous enseigner toutes les bonnes pratiques de programmation... vous aurez un cous à ce sujet au second semestre. Néanmoins, il présente quelques bonnes habitudes que vous devez prendre dès le début.

Interface vs. implémentation d'un algorithme

Dans la première partie de ce cours, nous avons parlé à plusieurs reprises de "contrat" ou "spécification formelle".

Le contrat caractérise l'interface d'un algorithme, c'est-à-dire qu'il explique le plus clairement possible ce que l'algorithme est capable de produire comme sorties étant donné ce qu'on lui fournit en entrée. Les pré-conditions permettent de préciser sous quelles conditions l'algorithme sera capable de fonctionner et les post-conditions nous renseignent sur ce que l'on peut s'attendre à obtenir comme résultats.

Par conséquent, lorsqu'on lit un contrat, i.e. que l'on consulte l'interface d'un algorithme, on est renseigné sur ce à quoi on peut s'attendre, ainsi que sur les limites de l'algorithme que l'on va utiliser, sans pour autant avoir besoin de comprendre comment l'algorithme est implémenté. Il nous appartient de composer avec ces informations. Par exemple, quand vous utilisez range(0,4) en Python, vous savez que vous allez obtenir une liste de 4 éléments de 0 à 3, mais vous ne savez pas comment Python procède pour créer cette liste.

Le contrat permet donc de savoir très exactement ce que l'algorithme est capable de faire, mais il ne dit rien sur comment l'algorithme va s'y prendre pour résoudre le problème.

Le reste de ce chapitre porte sur les bonnes pratiques de programmation pour coder des algorithmes, donc sur le comment.

Qu'est-ce qui caractérise un code "bien écrit" ?

Un algorithme, ou code "bien écrit" doit avoir les propriétés suivantes :

  • Être facile à lire, pas soi-même mais aussi par les autres.

  • Avoir une organisation logique et évidente.

  • Être explicite, montrer clairement les intentions du développeur.

  • Être soigné et robuste au temps qui passe.

Nous allons regarder un peu plus en détails chacune de ces caractéristiques.

La code doit être facile à lire

Pour que le code soit facile à lire, il faut d'une part qu'il soit bien structuré et bien présenté, et d'autre part, que les noms des variables et des fonctions soient choisis avec soin.

Pour ce qui est de la structure et de la présentation, Python nous aide beaucoup car le langage impose beaucoup de choses qui nous "forcent" à bien présenter notre code. Par exemple, les blocs d'instructions du même niveau doivent être précédés du même nombre d'espaces, ce qui nous conduit naturellement à bien indenter notre code.

Dans d'autres langages, plus de libertés de présentation sont offertes au développeur, par conséquent, il convient de se forcer à respecter des conventions pour écrire le code le plus propre possible.

Par exemple, regardez le code Java ci-dessous. Il est évident que le manque d'indentation ne facilite pas la lecture et la compréhension du code, n'est-ce pas ?

int i,j,k, m, n, o = 0;
n = 9; o=13;
int [][] p = new int [n][o];
for(i=0;i<n;i++){
p[i][0] = i;
m = i;
for(j=1;j<o;j++){
for(k=0;k<j-1;k++){m = m + p[i][k];}
p[i][j] = m;}}
System.out.println("----");
for(i=0;i<o;i++){
for(j=0;j<n;j++){
System.out.print("|" + p[j][i]);}
System.out.println("|");}

Pour ce qui est du choix des noms des choses (variables et fonctions), nous en reparlerons un peu plus loin dans ce chapitre.

Le code doit avoir une organisation logique et évidente

Ce point est plus délicat car nous avons souvent des solutions différentes pour résoudre le même problème. Il est donc normal qu'un code qui semble logique à quelqu'un semble "tordu" à son voisin.

Étant conscient de cela, il faut vous efforcer de trouver des solutions logiques aux problèmes que vous devez résoudre et d'éviter d'emprunter des chemins plus compliqués qui ne feraient que semer la confusion.

Par exemple, si l'on vous demande d'afficher tous les nombres de 1 à 10, il suffit de faire une boucle qui fait évoluer un compteur entre 1 et 10 et qui affiche, à chaque tour, la valeur de ce compteur. La solution qui consisterait à faire une boucle qui fait évoluer un compteur de 9 à 0 et qui afficherait à chaque tour le résultat de 10 - compteur fonctionne aussi mais est à proscrire car elle est "tordue".

Le code doit être explicite

Lorsque l'on écrit des algorithmes ou que l'on développe des programmes, on est parfois tenté de prendre des raccourcis car "on sait" que telle ou telle méthode permet de faire telle ou telle chose bien pratique.

Il n'est pas interdit de prendre ces raccourcis, mais il faut toujours prendre le soin d'expliquer, au moins à travers des commentaires, pourquoi on fait cela. C'est important à la fois pour permettre aux autres de comprendre pourquoi votre solution est astucieuse... mais aussi pour vous, au cas où vous ne vous souveniez plus de "pourquoi vous avez fait ça".

Par exemple, si vous devez afficher une matrice de dimensions MxM, la procédure usuelle est de faire deux boucles imbriquées permettant d'afficher chacun des éléments de la matrice. Or, si vous savez que votre matrice est triangulaire, vous allez probablement vouloir optimiser votre double boucle d'affichage. C'est naturellement une bonne idée... mais pensez bien à rappeler dans le commentaire pourquoi vous procédez de la sorte.

Le code doit être soigné et robuste au temps qui passe

Lorsque l'on écrit du code, on a la fâcheuse tendance à s'arrêter dès que celui-ci fonctionne. C'est un tort ! Le code doit être entretenu. Cela signifie qu'il faut relire son code après l'avoir terminé, vérifié que l'on a bien supprimé les éléments obsolètes, vérifier que les commentaires sont à jour et cohérents avec le code conservé, etc.

Cette opération de "maintenance" du code est cruciale, mais elle est pourtant souvent négligée par beaucoup, ce qui peut poser des problèmes, notamment lorsque vous rencontrez un bug.

L'exemple le plus classique est celui-ci : vous implémentez une méthode tri qui permet de trier les éléments d'un tableau. Vous n'êtes pas satisfait du comportement de cette méthode lorsque vous l'utilisez depuis votre programme principal. Vous implémentez donc une autre méthode test qui utilise une autre stratégie pour trier les éléments du tableau. Cette méthode marche mieux. Vous l'utilisez donc dans votre programme principal. Votre programme fonctionne et vous passez à autre chose, sans penser à intégrer vos modifications proprement dans votre programme. Quelques jours plus tard, vous reprenez votre code et vous observez un bug. Vous pensez que cela provient du tri du tableau. Vous allez donc observer ce qui se passe dans tri. Après quelques heures de recherche, vous êtes furieux contre vous-même car vous réalisé enfin que la méthode tri n'est plus utilisée depuis longtemps dans votre code...

Croyez-le, les problèmes de ce type arrivent beaucoup plus souvent qu'on ne le pense... surtout quand on cherche à faire vite.

Avant de passer à la suite, un autre exemple de code. Voyez-vous un problème ?

#la boucle s'arrête si i est négatif ou si continuer prend la valeur false
i = 0
j = 4
while continuer:
        print("mon code marche")
        #i += 1
        j += 1
        if j > 10:
           continuer = False

C'est bien joli tout ça, mais coder proprement ça prend du temps !

C'est faux ! Il ne faut pas confondre vitesse et précipitation.

On a souvent tendance à penser que l'on perd énormément de temps à soigner son code, à le structurer correctement, à le réorganiser et à le documenter, mais c'est faux. Au contraire, on gagne du temps à faire tout cela.

Voici quelques explications pour vous en convaincre :

  • Si vous adoptez les bonnes pratiques dès le début, vous faites déjà 50% du travail.

  • Si le code est bien écrit, il est plus facile, et donc plus rapide à relire, et n'oubliez pas que vous passez plus de temps à lire votre code qu'à l'écrire... donc quand votre code est propre, vous vous faites gagner du temps.

  • Si le code est logique et bien structuré, il sera plus facile de retrouver les bugs qu'il contient, et donc de l'améliorer..

Ce sont donc autant de raisons qui devraient vous convaincre qu'il est important d'être organisé, clair, méthodique et rigoureux quand vous développez.

De l'importance des commentaires

Les commentaires sont essentiels pour "éclairer" le code. Un commentaire est un texte qui est ignoré par l'ordinateur lorsqu'il exécute le programme, mais qui peut être lu par le développeur lorsqu'il lit le programme.

En Python, une ligne qui débute par le signe # est un commentaire. On peut aussi faire des blocs de commentaires, sur plusieurs lignes, en utilisant les triples guillemets.

Bien que les commentaires soient essentiels, il ne faut pas en abuser.

Un bon commentaire peut :

  • Faciliter la lecture du code,

  • Apporter une indication sur un choix de conception,

  • Expliquer une motivation qui ne serait pas évidente (comme dans l'exemple de la matrice triangulaire vu plus haut)

  • Donner un exemple pour permettre de mieux comprendre ce que fait le code.

Quelques exemples de mauvais commentaires :

  • Un commentaire qui décrit un morceau de code qui n'existe plus,

  • Un commentaire qui explique une évidence,

  • Un commentaire sur plusieurs lignes pour expliquer une chose simple.

  • Un commentaire sur l'historique des modifications d'un fichier. C'est parfois utile, mais dans la plupart des cas, il vaut mieux confier cette tâche à votre gestionnaire de versions qui fera le travail pour vous.

Vous trouverez ci-dessous quelques exemples de mauvais commentaires :

i = 0 #initialisation de la variable i à 0
i = i + 1 # incrémentation de la variable i
# Additionne a et b et stocke le résultat dans c
c = a + b
# Ci-dessous une double boucle pour afficher un tableau
for i in range(10):
    print("Valeur ", i)
# fin du for
# Et maintenant, on va s'occuper de retourner la valeur de i
# Pour cela, on utilise le mot clé return
# Et on passe ensuite la valeur de i
return i

Comme vous le voyez dans l'exemple ci-dessus, il n'est pas judicieux d'utiliser un commentaire pour signaler la fin d'un bloc d'instructions. En général, si vous avez besoin de ce type de commentaire, c'est que votre code est déjà trop long. Dans ce cas, demandez-vous si vous ne pouvez pas découper votre code en fragments plus simples.

Attention : les commentaires ne doivent pas pallier un manque de clarté du code. Si vous avez besoin de commentaires pour cela, c'est probablement que vous pouvez améliorer votre code pour le rendre plus lisible. Essayez donc de le refactorer, c'est-à-dire de le ré-écrire, au moins partiellement, en l'améliorant.

Comme nous l'avons dit plus haut, les commentaires, tout comme le code, doivent être maintenus, c'est-à-dire qu'ils doivent évoluer avec le code, et disparaître si le code disparaît. Par conséquent, il faut veiller au bon dosage de vos commentaires, de sorte à ne pas alourdir inutilement votre travail de maintenance.

Enfin, une bonne pratique est d'utiliser des outils tels que doctest qui permettent d'écrire des petits tests pour vos fonctions tout en les documentant. Les avantages de cette pratique sont d'une part que cela vous "force" à tester votre code, et qu'il permet en même temps d'en avoir une documentation "par l'exemple" qui sera forcément cohérente avec le code et à jour (sinon, les tests ne fonctionneraient pas). Pour en savoir plus, n'hésitez pas à consulter la documentation de doctest pour Python, https://docs.python.org/3.5/library/doctest.html.

Comment nommer les choses ?

Les noms que vous choisissez pour vos variables et vos fonctions vont grandement contribuer à la lisibilité de votre code.

Par exemple, vous conviendrez que le bloc de code suivant n'est pas très lisible tandis que celui d'après, qui fait exactement la même chose, est plus clair.

xretyers = 4
eijfzeipfjzpeij = 1
xretyers = eijfzeipfjzpeij + xretyers
x = 4
x += 1

La première règle est donc de choisir des noms de variables prononçables et faciles à retenir.

Vous devez choisir des noms de variables explicites pour vous mais aussi pour les autres.

Par exemple, a est bien moins explicite que adresseClient. De même, lf est moins explicite que largeurFenetre. Pensez également que vous serez probablement amenés à chercher vos variables dans votre code avec un outil de recherche. À votre avis, combien d'occurrences de a allez vous trouver ? Et combien d'occurrences de adresseClient ?

Évitez également de choisir des noms de variables qui induisent un contre-sense. Par exemple, si vous écrivez matrice=8, on pourrait penser que la variable est une matrice, or, il s'agit clairement d'un entier. Au moment de l'affectation, il est facile de se rendre compte du type de la variable, mais maintenant, imaginez que vous rencontriez, au beau milieu du code, la ligne suivante :

matrice = matrice * 4

Comment allez vous interpréter cette instruction ?

Évitez également les noms de variable qui n'ont pas de sens, comme plop, surtout si vous en utilisez plusieurs dans la même portion de code. Vous risquez de vous perdre dans les noms et donc d'introduire inutilement des bugs dans votre code.

Ne trichez pas non plus quand vous choisissez vos noms de variables. Dans la plupart des langages de programmation, certains mots sont "réservés" car ce sont des instructions du langage. Par exemple, en Python, on ne peut pas nommer une variable if ou list car ce sont des mots réservés du langage. Si vous pensez avoir besoin de ces mots pour nommer vos variables, ne trichez pas en écrivant iff ou llist, vous risqueriez de vous perdre dans vos propres astuces.

La plupart des langages imposent des règles pour le choix des noms de variables. Par exemple, en Python, un nom de variable ne peut pas commencer par un chiffre (il y a d'autres règles). En plus de ces règles imposées par les langages, essayez de ne pas utiliser des caractères qui pourraient porter à confusion. En particulier, les caractères o 0 O l i et 1 peuvent se ressembler fortement, voire être identiques en fonction de la police de caractères utilisée pour l'affichage à l'écran.

Enfin, essayez d'être cohérents lorsque vous choisissez vos noms de variables. Par exemple, si vous décidez d'utiliser le français pour nommer vos variables, utilisez le français tout du long, n'alternez pas avec l'anglais. longueurPath ou lenghtChemin sont des noms pour le moins curieux !

À propos des environnements de développement

Vous avez à votre disposition des environnements de développement. Pensez à les utiliser et apprenez à exploiter leurs nombreuses possibilités.

La plupart de ces environnements vous offrent des outils de refactoring qui vous permettent de réorganiser votre code facilement tout en garantissant une certaine cohérence. Par exemple, ils vous permettent de renommer une variable partout dans le code. C'est l'occasion de changer tous vos plop par des noms plus significatifs.

En général, ces outils vous permettent également de vérifier que votre code est correctement indenté, et qu'il respecte des conventions de programmation préétablies et matérialisées par des règles. Vous devez apprendre à les utiliser, cela vous fera gagner beaucoup de temps.

Ces outils vous offrent également des fonctionnalités d'auto-complétion (c'est-à-dire qu'ils complètent automatiquement le texte que vous êtes en train de taper). Ce sont des fonctionnalités très pratiques qu'il faut également apprendre à maîtriser. Grâce à ces fonctionnalités, vous n'avez plus d'excuses : inutile de choisir des noms de variables courts (type a b c) sous prétexte que c'est plus rapide à taper...

Référence

La plupart des idées de ce cours proviennent de l’ouvrage formidable "Coder proprement". Robert C. Martin. Pearson Education France, 2009.