Que dire après 37 ans de programmation quotidienne ?

Plan

Que dire après 37 ans de programmation quotidienne ?

Tout d'abord une évidence : les codes sources sont lus bien plus souvent qu'ils ne sont écrits. Il faut donc qu'ils soient le plus lisible possible. Aucune autre règle n'est plus importante que celle-ci. Éventuellement, si l'application est inutilisable car trop lente, on pourra déroger à cette règle pour optimiser la fonction qui la ralentit.

Un code lisible est un code court, dont les algorithmes sont bien localisés, les identificateurs explicites, respectant les conventions de présentation classiques et n'utilisant que des constructions habituelles du langage. Il doit pouvoir facilement être lu de manière linéaire. La documentation doit expliquer comment utiliser le code. Les commentaires doivent expliquer ce qui ne peut pas être compris en lisant le code localement, mais aussi pourquoi le code a été écrit de cette manière et pas d'une autre qui peut sembler plus simple. De manière générale les commentaires expliquent ce qui sort de l'ordinaire ou qui ne peut pas être compris en lisant en diagonale. Si un commentaire peut être transformé en code ou bien rendu inutile par une simplification du code, alors il faut le supprimer.

De la même façon qu'un algorithme n'est pas éparpillé dans le code, une fonctionnalité d'une application ne doit pas être disséminée dans de multiples fichiers. Éventuellement, de multiples fichiers préfixés par le nom de la fonctionnalité, par exemple sauvegarde.py, sauvegarde.css, sauvegarde.js, sauvegarde-fr.po, sauvegarde-en.po... On doit pouvoir facilement voir le code source, les traductions, les tests de régression, images et autres ressources nécessaires à l'implémentation de la fonctionnalité. Le coeur de l'application se contente d'intégrer les fonctionnalités et les faire communiquer (FyHooks framework).

Il faut généraliser au maximum, car en enlevant les cas particuliers, cela simplifie le programme car il y a des tests en moins. La généralisation des algorithmes fait aussi émerger des fonctionnalités non prévues initialement. La très grande majorité des bugs sont déclenchés dans des cas limites, un code comportant moins de conditions contiendra donc moins de bugs. Il sera aussi plus facile d'écrire des tests de régression permettant d'assurer la couverture du code la plus large possible (code coverage). Une manière simple d'éliminer des conditions est de les faire descendre dans le code qui est du plus bas niveau possible. Il faut si possible faire des fonctions bas niveaux les plus puissantes possibles et les réutiliser systématiquement. Leurs fonctionnalités seront alors disponibles pour toutes les fonctions haut niveaux. Par exemple, si l'on a une fonction de destruction de fichier, il faut qu'elle ait des options pour demander confirmation, mettre dans la corbeille... Toute amélioration de cette fonction aura des retombées sur toutes les fonctions l'utilisant.

Le stockage des données saisies par un être humain doit être fait de préférence dans un fichier en mode ajout (sans modifier le contenu déjà présent), dans un format lisible par une personne et manipulable facilement par une machine. Ceci permet de débugger facilement, de faire évoluer sans contraintes les structures de données, de garantir l'absence de perte de données, d'avoir un historique des modifications et de pouvoir utiliser un gestionnaire de version pour faire des sauvegardes efficaces des données.

Un code court (DRY don't repeat yourself)

Un code qui comporte des répétitions est plus long à lire et plus difficile à corriger et à faire évoluer, en effet chacune des répétitions doit être modifiée sans en oublier aucune. Dans certains cas des répétitions n'ont pas la même sémantique et doivent donc être différenciées. Le choix du langage de programmation a une forte influence dans ce domaine, mais dans la plupart on peut faire disparaître les répétitions.

Voici quelques exemples de répétitions possibles, et comment les éliminer pour améliorer la lisibilité mais aussi les performances des programmes compilés car leur code exécutable tient plus facilement dans le cache du processeur. Généralement, appliquer les substitutions qui suivent raccourcit le code source, ce n'est pas le cas pour les exemples qui suivent car ils sont trop simples.

Répétition d'expression

La partie répétée à gauche est pi * x * x

À éviter Mieux
# Surface du disque A
surface_A = pi * rayon_A * rayon_A
# Surface du disque B
surface_B = pi * rayon_B * rayon_B
def surface_disque(rayon):
    return pi * rayon * rayon
surface_A = surface_disque(rayon_A)
surface_B = surface_disque(rayon_B)

On remarque que les commmentaires sont inutiles car le nom de la fonction apporte la sémantique manquante dans la première version.

Répétition d'argument (manque de structure)

Si vous avez plusieurs fonctions avec des arguments identiques, c'est que cet ensemble d'arguments représente quelque chose. Autant nommer cette chose, les passages de paramètres seront simplifiés.

La partie répétée à gauche est (x, y)

À éviter Mieux
def norme(x, y):
   return ((x**2 + y**2)**0.5

def normalise(x, y):
   n = norme(x, y)
   return (x/n, y/n)

p = normalise(6, 7)
class Point:
   def __init__(self, x, y):
      self.x = x
      self.y = y

def norme(p):
   return ((p.x**2 + p.y**2)**0.5

def normalise(p):
   n = norme(p)
   return Point(p.x/n, p.y/n)

p = normalise(Point(6, 7))

Le programme sans redondance est plus long, mais il introduit le concept de point, il est ainsi plus facilement lisible, modifiable et surtout on peut facilement lui ajouter des fonctionnalités ou continuer à le faire évoluer (transformer norme et normalise en méthodes de la classe Point

Il ne faut pas hésiter à ajouter des structures de données qui ont été oubliées lors de l'analyse.

Répétition d'argument (manque de lien)

Dans une structure de données hiérarchique, pour économiser de la place mémoire on ne stocke pas le lien vers le parent. Cela alourdit malheureusement les passages de paramètres. De plus, lorsque l'on doit ajouter un paramètre, il est nécessaire de l'ajouter à toutes les fonctions de la chaîne d'appel. Il est donc plus simple d'ajouter un lien vers le parent.

La partie répétée à gauche est , ligne

À éviter Mieux
def position(cell, ligne, table):
   return (ligne.index(cell),
           table.index(ligne))

def rang(cell, ligne):
   return sorted(ligne).index(cell)
def position(cell):
   return (cell.ligne.index(cell),
           cell.table.index(cell.ligne))

def rang(cell):
   return sorted(cell.ligne).index(cell)

Les fonctions position et rang n'ont maintenant besoin que d'un seul paramètre. Ceci permet de simplifier tout le code qui utilise ces fonctions.

Répétition de structure de contrôle

La partie répétée à gauche est for ligne in a: for colonne in b:

À éviter Mieux
if ajoute:
   for ligne in a:
      for colonne in b:
         ajouter(t[ligne][colonne])
else:
   for ligne in a:
      for colonne in b:
         enlever(t[ligne][colonne])
if ajoute:
   action = ajouter
else:
   action = enlever
for ligne in a:
   for colonne in b:
      action(t[ligne][colonne])

C'est une des manières de faire disparaître cette répétition, il y en a bien d'autres, voici d'autres méthodes en Python et langage C :

Générateur Python Macro C
def cellules(t):
   for ligne in a:
      for colonne in b:
         yield t[ligne][colonne]
if ajoute:
   for cellule in cellules():
      ajouter(cellule)
else:
   for cellule in cellules():
      enlever(cellule)
#define CELLULES(X) \
   for(int i=0; i < a ; i++) \
         for(int j=0; j < b ; j++) \
              X(t[i][j]) ;
if ( ajoute )
   CELLULES(ajouter) ;
else
   CELLULES(enlever) ;

Répétition syntaxique

Dans les langages s'inspirant de la syntaxe du langage C, répéter trois fois l'indice de boucle est une source classique d'erreurs. En effet, on fait une lecture globale du code sans lire le détail car toutes les boucles sont faites de la même façon. Il n'est donc pas facile de voir que l'indice n'est pas le bon.

La partie répétée à gauche est i

À éviter Mieux
for(i=0; i<n; i++)
  action(i)
#define FOR(V, DEBUT, FIN) \
   for(V=(DEBUT); V<(FIN); (V)++)
FOR(i, 0, n)
    action(i) ;
À éviter Mieux
for(i=liste->start; i; i = i->next)
  action(i)
#define ITEMS(I, LISTE) \
  for(I=(LISTE)->start;I;I=(I)->next
ITEMS(i, liste)
    action(i) ;

C'est une des façons de redéfinir la boucle for du langage C, on peut aussi créer un objet itérateur.

Répétition de variable

La partie répétée à gauche est t

À éviter Mieux
t = calloc(sizeof(*t), n) ;
#define CALLOC(V, N) \
    (V) = calloc(sizeof(*(V)), N)
CALLOC(t, n) ;

La majorité des erreurs d'allocation en langage C sont dues au fait que ce genre de macro n'est pas utilisé. Cette macro peut être améliorée afin de vérifier si la mémoire est disponible ou bien noter à quel endroit dans les sources elle a été allouée. Si cette macro n'est pas utilisée, pour faire ces améliorations il faut reprendre manuellement tout les calloc du code source pour les transformer, c'est un travail considérable qui n'est fait que si l'on est contraint.

Code trop spécialisé

Tout est répété à gauche sauf le + qui devient -. Vérifier ce fait en lisant le code prend du temps, c'est fatiguant mentalement et l'on peut laisser échapper des différences.

À éviter Mieux
def affiche_croissant(debut, fin):
   while debut != fin:
       print debut
       debut += 1
def affiche_decroissant(debut, fin):
   while debut != fin:
       print debut
       debut -= 1
def affiche(debut, fin, increment):
    while debut != fin:
       print debut
       debut += increment
def affiche_croissant(debut, fin):
    affiche(debut, fin, 1)
def affiche_decroissant(debut, fin):
    affiche(debut, fin, -1)

Ce code est imparfait car il peut se transformer en boucle infinie, et aussi parce que la variable debut est modifiée dans la fonction, son nom ne représente plus son contenu. En supprimant la répétition, on a une seule fonction à corriger.

Symétrie

Dans le code de gauche, la deuxième partie répète la première en inversant les deux variables et en faisant la négation.

À éviter Mieux
def soustrait(a, b):
   if hasattr(a, "soustrait"):
     return a.soustrait(b)
   if (hasattr(a, "ajoute")
       and hasattr(b, "negation")):
     return a.ajoute(b.negation())
   if hasattr(b, "soustrait"):
     return b.soustrait(a).negation()
   if (hasattr(b, "ajoute")
       and hasattr(a, "negation")):
     return b.ajoute(a.negation()
                    ).negation()
   on ne peut pas soustraire...
def soustrait(a, b, stop=False):
   if hasattr(a, "soustrait"):
      return a.soustrait(b)
   if (hasattr(a, "ajoute")
       and hasattr(b, "negation")):
      return a.ajoute(b.negation())
   if stop:
      on ne peut pas soustraire...
   else:
      return soustrait(b, a, stop=True
                      ).negation()

Dans la version de droite, on rend explicite la méthode. Si on a besoin d'ajouter un cas manquant, il suffit de le faire à un seul endroit.

Valeurs littérales

Elles sont à éviter car elles rendent le code difficilement modifiable sans le comprendre. Le code de gauche ne permet pas de savoir si 128 est lié à 256 ou bien si c'est une coïncidence :

À éviter Mieux
int i, t[256] ;
for(i = 0; i < 128; i += 2)
   { t[i] = 0 ; t[i+1] = 1 ; }
const int largeur = 256 ;
int i, t[largeur] ;
for(i = 0; i < largeur/2 ; i += 2)
   { t[i] = 0 ; t[i+1] = 1 ; }

Le code peut contenir des littéraux égaux représentant des informations complètement différentes. Ceci empêche de modifier le code en faisant de simple substitution de valeur, mais rend aussi la lecture plus difficile. Dans le code suivant, la valeur répétée est 256

À éviter Mieux
image = creer_table(256, 256)
for ligne in range(256):
   for col in range(256):
      image[ligne][col] = ligne + col
largeur = 256
hauteur = 256
image = creer_table(largeur,hauteur)
for ligne in range(hauteur):
   for col in range(largeur):
      image[ligne][col] = ligne + col

Il faut absolument nommer les littéraux pour que le code soit compréhensible et modifiable. On doit laisser les nombres dont la sémantique est leur propre valeur : 0 : valeur de départ, 1 : valeur suivante, 2 : double ou moitié.

Macro utile si vous programmez en langage C : #define TAILLE_TABLEAU(T) (sizeof(T)/sizeof(*(T)))

Fausses répétitions de types

La partie répétée à gauche est double

À éviter Mieux
struct point {
   double x, y ;
   double poids ; // entre 0 et 1
} ;
typedef double Coord ;
typedef double Reel_0_1 ;
struct point {
   Coord x, y ;
   Reel_0_1 poids ;
} ;

Ici aussi le fait d'éliminer la répétition en ajoutant de la sémantique évite d'avoir à commenter le source. Cela permet aussi de faire évoluer facilement les types utilisés.

Manque de modélisation du problème

À éviter Mieux
if i == 1: return 3
if i == 2: return 9
if i == 3: return 19
if i == 4: return 33
if i > 10000: return -4
if i > 1000: return -3
if i > 100: return -2
if i > 10: return -1
if i <= 4:
    return 2*i*i + 1
n = 0
while i > 10:
    n += 1
    i //= 10
return -n

Une erreur de typo dans le code de gauche n'est pas corrigeable car on ne sait pas ce que le programmeur a voulu faire. Que se passe-t-il dans les cas suivants i == 5, i < 0 ? Veut-on réellement retourner -4 pour i > 100000 ?

Comprendre facilement

Localiser les algorithmes

Une fonction doit contenir un seul algorithme. Si l'algorithme est en plusieurs parties, alors ces parties doivent être clairement séparées dans le source.

À éviter Mieux Après simplification
b = autre_calcul()
a = calcul_long()
if not b:
   return
return a * b
b = autre_calcul()
if not b:
   return
a = calcul_long()
return a * b
b = autre_calcul()
if b:
   return calcul_long() * b

Dans le code central le calcul de a ne coupe plus en deux le calcul de b, il est proche de l'endroit où il est utilisé et de plus il n'est pas calculé inutilement. Avec la nouvelle écriture, on s'aperçoit immédiatement que l'on peux simplifier le code.

Dans le code suivant, il faut regarder à plusieurs endroits pour comprendre ce qu'il se passe quand continuer devient faux.

À éviter Mieux
continuer = True
i = 1
while continuer and i < len(table):
   if table[i-1] == table[i]:
       continuer = False
   i += 1
if not continuer:
   return table[i-1]
i = 1
while i < len(table):
   if table[i-1] == table[i]:
       return table[i]
   i += 1

La construction if ... else

La négation est un opérateur qui n'est pas naturel pour un être humain, il ne faut pas faire de négation dans le test du if : il est préférable d'inverser les deux blocs d'instructions.

À éviter Mieux
if not a or not b:
    action1()
else:
    action2()
if a and b:
   action2()
else:
   action1()

Une exception néanmoins : si un des blocs fait une ligne et l'autre 20, il est préférable de mettre le petit bloc en premier. Ceci permet d'avoir une vision globale de la structure sans avoir besoin de la lire jusqu'à la fin.

Taille des fonctions

N'hésitez pas à faire des fonctions d'une seule ligne, cela ne ralenti pas le code dans les langages compilés et cela le rend facilement compréhensible car il n'y a pas besoin d'analyser l'état des différentes variables au cours de l'exécution de la fonction. D'autre part, le nom de la fonction est un commentaire explicite. La taille maximum d'une fonction est ce qui affichable sur une seule page écran, soit environ 60 lignes.

En faisant des fonctions petites, vous réduisez la redondance du code car vos fonctions sont facilement réutilisables. La taille occupée par le code dans la mémoire et surtout dans le cache instruction du processeur est moins importante. En conséquence, votre code peut s'exécuter plus rapidement. La même chose s'applique aux langages interprétés avec une compilation à la volée (JIT), le code généré occupera moins de place.

Return multiple

Il est généralement conseillé de faire un seul point de sortie par fonction, car cela évite de faire du copier/coller et permet de facilement faire évoluer la valeur de retour. Néanmoins, l'utilisation de plusieurs points de retour peut améliorer la lisibilité du code, on ne doit donc pas se l'interdire.

À éviter Mieux
entier = None
if chaine != "":
  signe = 1
  if chaine[0] == "-":
    signe = -1
  if chaine[0] in "-+":
    chaine = chaine[1:]
  if chaine == "0":
    entier = 0
  elif chaine:
    if chaine[0] != "0":
      while chaine and chaine[0].isdigit():
        entier = 10*entier + int(chaine[0])
        chaine = chaine[1:]
      if chaine == "":
        entier *= signe
      else:
        entier = None
return entier
if chaine == "":
   return
signe = 1
if chaine[0] == "-":
   signe = -1
if chaine[0] in "-+":
   chaine = chaine[1:]
if chaine == "0":
   return 0
if chaine == "" or chaine[0] == "0":
   return
entier = 0
while chaine and chaine[0].isdigit():
   entier = 10*entier + int(chaine[0])
   chaine = chaine[1:]
if chaine == "":
   return entier * signe

Le programme de droite est plus facilement compréhensible car il n'y a pas d'imbrications et que l'on peut le lire linéairement. En effet, il n'est pas nécessaire d'aller à la fin pour savoir comment le retour est fait.

Commentaires

Les commentaires doivent :

  • expliquer pourquoi cela a été programmé de cette manière.
  • expliquer pourquoi cela n'a pas été programmé autrement (plus simplement)
  • expliquer les problèmes restants dans le code. En Python la chaîne XXX indique un code qu'il faut terminer.
  • donner des exemples d'utilisation.

Les autres types de commentaires sont à remplacer par autre chose :

  • Une explication haut niveau de l'algorithme doit être simplement remplacée par un code source de même niveau. Il ne pourra donc pas y avoir de différence entre l'algorithme dans le commentaire et l'algorithme codé.
  • Les pré et post conditions ainsi que les invariants doivent être exprimés sous la forme d'assertions pouvant être remplacées par une impression dans un fichier de log (désactivable). Cela sera d'une aide précieuse pour le débuggage.
  • Une ensemble de lignes de codes formant une partie de l'algorithme doit être remplacé par une fonction. Le nom de la fonction remplacera le commentaire.
  • Dans beaucoup de cas, l'ajout de variables intermédiaires avec des noms explicites simplifie les expressions et rend inutile les commentaires.
  • Un commentaire de type TODO peut être remplacé par un appel à une fonction nommée TODO_...(...) ne faisant rien en attendant qu'elle fasse la bonne action. L'avantage est que l'API de la fonction à faire est intégré très tôt et que l'on a pas besoin de modifier le code quand la fonction est réellement implémentée.

Les docstrings : des commentaires très spéciaux

Ce sont des commentaires structurés qui peuvent être analysés par des outils extérieurs au langage ou bien par l'application en train de s'exécuter.

  • Générateurs de documentation comme JavaDoc, DOxygen...
  • Évaluateurs de tests unitaires.
  • Indications pour l'analyseur de code afin de l'aider. à détecter les problèmes ou les utilisations normales.

Exemple de docstring en python 2, expliquant ce que fait la fonction et donnant des exemples qui seront vérifiés automatiquement lors des tests unitaires.

def add(a, b):
    # type: (int, int) -> int
    """Cette fonction retourne la somme de 2 entiers

        :Example:

        >>> add(1, 1)
        2
        >>> add(1, -1)
        0
    """
    return a + b

Des constructions classiques du langage

Les programmeurs ne connaissent pas les priorités de tous les opérateurs. Donc pour les opérateurs autres que les 4 opérations, les comparaisons et les opérateurs booléens, il est préférable d'ajouter les parenthèses même si elles sont inutiles. De même pour l'associativité des opérateurs si cela influence le résultat : float i=1 ; (1000*i)/1000 - 1000*(i/1000) = 5.42101e-20

En règle générale, il est préférable d'être explicite plutôt que d'utiliser des comportements implicites, cela montre à celui qui lit le code que le comportement est intentionnel. Cela permet de mettre en évidence ce que veut faire le programmeur.

Les langages de programmation comportent souvent des constructions inhabituelles, qui ne sont pas compréhensibles par un programmeur qui n'est pas un expert du langage. Le but à atteindre est qu'une très grande partie du code source soit lisible par un non-initié.

Lisible de manière linéaire

Pour quelqu'un ne connaissant pas le code, il est préférable de mettre les fonctions bas niveau en début de code. Cela permet de commencer par des fonctions simples, de comprendre les entités de base, pour finalement analyser les fonctions de haut niveau sans avoir besoin de faire des aller/retour.

De bons identificateurs

Les identificateurs de variables, fonctions, classes et fichiers doivent expliquer ce qu'ils représentent, voici de mauvais identificateurs :

  • a, b, c... sont à proscrire, mais i, j, k sont recommandés comme indice de boucle.
  • toto, truc, machin... qui dénotent que le programmeur travaille dans l'urgence ou ne sait même pas ce que représente l'identificateur.
  • entier, flottant, longueur... nommer une chose par son type apporte rarement de l'information au lecteur.
  • Les acronymes composés avec des premières lettres de mot rendent le code incompréhensible pour les non initiés. Il ne faut pas en utiliser même si le code est visuellement plus court.

Les identificateurs doivent pouvoir être compris hors contexte. En effet une personne découvrant du code ne connaît pas l'algorithme, de bons identificateurs vont lui permettre de le comprendre plus rapidement.

Si les noms de variable et de fonction sont longs, cela incitera à coder des expressions plus simples dans le code. Cela impliquera la création de variables intermédiaires dont le nom aide à la compréhension de l'algorithme.

Des identificateurs sans fautes d'orthographe, et qui s'ils sont en français ne font pas un contre sens du fait du manque d'accent.

Des noms standardisés à travers tout le code, que ce soit dans les codes sources, les noms des fichiers et les autres ressources (fichier CSS par exemple). Cela simplifie l'exploration du code. Il faut bien sûr que les noms soient suffisamment longs pour ne pas être ambigus. Si vous utilisez user_key comme nom de variable, il faut le réutiliser partout dans le code exactement de la même façon.

Dans les différents langages de programmation, il y a des conventions indiquant quand utiliser les majuscules, soulignés et autres symboles. Respectez-les ! Certaines conventions indiquent le type de la variable dans son nom, c'est une béquille imposée aux programmeurs mettant de mauvais noms aux variables.

Attention, des expérimentations ont montré que les programmeurs débutants mettent le même temps (très long) pour comprendre un code avec de bons noms de variable que le même code avec des noms n'ayant aucune signification. Il est donc difficile de les convaincre de nommer correctement leurs variables. Les programmeurs expérimentés analysent bien plus rapidement un code avec de bons identificateurs.

Conventions de présentation

Indentation

Ce décalage à droite en fonction de la profondeur dans une structure est fondamental car il permet d'appréhender visuellement les imbrications sans avoir besoin de lire. Une indentation fausse va induire en erreur le lecteur. Dans la réalité, des mauvaises indentations ont été utilisées afin de camoufler un bug créant une faille de sécurité.

Espacement fixe (fonte non-proportionnelle)

Il permet de faire des alignements verticaux dans le code en mettant en évidence des répétitions entre les lignes. Il est possible de faire de l'ascii art dans les commentaires afin d'expliquer les algorithmes. L'alignement vertical fait ressortir les différences entre les lignes.

calcul_point(latitude      , longitude       , &point[0], rayon, centre) ;
calcul_point(latitude+d_lat, longitude       , &point[1], rayon, centre) ;
calcul_point(latitude+d_lat, longitude+d_long, &point[2], rayon, centre) ;
calcul_point(latitude      , longitude+d_long, &point[3], rayon, centre) ;

Longueur des lignes

Des tests psychologiques ont montré que l'on lisait bien moins vite les textes formés de nombreux symboles sur la même ligne. Les standards de programmation limitent les lignes à 80 colonnes. Utiliser une fenêtre plus large est une perte de place car la partie droite reste vide. Le programmeurs débutants élargissent les fenêtres car leur code contient de nombreux niveaux d'indentations, ceci devrait être un signal leur indiquant que leur fonction est trop complexe et devrait être découpée en plusieurs fonctions.

Espacement vertical et horizontal

On utilise des lignes vides pour espacer verticalement le code, afin de séparer différentes parties d'un algorithme dans une fonction. Il ne faut pas abuser de cet espacement car la place verticale ne doit pas être gaspillée et son utilisation abusive lui fait perdre toute valeur.

On utilise le caractère espace pour espacer horizontalement, il permet entre autre :

  • de rendre le code plus lisible en respectant les conventions d'écriture de la langue. En français : espace avant ';' et '(' et après ')' et ','
  • de mettre en évidence l'ordre des opérations dans une formule complexe en mettant d'autant plus d'espace autour d'un opérateur qu'il est moins prioritaire.
  • d'aligner verticalement les codes identiques.

Une instruction par ligne

Mettre plusieurs instructions par ligne pour gagner de la place verticalement est une mauvaise idée car les instructions en fin de ligne peuvent échapper au regard lors d'une lecture rapide. En effet, toutes les conventions de codage imposant une seule instruction par ligne, les programmeurs ont pris cette habitude de lecture. Néanmoins, dans certains cas, le fait de mettre plusieurs instructions par ligne permet de rendre le code plus lisible car cela rend visible une structure répétitive.

switch(direction)
  {
     case Left : x-- ; c = '<' ; break ;
     case Right: x++ ; c = '>' ; break ;
     case Up   : y-- ; c = '^' ; break ;
     case Down : y++ ; c = 'V' ; break ;
  }

Découpage des lignes

Si une ligne dépasse les 80 symboles, il faut la découper, plusieurs possibilités :

  • Si l'indentation est trop grande, il faut subdiviser la fonction.
  • Si l'expression est trop compliquée, il faut utiliser des variables intermédiaires qui rendront le code facilement compréhensible si les noms sont bien choisis.
  • Sinon, découper aux endroits logiques (opérateurs de faible priorité) en tentant de faire apparaître des alignements verticaux si c'est possible.

Condition

Dans les conditions comme a == b la convention utilisée par les mathématiciens et les informaticiens est de mettre ce qui change le plus rapidement à gauche. Il faut l'appliquer car elle permet de lire plus rapidement le code sans se poser de question. Certaines personnes recommandent d'écrire 5 == a afin que le compilateur déclenche une erreur dans le cas où l'on écrit 5 = a par erreur. Les compilateurs préviennent maintenant quand on fait une affectation dans une condition, il n'est donc pas nécessaire d'écrire la condition à l'envers.

Il est interdit de faire des affectations dans les conditions !

Comment débugger ?

Cela dépend du langage de programmation utilisé, de l'application qui est développée et du type de bug rencontré.

Les outils à utiliser sont intégrés ou non à votre environnement de développement. Il faut utiliser ceux qui sont le plus adaptés à l'application qui est développée ou créer les vôtres.

Analyse statique du code

Utilisez des outils d'analyse de code statique qui vous donnerons des indications sur les endroits problématiques dans votre code source. Problèmes au niveau de la logique du code, de sa lisibilité, de sa maintenabilité, des erreurs de programmation courantes... Pour le langage C, le premier outil de ce type s'appelait lint. On appelle souvent ces programmes des linters.

Au final, après un très long rapport, ces programmes assignent une note globale au code source, c'est toujours un plaisir de réussir à l'améliorer.

Analyse de l'exécution

Cette analyse du programme au cours de son fonctionnement peut se faire de plusieurs manières.

En modifiant le code exécutable du programme pour récupérer les informations (c'est de l'instrumentation). Le programme ne sera donc plus le même et cela peut faire disparaître le problème à analyser. L'analyse de la couverture de code (code coverage) et du nombre d'appels des fonctions nécessitent l'instrumentation du code. On peut ajouter dynamiquement de l'instrumentation dans le noyau Linux à des endroits prédéfinis.

D'autres examinent son fonctionnement sans le modifier, mais en le traçant. Par exemple les profiler peuvent interrompre régulièrement le programme pour regarder quelle instruction est en train d'être exécutée. Les débuggeurs aussi prennent le contrôle du programme en train de s'exécuter sans le modifier. Pour cela ils utilisent l'appel système ptrace.

D'autres enfin émulent totalement son fonctionnement au niveau des instructions du processeur. Cela ralentit considérablement l'exécution mais permet d'analyser finement son fonctionnement. valgrind est le plus connu des programmes faisant cela. Cela lui permet de détecter la plupart des problèmes d'accès incohérents à la mémoire.

Faire afficher des traces par le programme lui même

Dans certains cas les outils précédents ne suffisent pas. Il est alors pratique d'ajouter des impressions dans l'application afin d'avoir une trace lisible du fonctionnement des algorithmes.

Pour les applications complexes, il est préférable de mettre directement ces traces dans le code en les intégrant au système de log de l'application. Il est alors facile de les activer ou désactiver dynamiquement ou statiquement, globalement ou localement.

À éviter Mieux
// Met à 0 la table
for(i=0; i < SIZE(t) ; i++)
   t[i] = 0 ;
#define PRINTF printf
[...]
PRINTF("Met à 0 la table [%d]\n", SIZE(t)) ;
for(i=0; i < SIZE(t) ; i++)
    t[i] = 0 ;

Dans l'exemple précédent, PRINTF peut être améliorée pour indenter en fonction de la profondeur de récursion ou bien pour ajouter de la couleur.

Rapport d'erreur

Lorsqu'une application plante ou qu'une exception se produit, il est intéressant d'avoir une trace de ce qu'il s'est passé.

Une façon simple de faire cela est d'ajouter une méthode backtrace à tous les objets qui sont intéressants. En cas de problème on affiche la pile d'appel des fonctions en appellant la méthode backtrace sur toutes instances définies dans les variables locales à chaque niveau.

On peut pour améliorer l'affichage générer ces traces en HTML et se les faire envoyer par mail dans le cas d'une application en production.

pre et post conditions

Il est important d'ajouter des pre et post conditions car elles permettent de détecter les problèmes le plus rapidement possible et non longtemps après.

De plus elles font parties de la documentation et aident le développeur à comprendre le code.

Si c'est possible, il faut les laisser dans l'application en production.

À éviter Mieux
unsigned int somme(unsigned int a,
                   unsigned int b)
{
 // Il ne faut pas appeler
 // cette fonction si un
 // débordement est possible
 return a + b ;
}
unsigned int somme(unsigned int a,
                   unsigned int b)
{
 // Refusera à tord d'ajouter 2^31 et 1
 assert( ((a | b) >> (sizeof(a)*8 - 1))
                == 0 ) ;
 return a + b ;
}

Intégrer le debuggage à l'application

Dans le cas d'une application complexe avec une interface utilisateur, il est intéressant d'ajouter des outils d'aide au développement directement sur l'interface utilisateur. Par exemple en associant à chaque élément de l'interface utilisateur un bulle d'aide permettant :

  • de savoir par quel fichier source l'élément a été créé ;
  • de savoir quel est son identificateur ;
  • de recharger dynamiquement le code exécutable associé à cet élément s'il a été corrigé ;
  • d'activer les traces d'exécution ;
  • d'activer les pre et post conditions coûteuses ;
  • d'avoir des statistiques ;
  • d'afficher lisiblement et interactivement le détail des structures de données associées ;
  • ...

Choix de l'éditeur de texte

Pour travailler de manière efficace il faut utiliser de bons outils. Plus l'outil est puissant, plus il vous permettra de gagner du temps. Évidemment un outil puissant nécessite une prise en main longue. Par exemple «emacs» possède des centaines de raccourcis clavier et des milliers de fonctions.

Comme l'éditeur de texte est un outil complexe et long à maîtriser, il est dommage d'en apprendre plusieurs, il est donc préférable d'en maîtriser un seul et de l'utiliser pour éditer tous les types de fichier.

La suite de cette section indique les fonctionnalités minimales qu'un éditeur de texte doit posséder. Ensuite, c'est à vous de voir si l'outil que vous utilisez a ces fonctionnalités.

Fiabilité

Vous devez pouvoir faire confiance à votre éditeur de texte, il ne doit pas :

  • perdre le contenu du fichier au moment de la sauvegarde ;
  • perdre vos modifications en cas de coupure de courant ;
  • modifier tout seul le contenu des fichiers ;
  • changer tout seul l'encodage des fichiers, que cela soit pour les accents ou l'encodage de fin de ligne ;
  • planter.

Par contre, l'éditeur doit être capable :

  • d'éditer des fichiers binaires ;
  • d'éditer des fichiers de plus de 100Mo ;
  • de travailler avec des fichiers dont le contenu est sur une seule ligne ;
  • d'afficher correctement le contenu du fichier sans rien oublier.

Et il doit vous prévenir

  • si le fichier que vous sauvegardez a été modifié sur le disque ;
  • si quelqu'un d'autre édite le même fichier ;
  • si vous quittez l'éditeur sans sauvegarder.

L'éditeur de texte que vous utilisez doit impérativement respecter tous les points précédents. Si ce n'est pas le cas, ne l'utilisez pas, un éditeur non fiable ne vous fera pas perdre des heures, mais des jours de travail.

Affichage

Voici quelques points qui vous feront gagner du temps :

  • Il doit afficher interactivement la coloration syntaxique en fonction du langage de programmation.
  • Il doit afficher interactivement les problèmes de compilation et les conseils de programmation trouvés par les outils d'annotation de code.
  • Il doit afficher l'espace insécable différemment de l'espace normal.
  • Il doit utiliser une fonte à espacement fixe pour l'affichage afin de pouvoir aligner verticalement le code.
  • Il doit mettre en évidence le "bloc" dans lequel se trouve le curseur. Le bloc pouvant être sur plusieurs lignes pour les structures de contrôle ou dans la même ligne pour les expressions mathématiques parenthésées.
  • Il doit pouvoir cacher ou montrer des blocs de texte.
  • L'ascenseur vertical doit pouvoir afficher une version réduite du document.
  • L'ascenseur vertical doit pouvoir afficher les endroits où sont trouvés la chaîne de caractère que l'on est en train de chercher.
  • Il doit pouvoir fonctionner en mode texte dans un environnement qui n'est pas graphique, par exemple sur une machine distante sur laquelle on se connecte via 'ssh'.
  • Il ne doit pas avoir d'ascenseur horizontal par défaut, voici quelques raisons :
    • Lors de l'édition de source, il peut y avoir du code non affiché à droite de l'écran et l'on peut facilement l'oublier.
    • Normalement le code ne doit pas 80 colonnes donc on n'a pas besoin de défilement horizontal.
    • Si le fichier contient une ligne de 100000 caractères, elle est difficilement éditable.
  • On doit pouvoir cacher ou masquer tout ce qui n'est pas du code car c'est la seule chose importante : les barres de menu, les onglets, les fenêtres de navigation. En effet, quand il y a de nombreuses entités, l'accès à un fichier ou à une fonction en tapant le début de son nom est bien plus rapide que d'avoir à le rechercher en lisant sur l'écran.

Édition du texte

Outre les fonctions classiques d'édition de texte, l'éditeur doit :

  • comprendre les expressions régulières étendues ;

  • pouvoir faire un 'undo', mais cela seulement sur la zone de texte actuellement sélectionnée ;

  • indenter correctement automatiquement ;

  • permettre de travailler sur des zones rectangulaires de texte ;

  • permettre de faire de la complétion de mot.

    Cette complétion doit fonctionner de la manière la plus intelligente possible en vous proposant les derniers mots utilisés, les mots proches de l'endroit où vous êtes, les mots des fichiers ouverts et ce quel que soit leur type.

    Mais aussi une complétion qui peut utiliser la connaissance du langage édité pour proposer des valeurs qui ne produiront pas d'erreur de compilation ou d'exécution.

Divers

Quelques points non classables :

  • L'éditeur doit permettre simplement de faire interactivement des macros utilisant toutes les fonctionnalités de l'éditeur. Quand vous relancez la macro, toutes les actions que vous avez faites sont répétées.
  • Il doit permettre de répéter automatiquement une commande plusieurs fois.
  • L'utilisation de la souris ne doit pas être obligatoire, car elle impose de lâcher le clavier, ce qui fait perdre du temps.
  • L'éditeur doit pouvoir être rapidement lancé à partir du shell pour éditer un fichier dans le répertoire courant.

Que faut-il utiliser ?

Les remarques personnelles qui suivent vous indiquent les éditeurs que vous ne devriez pas utiliser :

  • Ceux qui sont seulement graphiques, ce qui est déjà une énorme tare :

    • Pas «gedit», car il n'est pas fiable.
    • Pas «sublime», car il est payant.
    • Pas «eclipse» et «Atom», car ils sont trop lourds.
    • Pas «Kate», sauf si vous utilisez KDE.
    • Pas «QtCreator», car c'est trop spécialisé.
    • Pas «PyCharm», car il crée 3000 fichiers de configuration.
    • Pas «Geany», car il n'indente pas correctement, fait mal les complétions et n'affiche pas les espaces insécables.
  • Pas «emacs», car les raccourcis clavier sont trop longs et non standards.

  • Pas «vi» ou «vim», car ils ne sont pas intuitifs.

  • Pas «nano», car il n'est pas assez puissant.

Le plus agréable des éditeurs graphiques est actuellement «Geany», mais c'est un avis personnel. Le plus puissant reste toujours «emacs». J'entend dire beaucoup de bien de «Visual Studio Code», mais il faut une machine puissante.

Néanmoins, vous devez savoir utiliser «vi» car :

  • Vous le trouverez sur tous les Unix, même dans les Unix embarqués.
  • Il est léger et rapide.
  • C'est l'éditeur qui vous permettra de ne pas avoir de problèmes articulaires, car il n'utilise ni la touche contrôle ni la touche alt.

Choix du langage de programmation

Excepté si le langage est imposé, il faut choisir un langage adapté à ce que l'on veut faire parmis ceux possibles sur les machines qui devront faire fonctionner le programme.

Performance d'exécution

Les ordinateurs sont maintenant rapides et pour la très grande majorité des applications la performance CPU ne doit pas être un critère de sélection.

Dans le cas d'application nécessitant de la performance, elle est généralement liée à une fonction très particulière de l'application et non à l'ensemble du code. Dans ce cas il peut être intéressant de coder dans 2 langages différents.

Généralement, si un langage s'exécute rapidement :

  • Le code est plus complexe à écrire.
  • La compilation prend du temps.

Si l'application nécessite de très grandes performances, il faut dès le départ la concevoir comme une application distribuée fonctionnant sur des machines éloignées. Ainsi il n'y aura pas de goulot d'étranglement que cela soit au niveau processeur, disque ou réseau. De plus on pourra rapprocher géographiquement les serveurs et les clients.

Failles de sécurité

Si la sécurité est importante, il faut que le langage garantisse que les opérations dangereuses ne soient pas exécutées :

  • Utilisation directe de données utilisateurs sans les valider. (notion de tainted data)
  • Débordement de tableau.
  • Cloisonnement des structures de données rendant l'accès impossibles aux valeurs non utiles.
  • Données immutables par défaut.
  • Gestion automatique des valeurs sentinelles. Ce sont des valeurs particulières retournées par les fonctions pour indiquer des cas spéciaux à ne pas traiter comme les autres. De nombreux bugs sont dus au fait que l'on oublie de les vérifier.

Il faut que la communauté autour de ce langage soit vivante afin de corriger les problèmes de sécurité qui seront découverts.

Travail collaboratif

Si l'on travaille à plusieurs, il est préférable que le langage impose le respect de contrat :

  • Type des paramètres en entrée et en sortie.
  • Précondition devant être vérifiée par les paramètres.
  • Postcondition devant être vérifiées sur la sortie.
  • Une documentation avec des exemples d'utilisation qui sont aussi des tests de régression.

Cela permettra à de nombreuses personnes de travailler dans la sérénité.

Qui permet de faire un code court

Un code court (en nombre de tokens) :

  • Comporte moins d'erreur.
  • Est lisible plus facilement.
  • Est plus facilement modifiable.
  • Est plus vite écrit.

Avec un écosystème

A part dans le cas d'une application très pointue pour laquelle un langage spécialisé est préférable. Il vaut mieux choisir un langage connu par plein de gens, même s'il est moins bien. Ces langages connus par plein de gens sont "généralement" facile à apprendre (C++ est une exception).

Du fait du grand nombre de personnes programmant avec ces langages, ils permettent d'avoir :

  • Des librairies qui vous permettent de faire pleins de choses
  • Débuggeur.
  • Analyseur de performance.
  • Analyseur de beauté de code (linter).
  • Transpiler permettant de le traduire en JavaScript par exemple.
  • Gestionnaire de paquets.
  • Plein de réponses à vos questions sur le web.

Mais aussi d'être portable sur plein de systèmes d'exploitation et sur des microcontroleurs.

Choix de l'environnement de travail

Cette partie est réellement personnelle et peut éventuellement ne pas vous convenir. Âmes sensibles passez votre chemin.

Pour pouvoir travailler rapidement il faut que l'environnement de travail soit une extension de votre corps. Vous l'utiliserez alors sans vous poser la moindre question. C'est possible avec votre corps grâce à la proprioception qui vous permet de connaître la position dans l'espace de toutes les parties de votre corps sans avoir besoin de les regarder.

Il faut donc un environnement de travail qui vous permette de naviguer sans avoir besoin de regarder l'écran. Vous n'aurez alors plus besoin de constamment analyser ce qu'il se passe sur l'écran avant de continuer à travailler. Vous pourrez alors enchaîner les actions aussi vite qu'elles vous viennent à l'esprit sans avoir à attendre une réaction de la machine. Et vous n'aurez plus la charge cognitive liée à l'analyse de tout ce qu'il se passe sur l'écran.

Pour arriver à ce résultat, j'ai configuré mon gestionnaire de fenêtre en mettant tous les raccourcis (sauf «Alt+Tab») sur le pavé numérique. Voici 3 bonnes raisons pour le faire :

  • Quand vous programmez, vous tapez rarement de nombreux chiffres. il est donc plus rapide de les saisir en utilisant le haut du clavier texte plutôt que le pavé numérique. Vous pourrez même utiliser les 2 mains. Le clavier numérique est donc inutile dans un contexte de programmation.
  • Les raccourcis clavier des gestionnaires de fenêtre sont souvent en conflit avec ceux des applications. Ceci empêche donc d'utiliser tous les raccourcis de l'application.
  • Les neufs chiffres du pavé numérique peuvent être des raccourcis directs (sans Alt, Ctrl, Meta) vers neufs bureaux virtuels. À chaque environnement vous associerez une tâche qui ne changera pas : développement, mail, web, administration, postits, chats... Les gestionnaires de temps de travail peuvent comptabiliser le temps passé sur chaque bureau.

La plupart du temps il y aura une unique fenêtre sur chaque environnement, il ne sera donc pas nécessaire de changer de fenêtre car elle s'activera automatiquement. Concernant le focus :

  • Si votre environnement de travail dans le monde réel n'est pas encombré et est stable (pas sur un bateau avec un bureau minuscule et 3 chats) cliquer pour désigner la fenêtre qui a besoin du focus est une perte de temps et d'énergie.
  • Et quand vous cliquez dans une fenêtre (pas son titre) cela ne doit pas la faire passer en premier plan. Classiquement elle va passer devant la fenêtre dans laquelle vous voulez coller ce que vous venez de copier.

Quand il y a plusieurs fenêtres sur le même bureau, pour activer rapidement la bonne fenêtre il suffit de 3 raccourcis :

  • «Alt-Tab» qui a le fonctionnement classique.
  • Et les «-» et «+» du clavier numérique qui permettent de faire passer la fenêtre courante derrière ou devant les autres.

Si chaque bureau contient 2 paires de fenêtres supperposées on peut atteindre une fenêtre parmi le 36 en moins d'une demi-seconde :

  • 1 chiffre : on atteint 9 fenêtres
  • 1 chiffre + «Alt-Tab» : on atteint 9 fenêtres
  • 1 chiffre + «-» : on atteint 9 fenêtres
  • 1 chiffre + «Alt-Tab» + «-» : on atteint 9 fenêtres

Que mettre d'autre de manière logique sur le pavé numérique ?

  • «Ctrl+chiffre» déplace la fenêtre sur le bureau indiqué.
  • «/» maximize verticalement ou annule la maximisation.
  • «Enter» maximize en essayant de pas cacher les autres fenêtres (ou l'annule).
  • «*» plein écran (ou l'annule).
  • «Alt+. (Suppr)» ferme la fenêtre (la touche porte bien son nom).
  • «0 (Inser)» affiche le lanceur d'application car ce n'est pas dangereux ce qui est bien car il est lancé par une grosse touche.
  • «Alt+123456789» lance ou ouvre vos fenêtres favorites. Ne faite cette association que si vous les utilisez plusieurs fois par jour sinon vous ne vous en souviendrez pas et repasserez par le lanceur d'application.

Avec autant d'espace disponible il n'est plus nécessaire d'iconifier les fenêtres, c'est aussi un gain de temps.

Malheureusement un environnement graphique avec multifenêtrage a un gros défaut : les ouvertures de fenêtres ne sont pas instantanées et donc les événements clavier peuvent arriver dans la mauvaise fenêtre si vous tapez au clavier trop rapidement. C'est un énorme avantage d'Emacs : comme tout est fait de manière synchrone dans la même fenêtre (qui peut se subdiviser) ce que vous tapez au clavier arrivera toujours au bon endroit.

Concernant les terminaux, lors de la connexion ouvrez de manière automatique un terminal avec autant d'onglets que de projets en cours ou de sessions SSH sur des machines distantes. Les onglets toujours dans le même ordre afin que vous puissiez très rapidement vous mettre dans le bon contexte. Personnellement j'ai toujours un onglet Python ouvert que j'utilise comme calculatrice.

Et finalement, programmer est une tâche nécessitant de la concentration. Il faut donc enlever tout ce qui peut vous distraire, notamment toutes les alertes visuelles ou sonores (nouveaux messages par exemple).