Plan
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 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.
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.
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.
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.
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) ; |
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.
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.
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.
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.
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)))
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.
À é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
?
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 |
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.
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.
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.
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.
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
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 f=1 ; (1000*f)/1000 - 1000*(f/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é.
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.
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.
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é.
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) ; |
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.
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.
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 ; } |
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.
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 !
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.
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.
Pour Python, pylint
vérifie la propreté du source
et mypy
fait une vérification statique des types
dans les fonctions que vous aurez typées.
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. Le code coverage permet d'aider à écrire les test de régression mais aussi de trouver les codes morts car ils ne peuvent jamais être exécutés.
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.
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.
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.
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 ; } |
Le Fuzzing est un ensemble de techniques générant des entrées (claviers, souris, fichiers, paramètres...) "aléatoires" pour votre programme afin de le faire "planter".
Curieusement, ces techniques fonctionnent bien car les programmeurs ne pensent pas aux situations absurdes qui peuvent se produire.
Les meilleurs fuzzers sont ceux qui instrumentent l'application afin de déterminer où sont les tests. Ils peuvent alors modifier les données envoyées à l'application afin d'explorer une maximum de fil d'exécution. En 2014 un fuzzer a été capable de générer des images PNG valides en lançant un convertisseur d'image jusqu'à ce que celui-ci réussisse à faire la conversion.
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 ;
- ...
Les tests de régression sont fondamentaux et doivent obligatoirement être écrits si votre code :
- sera utilisé longtemps ;
- sera utilisé dans des environnements différents : compilateur, processeur, version de langage, langue de l'utilisateur... ;
- devra évoluer.
A vous de voir comment les écrire, cela dépend de ce que vous êtes en train de créer et des environnements de tests standards pour le langage de programmation utilisé. Quelques conseils généraux :
- faire des tests courts en temps d'exécution ;
- indépendants les uns des autres, et donc exécutable dans n'importe quel ordre ;
- vérifiant chacun le minimum de choses ;
- documentant ce qu'ils font et pourquoi c'est le résultat attendu ;
- stockés chacun dans un fichier séparé afin de pouvoir facilement utiliser
git bisect run
pour trouver automatiquement quand un bug a été introduit.
Les tests unitaires sont de bons candidats pour être inclus dans la documentation utilisateur dans le cas de librairies car ils testent normalement tous les cas d'utilisation.
Avoir des tests de couvertures n'atteignant pas 100% indiquent que vos tests de régression ne sont pas complets. Ce test de couverture va vous permettre de :
- trouver les lignes qui ne pourront jamais être exécutées, il faut alors les supprimer ou bien lancer une exception incluant le contexte permettant de reproduire le problème ;
- vous guider pour compléter vos tests de régressions ;
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.
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.
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 dépsser 80 colonnes on n'a donc 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.
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.
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.
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.
Je suis passé à «Visual Studio Code» mais il faut une machine puissante. Attention, il ne gère pas correctement les liens symboliques.
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.
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.
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.
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.
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é.
Un code court (en nombre de tokens) :
- Comporte moins d'erreur.
- Est lisible plus facilement.
- Est plus facilement modifiable.
- Est plus vite écrit.
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 microcontrôleurs.
Les textes qui suivent, sont des réflexions personnelles, pas des vérités absolues.
Les programmes multithreads :
- ne sont pas humainement réalisables sans bugs.
- ne passent pas à l'échelle quand il y a de très nombreuses threads.
- n'optimisent pas l'utilisation du cache processeur.
Pour résoudre ces problèmes, on peut :
lancer des processus qui partageront le minimum de mémoire ;
lancer un nombre restreint de threads/processus fonction du nombre de processeurs physique ;
Spécialiser chaque thread/processus pour une tâche donnée, l'idée étant qu'il ne puisse y avoir qu'un seul écrivain possible pour chaque structure de donnée.
utiliser des sockets qui permettront de répartir les processus sur de nombreuses machines ;
profiter du langage de programmation :
- En JavaScript, il n'y a pas de threads mais des workers ne partageant pas la mémoire. Il n'y a donc pas de sections critiques à protéger.
- En Rust, le compilateur vous prévient quand vous avez des problèmes de section critique.
- En Python, la programmation asynchrone ne permet pas de faire de parallélisme mais permet d'ordonnancer vous même les tâches à exécuter en indiquant les points de préemptions. Toute suite d'instructions sans point de préemption est donc automatiquement protégée de tout accès concurrent.
C'est un principe permettant de minimiser les efforts du programmeur pour le developpement et le debuggage. Cela permet d'offrir aux utilisateurs des fonctionnalités qui sont généralement complexes à programmer.
Le fonctionnement d'une application journalisée est le suivant :
L'utilisateur fait une action.
L'action est validée afin de voir si elle est réalisable et autorisée. Si ce n'est pas le cas, l'utilisateur est prévenu. Si l'action est possible :
- L'action est enregistrée dans un journal.
- L'action est exécutée afin de mettre à jour les structures de données de l'application.
- L'action génère ensuite un retour à l'utilisateur
Lors du démarrage de l'application journalisée toutes les actions enregistrées dans les journaux sont exécutées sans vérifications de validités ni de retour utilisateur.
Avantages :
- Les journaux ne peuvent pas perdre d'information car ils sont ouverts en mode ajout.
- Le journal contient l'historique des modifications, qui peut donc être affiché à l'utilisateur.
- Faire un 'undo' revient à réexécuter le journal sans la dernière entrée.
- Des sauvegardes régulières peuvent être faites de manière efficace avec
git
si les journaux sont enregistrés en simple texte.- Si le journal est une suite de valeurs JSON, il peut directement être traité par un navigateur web.
- En cas de bug, rejouer le journal le fera réapparaître.
- On peut optimiser le temps processeur ou l'espace mémoire en travaillant sur les vrais données.
- Si les structures de données internes changent, les journaux restent inchangés.
- On a une trace des actions des utilisateurs permettant de mettre en oeuvre des aides personnalisées.
L'inconvénient est que la lecture du journal prend du temps. Il est donc nécessaire d'avoir plusieurs journaux : un par structure de donnée manipulée. Cela permet de garder des journaux de taille raisonnable.
L'application est programmée de telle manière que l'ajout de fonctionnalité ne modifie pas le code source existant.
Les plugins sont le premier concept permettant de réaliser cela. Mais il est aussi nécessaire de modifier le fonctionnement de fonctionnalités qui existent déjà, sans modifier leur code source.
Ceci peut être fait en faisant un prétraitement sur les arguments des fonctions de l'autre fonctionnalité et/ou un post traitement sur leurs sorties.
Il est possible de créer des applications complexes sur ce principe en utilisant le FyHooks framework
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).