Conventions et conseils de programmation pour un code propre

    Pour chaque langage de programmation, il existe une ou plusieurs normes de programmations spécifiques, définies par des standards ISO. En C par exemple, il existe la norme ANSI C ou la norme C99. Le document suivant est une compilation de différentes normes permettant d'élaborer des conventions de programmation plus ou moins génériques, sortes de préceptes pour un code propre. En tant que conventions, les arrangements ci-dessous présentent des choix arbitraires, réfléchis toutefois pour structurer le code selon un maximum de clarté et de logique.
     Certains pensent que structurer, indenter, commenter est une perte de temps dans le processus de création de code et développent donc en deux temps: coder moche puis réindenter -et commenter à peine. Ceux-là se trompent gravement: coder propre et respecter dès le début d'un projet des conventions strictes permet un gain de temps considérable sur le long terme. Dès que le programme se complexifie, que les contributeurs se multiplient, que des relectures et des mises à jour doivent être faites, l'avantage d'un code propre est un bénéfice immense. Le suivi et la rigueur des conventions, la structuration logique et réfléchie ainsi que les commentaires abondants et pertinents marquent la différence entre un code propre, beau et admirable et le code juste 'qui marche', produit par le développeur moyen.

Conventions d'écriture

- Fonctions: Une fonction commence et s'écrit en minuscule. Son nom contient un verbe d'action à l'infinitif décrivant la fonction. Ce verbe est complété si nécessaire pour décrire au plus juste la fonction. Dans le cas général, il convient d'éviter de créer des fonctions prenant une liste trop importante de paramètres. Si toutefois le cas se présente, les paramètres pourront être placés en colonne pour être visible sur la même page.
exemples:
void dessiner_voiture(Modele modModele, int iNiveauDetail, bool bInterieur);
void dessiner_voiture(Modele modModele,
                                   int iNiveauDetail,
                                   bool bInterieur,
                                   int iCouleur,
                                   Materiau matTextureSiege,
                                   int iNombreSiege);
 
- Variables et Tableaux: Une variable commence par une minuscule et ne contient pas de verbe à l'infinitif. Son nom ne contient que des chiffres et des lettres ainsi que le symbole underscore '_'. Elle commence toujours par une lettre ou '_' et la ponctuation (., -, !, ....) ansi que les caractères accentués (é, è, ê, à,...) sont interdits.
exemples:
int iCouleur;
float fPrix;
 
    Remarquez que les exemples présentés jusqu'ici présentent des variables préfixées par une lettre. Il s'agit de la notation Hongroise inventée par Charles Simonyi, programmeur chez Xerox puis architecte en chef au sein de Microsoft. Cette notation préconise de préfixer les variables selon une convention précise suivant leur type. La lettre suivant le préfixe est nécessairement une majuscule.
  1. prefixes description
  2. i int (entier)
  3. n short int (entier court)
  4. l long int (entier long)
  5. f float (nombre a virgule flottante)
  6. d double (float double)
  7. c char (caractere)
  8. by byte (caractere non signe)
  9. b boolean (booleen true/false)
  10. s string (chaine de caracteres)
  11. w word (mot = double octet)
  12. dw double word (double mot)
  13. sz zero-terminated string
  14. (chaine de caracteres terminee par un char zero)
  15. str string object (objet String)
  16. h handle
  17. pt point
  18. rgb rgb triplet
  19. f (fd?) file (fichier)
  20. v void

  1. modificateurs description
  2. u unsigned (non signe)
  3. p pointer (pointeur)
  4. a ou ar array (tableau)
  5. m_ member variable (variable membre)
  6. g_ global variable (variable globale)
  7. s_ static variable (variable statique)
- Symboles (#define) : Les symboles sont écrits 100% en majuscules. Ils suivent les mêmes règles que les variables quant aux caractères utilisables.
exemple:
#define VITESSE_MAX 5
#define PI 3.1415
 
- Modèle de structure (struct) : Les modèles de structures commencent par une majuscule. Pour le reste, ils suivent les mêmes contraintes que les variables.
exemple:
struct  Pixel {
        int iCoordX;
        int iCoordY;
        int iCouleur;
};
 
- Les définitions de type (typedef) : Une définition de type qui ne soit pas un modèle de structure commence par une minuscule et termine par le suffixe '_t' comme pour une variable, tandis qu'une définition de type pour une structure reprendra la forme de cette structure.
exemple:
typedef unsigned int u16;
typedef struct Pixel Pixel
 
- cas particuliers :
    -> variables aux noms composés: On utilise alors les '_' ou les majuscules pour séparer les mots:
            int iVitesse_max; ou int iVitesseMax;
    -> les variables de boucles: Une seule lettre peut-être employée: i, j, k, ...
    -> les variables et fonctions exportées: La variable ou fonction accessible en dehors de son module ou classe de définition sera préfixée du nom du module:
            La fonction issue du module ou de la classe vehicule;
                void vehicule_dessiner_voiture(Modele modModele, int iNiveauDetail, bool bInterieur);

Conventions de documentation

Des conventions strictes de documentation seront proposées dans l'article suivant, afin de gérer des documentations automatique par Doxygen. En attendant, présentons ici des conventions plus génériques:
 
- en-tête de fichier : En début de tous les fichiers sources, créer un cartouche d'en-tête contenant au minimum le nom du fichier, une brève description de son utilité, le numéro de version, la date de dernière mise à jour, le nom du responsable du module ou du développeur.
- prototype et déclaration de fonction : Devant chaque prototype de fonction (.h) ou encore devant chaque corps de fonction (.c), indiquer brièvement l'utilité de la fonction ainsi que les domaines de validité des paramètres.
- dans le code : Commenter chaque partie réalisant une tâche importante. En général, commenter le but du code suffit. Le lecteur devra être capable de retrouver le 'comment' rien que par la lecture du code qui de lui-même doit-être suffisamment claire. Le choix de variables explicites, la structure claire et l'indentation stricte permettent de faciliter cette compréhension. Rappelez-vous: 'un code admirable n'est pas une résolution complexe écrite en peu de lignes, mais une résolution complexe écrite de sorte à être lu sans effort par un novice'.
- Préfixer les commentaires: Dans le cas d'un développement en équipe, il peut être utile de préfixer les commentaires d'un code auteur. Ceci permet de retrouver si besoin l'auteur du code pour en connaître les spécificités.

Conventions d'organisation

- indentation : Le code doit être indenté de manière stricte et claire. Si dans le langage ou le paradigme de programmation plusieurs normes existent, il appartient à l'auteur du code de choisir celle qu'il désire utiliser. En revanche, une fois ce choix fait, la convention devra être suivie tout au long du programme. N'oubliez pas qu'il est possible de jouer sur la taille des tabulations, rendant les retraits plus ou moins profond.
 
exemples corrects:
// Prise en compte des embouteillages (style Y&R)
if (iNb_voiture<3) {
      puts("avancer");
      return 1;
else {
      puts("reculer");
      return 0;
}
// Prise en compte des embouteillages (style GNU)
if (iNb_voiture<3) 
  {
     puts("avancer");
     return 1;
  }
else
  {
    puts("reculer");
    return 0;
  }
exemple incorrect:
 if (iNb_voiture<3) { puts("avancer"); } else { puts("reculer"); }
 
- multiligne et abréviation : Un code ne doit ni être abrégé, ni être inutilement allongé. Les développeurs sont certes réputés pour leur fainéantise, mais celle-ci doit être intelligente. Pour accélérer la lecture, la syntaxe complète doit être utilisée (éviter la syntaxe ternaire) et des retours à la ligne artificiels doivent être insérés (comme vu précédement pour les fonctions à longue liste d'arguments) pour éviter d'avoir à glisser la page pour lire une même ligne.
exemple (extrait du site du zéro):
Si nombreDeTouches vaut 108 et sansFil est un boolée valant Faux, que vaut nombre??
 
nombre = (sansFil || nombreDeTouches >= 108) ? 20 : 30;
nombre = (nombre == 20 && (sansFil && nombreDeTouches >= 108)) ? 40 : 50;
 
- modularité: Autant que faire se peut et suivant le langage ou le paradigme choisi, il convient d'utiliser au mieux les capacités modulaires du langage. Les fonctions utiles à un même traitement général seront regroupées dans un même fichier source. Un source sera donc divisé en plusieurs fichiers correspondants à des modules ou des parties importantes et faisant sens par elle-même. Dans le cas d'une voiture, les modules correspondraient aux sous-sytèmes globaux indépendants: système électrique, moteur, direction, carrosserie, accessoires...
 
- organisation générale : Respecter les normes d'organisation du langage utilisé permet une relecture plus aisée puisque le lecteur saura où trouver dans le code les déclarations et les informations qu'il souhaite.
exemple pour le C avec un module mon_module.c :
1.  tous #include dans l'ordre , "yyy.h", "mon_module.h"
2.  tous les #define.
3.  description des modèles de structures (struct)
4. déclaration de toutes les fonctions restreintes au fichier (static)
5. déclaration de des variables globales restreintes au fichier (static)
6. définition des fonctions exportées
7. définition des fonctions static
le fichier d'en-tête mon_module.h est organisé comme suit:
1. #ifndef MON_MODULE_H
    #define MON_MODULE_H
2. tous les #include , "yyy.h"
3. tous les #define des symboles exportés
4. les typedef des modèles de structure exportées
5. description complète des modèles de structure exportés
6. prototypes des fonctions exportés
7. #endif
 
- décomposition fonctionnelle: Chaque fonction ne doit réaliser qu'une seule tâche ou n'implémenter qu'un unique algorithme, en réponse au problème à résoudre. Cette tâche ne doit cependant pas être trop unitaire (rien ne sert de créer une fonction pour soustraire deux nombres), ni trop globale (une fonction de tri par tas utilisera elle-même la fonction tamiser). Il appartient à l'auteur de trouver l'équilibre permettant une lecture facile du code, sans multiplier toutefois les renvois.
- variables intermédiaires : A l'intérieur d'une fonction ou d'un bloc effectuant des calculs longs, il convient d'utiliser des variables intermédaires, sans pour autant les multipliers artificiellement. En règle générale, on convient que l'usage d'une variable intermédiaire est valable si celle-ci a une signification propre au regard du problème considéré. Dans un langage à la structure stricte comme le C, les variables temporaires sont déclarées en début de bloc. Lorsque c'est possible en revanche, comme en C++, il sera préféré une déclaration à l'endroit d'utilisation. Notons également qu'il est inutile de créer une variable intermédaire pour récupérer une valeur à retourner. Il est possible de retourner directement l'expression:
 
return (expression);
 
exemple correct:
//------
//150 lignes de programmes
while (nb_voiture < 3) {
    int ma_variable;
    /*
     * 150 lignes n'utilisant pas ma_variable
     */
    ma_variable = ...;
}
// 150 lignes n'utilisant plus ma_variable
//------
exemple encore plus correct:
//------
// 150 lignes de programmes
while (nb_voiture < 3) {
    /*
     * 150 lignes n'utilisant pas ma_variable
     */
    int ma_variable;
    ma_variable = ...;
}
// 150 lignes n'utilisant plus ma_variable
//------
exemple incorrect:
//------
// 150 lignes de programmes
int ma_variable;
while (nb_voiture < 3) {  
    /*
     * 150 lignes n'utilisant pas ma_variable
     */
    ma_variable = ...;
}
// 150 lignes n'utilisant plus ma_variable
//------
- structure de contrôle conditionnel / inconditionnel: Il convient de veiller à utiliser les contrôles de condition les mieux adaptés à une situation. En règle générale, l'emploi de l'instruction switch case + break default est préférable à une longue liste de if + else.
- structure de boucle: Lorsque le nombre d'itération d'une boucle est connu ou peut être déterminé comme intervalle entre deux expressions, il est préférable d'utiliser une boucle for. Dans le cas où au contraire le nombre d'itération dépend d'une condition à remplir, la boucle while sera privilégiée.
- omission de renvoi d'une fonction: Dans certains cas, lorsqu'une fonction renvoit un paramètre non utilisé par la suite dans le programme, il est inutile de récupérer cette valeur de retour. En revanche, pour les fonctions critiques renvoyant un indice de réussite (0= succès; -1= échec; 1=erreur de dépassement....), l'étude attentive de ce paramètre de retour permet une résolution et un traitement d'erreur efficace.
exemple incorrect:
int code_retour;
code_retour = executer_ma_fonction();
//code_retour  jamais utilisé
exemple correct:
// avant...
executer ma_fonction();
// après, code_retour jamais utilisé..

Conventions de restrictions

- étiquettes et labels: Dans la plupart des langages, il convient d'exclure les possibilités d'étiquettes et de labels. Ainsi l'instruction goto peut dans 99.99% des cas être évitée par l'utilisation appropriée de break, continue ou return ainsi que de conditions sur une variable booléenne. Le problème dans l'usage des goto est la génération d'un code difficile à relire par la multiplicité des renvois, parfois à des endroits complètement différents où incongrus. L'ordre de l'exécution des fonctions n'est plus l'ordre de lecture et le code résultant devient artificiellement compliqué à relire. On appelle un tel code un code spaghetti -et cela constitue presque une insulte.
- variables globales globales et variables globales restreintes: Une variable globale est accessible et modifiable partout dans le code, même dans les fichiers séparés. Cette pratique n'est pas conseillée voir prohibée sauf si la variable globale est définie comme constante. Une variable globale peut cependant être restreinte au fichier dans lequel elle est déclarée (en C par l'emploi de static).

Deux conseils inclassables

- Couper n'est pas copier: D'une manière générale, la fonction copier pourrait être désactivée des environnements de développement. Dite ainsi, la mesure paraît drastique, mais l'explication est simple. Si vous avez besoin de copier-coller un bout de code d'un endroit à un autre de votre programme, c'est  très souvent que ce morceau mérite d'être transformé en une fonction (ou en méthode en Java). Ainsi, vous n'aurez plus besoin de rallonger votre code mais pourrez faire un appel à la-dite fonction. Pondérez cependant ce fait en considérant qu'une multiplication excessive de fonctions est nuisible à la lecture et la compréhension rapide du code. Comme pour chacune des préconisations de cet article, tout est une question de compromis.
- Supprimer le code mort: Bien souvent dans les contributions, il est donné de voir des parties entières de code mises en commentaires. Ces parties de code étant obsolètes, l'auteur les a commentées sans les supprimer: dommage! Bien plus grave, quand des parties de code mort (jamais exécutées) sont laissées tel quel, même pas commentées. Toute fonction, méthode, classe, structure ou variable n'étant plus utilisé doit être supprimée du code.
 
Votre note : Aucun(e) Moyenne : 4 (4 votes)