Écrire de bons commentaires

Les différents styles de commentaires, et ce qu'il faut éviter

Comment écrire de bons commentaires

Écrire du code est une chose, mais faire en sorte que celui-ci reste compréhensible à tous y compris soi-même, nécessite un peu d’effort. C’est là que le commentaire entre en jeu. Les commentaires sont là pour aider à la compréhension du code. Ils accompagnent le lecteur en lui évitant une fastidieuse lecture de code. Le seul souci, c’est que parfois ces commentaires sont mal écrits. Non seulement, la lecture n’est plus facilité, mais elle devient alors très difficile !

Le but de cet article est de montrer, dans les grandes lignes, comment écrire un bon commentaire. Je me base sur des commentaires en C ou en C++, mais à part quelques parties spécifiques, cette explication pourrait convenir à n’importe quel langage. Pour les besoins de cet article, j’écrirais tout en français. Néanmoins, il est fortement conseillé d’écrire en anglais le code et les commentaires.

Pourquoi écrire un commentaire ?

Avant même d’aller plus loin, voyons l’un des exemples les plus basiques. Si je vous donne ce code, êtes-vous capable de me dire ce qu’il fait du premier coup d’oeil ?

void func(int* var1, int* var2)
{
  *var1 ^= *var2;
  *var2 ^= *var1;
  *var1 ^= *var2;
}

Difficile sans commentaire de comprendre cette fonction. Si j’avais ajouté: Échange la valeur des deux arguments sans utiliser une variable intermédiaire, ça aurait été tout de suite plus compréhensible, non ? (PS: Pour ceux qui ne sont pas débutants et qui avaient reconnu du premier coup d’oeil cette fonction, je réitère mon argument avec ce morceau de code: http://www.ioccc.org/2000/bellard.c)

De plus, ce n’est pas parce que vous comprenez votre code que votre collègue le comprendra. Enfin, ne touchez pas à votre code pendant 6 mois, et vous verrez que ça reviendra à lire le code d’un inconnu.

Quelques exemples typiques de mauvais commentaires

Avant de voir les bonnes pratiques, on va d’abord faire un tour des pires types de commentaires possibles. Tous les exemples que je vais donner ici, sont de vrais exemples auxquels j’ai été confronté. Si vous vous reconnaissez dans l’une de ces pratiques, lisez attentivement la suite !

Bridget Jones (alt.mylife)

La personne qui raconte sa vie dans les commentaires.

/*
Cette fonction permet de calculer la moyenne d'un tableau.
Dans un premier temps, je ne mettais pas la taille, mais après je me suis rendu compte
qu'on ne pouvait pas deviner celle-ci à partir du tableau, et qu'il fallait forcément la spécifier.
Ensuite, j'ai eu plusieurs soucis. Au début, j'oubliais de retourner un double et je retournais un entier.
Donc forcément le résultat était tronqué. Ensuite, j'ai oublié de caster l'un des membres de ma division, ce qui
malgré un retour de type double, me tronquait toujours mon résultat. Heureusement, Bob est venu m'aider, et m'a signalé
mon erreur.
Il faut bien faire attention à ce que le tableau ne soit pas corrompu, et que la taille corresponde bien à ce qu'il y a dans le tableau.
Petite astuce: Si on a un grand tableau mais que l'on veut ne faire que la moyenne des 5 premiers éléments par exemple, il suffit d'indiquer une
taille de 5. Très pratique !
*/
double moyenne(int tab[], int taille)
{
  int res = 0;
  for (int i = 0; i < taille; ++i)
    res += tab[i];

  return (double) res / taille; // Merci Bob
}

Ici, c’est assez évident. Bien que la personne à l’origine du commentaire décrive bien ce que fait sa fonction et comment l’utiliser, le peu d’informations utiles est noyé sous de l’information totalement inutile. Expliquez ses déboires, pourquoi pas, mais pas dans le code. Il y a d’autres vecteurs de communication pour le faire (blog, wiki, mail, etc…)

Le kikoolol

On aurait aussi pu appeler ça le commentaire boulet. Ce type de commentaire est peu utile, souvent bourré de fautes et ne sert strictement à rien:

/* s'est une fonction evidente ke jé fait kan je m'ennuyé, lol :p */
int fonction_l33t_h4Xorr(int t[], int a, int b)
{
  for(int y = -1; y <= 1; y++)
   for (int x = -1; x <= 1; x++)
     if(((x * y) + x + y) == 0)
       t[a][b] = t[a][b] - a;

  return t[a][b] + b; // si on fé 0 + 0, sa fé la téte a toto, MDRRRR !!!!!
}

Un commentaire est là pour aiguiller le lecteur sur ce que fait un bout de code, pas pour être amusant. Un minimum de sérieux est nécessaire. D’une manière générale, évitez les smiley, les lol et tout ce qui n’est pas approprié dans un commentaire.

L’artiste

L’artiste aime bien dessiner dans ses commentaires.

/*
   _     _     _______  ______   ______   ___   _______  ___   _______  __    _
  (c).-.(c)   |   _   ||      | |      | |   | |       ||   | |       ||  |  | |
   / ._. |    |  |_|  ||  _    ||  _    ||   | |_     _||   | |   _   ||   |_| |
 __|( Y )/__  |       || | |   || | |   ||   |   |   |  |   | |  | |  ||       |
(_.-/'-'|-._) |       || |_|   || |_|   ||   |   |   |  |   | |  |_|  ||  _    |
   || A ||    |   _   ||       ||       ||   |   |   |  |   | |       || | |   |
 _.' `-' '._  |__| |__||______| |______| |___|   |___|  |___| |_______||_|  |__|
(.-./`-'|.-.)
 `-'     `-'
 Permet de calculer la somme de a et b.
 */
int addition(int a, int b);

Certes, ça peut être joli, mais ce n’est pas le but d’un commentaire. La finalité reste d’informer le lecteur efficacement. De plus, dans le cas présent, si le nom de la fonction change, beaucoup de temps sera perdu à réécrire le nom de la fonction en ASCII Art.

Captain Obvious

On arrive ici, au type de mauvais commentaire que tout débutant à un jour fait. C’est quasi-inévitable. Un débutant, sait qu’il doit écrire des commentaires, mais s’y prend souvent mal.

// Déclaration de fonction
void jouer()
{
  // Variables
  int nbJoueur = 2; // Nombre de joueur
  int tour = 0; // Numero du tour
  bool arreter = false; // Variable pour savoir si on arrête le jeu

  // Boucle
  while (!arreter) // Tant que arreter est faux
  {
    tour = tour + 1; // On ajoute 1 au nombre de tour
    std::cout << tour << std::endl; // On affiche la valeur de tour
    combattre(); // Lance la fonction de combat
    if (tour > 10) // Si tour est supérieur a 10
       arreter = true; // On met arreter a true
  }

  // Fin de la fonction
}

Ici, aucun commentaire n’est pertinent. Un commentaire doit être là pour expliquer ce qui ne se voit pas au premier abord, pour résumer un comportement ou expliquer un algorithme délicat. Il ne doit jamais paraphraser le code ! Si un commentaire n’apporte pas une valeur supplémentaire à celui-ci, c’est qu’il n’est pas nécessaire. De même, les commentaires indiquant que l’on est dans une boucle ou une fonction sont strictement inutiles. Au final, pour cet exemple, il faudrait ne garder que le code et mettre un seul commentaire au-dessus de la fonction: Fait combattre les deux joueurs pendant 10 tours.

Le pigiste

Parmi les choses à prendre en compte, il y a aussi le placement de commentaire. On évite d’en mettre n’importe où.

double /* retour */ moyenne(int tab[] /* attention à la taille */, int taille /* entier positif */)
{
  /* variable temporaire */ int res = 0; // On utilise une variable temporaire pour stocker
  for (int i = 0; i < taille; ++i)       // la somme des éléments du tableau. On pense ensuite
    res += tab[i];                       // à caster cette variable en double pour ne pas
  return (double) res / taille;          // que le résultat soit tronqué lors de la division.
}

Un commentaire se met généralement au-dessus du nom de fonction. On essaie de faire en sorte que le commentaire soit le moins intrusif possible. On insère un commentaire au milieu du code uniquement si c’est justifié. Ici le commentaire mis sur la droite est très gênant, car si on veut modifier la fonction, il faudra redécaler le commentaire.

Le nécromancien

Du code mort partout, tout simplement.

/*
double moyenne(int tab[])
{
  int res = 0;
  for (int i = 0; tab && tab[i] != 0; ++i)
    res += tab[i];

  return (double) res / taille; // Merci Bob
}
*/

double moyenne(int tab[], int taille)
{
  int res = 0;
  for (int i = 0; i < taille; ++i)
    res += tab[i];

  return (double) res / taille;
}

/*
void ancienneFonction()
{
}
*/

// double moyenne(double tab[], double taille)
// {
//   double res = 0;
//   for (int i = 0; i < taille; ++i)
//     res += tab[i];
//
//   return res / taille;
// }

Si un morceau de code n’a plus vocation à être utilisé, il doit tout simplement être effacé. Pour garder une trace des anciennes versions d’un code, il y a des gestionnaires de version (git, svn, mercurial, …). Il n’y a aucune raison de garder du vieux code en commentaire.

Dave Null

Pas de commentaire du tout. C’est le cas le plus fréquent.

Écrire un commentaire propre

Un commentaire doit être concis, précis et donner des informations qui ne peuvent être déduites immédiatement. On se sert généralement du commentaire, pour résumer le fonctionnement d’un algorithme, expliquer comment utiliser une fonction ou une classe, ou indiquer certaines limitations.

Avant de voir quelques cas pratiques, il est important d’établir un style de commentaire. Il existe plusieurs styles, mais l’un d’entre eux est particulièrement populaire: Doxygen. Le style Doxygen est généralement associé au logiciel du même nom.

Doxygen

Doxygen est avant tout un outil qui est capable d’analyser du code, et de générer une documentation à partir de celui-ci. Si on respecte la syntaxe Doxygen dans les commentaires, celui-ci en prendra compte lors de la création de cette documentation. Doxygen utilise des marqueurs spéciaux. Il en existe beaucoup, mais je ne décrirais que les marqueurs les plus usités:

  • @brief: On écrit l’explication dans cette partie. Tout commentaire, par défaut, est considéré comme @brief. On ne le met donc pas.
  • @param: Permet de décrire un argument.
  • @return: Permet de décrire la sortie de la fonction.
  • @class: Indique le nom de la classe, tout simplement.
  • @code @endcode: Lorsqu’il est nécessaire de faire un exemple, on utilise ces balises.

Pour différencier un commentaire Doxygen d’un commentaire normal, on ajoute un “!”.

Exemple:

/*!
** Un magnifique commentaire Doxygen !
**
** Plein d'explications...
*/

C’est cette syntaxe que j’adopterais pour le reste de l’article. Si générer de la documentation à partir du code vous intéresse, je vous invite à regarder la documentation de Doxygen sur internet. Il y a beaucoup de tutoriaux très bien fait.

Passons maintenant au vif du sujet, comment commenter proprement.

Fonctions

Pour commenter une fonction, on indique ce qu’elle fait. On décrit les arguments qu’elle prend en expliquant si nécessaire les contraintes. Le type de retour doit être expliqué aussi.

Prenons un exemple:

/*!
** Fusionne une liste d'entiers et une liste de décimaux,
** sans garder de doublons.
**
** @param list1 Une liste d'entiers
** @param list2 Une liste de décimaux
** @return La fusion des deux listes
*/
std::list<double>
fusionneListe(const std::list<int>& list1, const std::list<double>& list2)
{
  std::list<double> res;
  for (std::list<int>::const_iterator iter it = list1.begin(); it != list1.end(); ++it)
    res.push_back(*it);
  for (std::list<int>::const_iterator iter it = list2.begin(); it != list2.end(); ++it)
    res.push_back(*it);
  res.unique();

  return res;
}

Header ou fichier source ?

C’est une question qui est souvent sujette à discussion. Faut-il commenter dans le header ou dans les sources ? En théorie, il faudrait commenter les deux, mais de manière différente. Dans le header, on devrait avoir une explication générale, et dans les sources, une explication plus technique.

header (commentaire descriptif):

/*!
** Détermine si un nombre est premier.
**
** @param number Un nombre strictement positif.
** @return Si le nombre est premier
*/
bool estPremier(unsigned int number);

source (commentaire technique):

/*
** 0 et 1 ne sont pas premiers, on peut donc écarter ces cas.
** Notre borne supérieur se détermine facilement, car un nombre au carré supérieur à
** celui sur lequel on travaille, ne peut être un multiple de celui-ci.
** On vérifie ensuite que parmi la plage de nombres restants, aucun ne peut diviser
** notre nombre.
*/
bool estPremier(unsigned int number)
{
  if (number <= 1)
    return false;
  for (unsigned int i = 2; i * i <= number; ++i)
    if (number % i == 0)
      return false;
  return true;
}

C’est évidemment un choix personnel. Certains préfèrent ne commenter que les fichiers sources en utilisant le commentaire descriptif et ne commentent pas les déclarations de méthodes ou de fonctions dans l’en-tête. Il arrive souvent que l’on commente dans un header, uniquement la classe, afin que l’en-tête ne soit pas trop surchargé. On voit alors rapidement quelles sont les fonctions ou méthodes disponibles. Sur ce point, c’est une question de goût, l’important étant de documenter au moins l’un des deux fichiers.

Classes

Le commentaire de classe est très facile. Il n’y a que le nom de la classe et l’explication à indiquer. On découpe généralement en deux paragraphes. Le premier décrit l’utilité de la classe, tandis que le deuxième décrit les comportements dont l’utilisateur doit être informé. S’il n’y a pas de comportement particulier, il n’y a qu’un paragraphe.

/*!
** @class Database
**
** Permet de gérer les interactions avec
** une base de données.
**
** La destruction de l'objet, provoque la
** fermeture automatique de la connexion.
** Lors d'une nouvelle connexion, l'ancienne
** est automatiquement fermée.
** Lors de la fermeture de connexion, toutes les
** transactions en cours sont arrêtées, et les
** requêtes envoyées.
*/
class Database
{
  public:
    Database();
    ~Database();

    void connect(const std::string& filename);
    void close();
    void createDatabase();
    void dumpAll();
    void dump(const std::string& tableName);
    void beginTransaction();
    void endTransaction();
};

Cas d’un code nécessitant un exemple

Parfois, l’explication ne suffit pas, et avoir un exemple peut aider à la compréhension. C’est là que les balises @code et @endcode entrent en jeu.

/*!
** @class SyncAction
**
** Cette classe permet de synchroniser des actions en faisant en sorte
** qu'une méthode choisie par l'utilisateur soit appelée uniquement quand
** toutes les méthodes lancées en parallèle soient terminées.
**
** @code
** void asyncFunction(SyncAction::Ptr cb_ptr)
** {
**   // Appelé pour chaque méthode.
**   cb_ptr = 0;
** }
**
** void methodeDeFin()
** {
**   // Appelé quand toutes les méthodes sont terminées.
** }
**
** SyncAction::Ptr cb_ptr = new SyncAction(boost::bind(methodeDeFin, ...));
** while (...)
** {
**   asyncFunction(cb_ptr);
** }
** cb_ptr = 0;
** @endcode
*/
class SyncAction
{
public:
  SyncAction(const boost::function<void ()>& callback);
  ~SyncAction();
  typedef boost::intrusive_ptr<SyncAction> Ptr;

protected:
  boost::function<void ()> _callback;
};

Quand écrire son commentaire

On sait comment écrire un commentaire, mais on ne sait pas encore quand. Faut-il écrire son commentaire avant de coder, pendant ou après ?

En théorie, il faudrait écrire ses prototypes de fonctions, de méthodes et de classes. Puis écrire les commentaires associés, et enfin écrire les algorithmes. Le commentaire est là pour aiguiller lors du développement. De plus, si on ne commente pas au fur et à mesure, on devra alors passer beaucoup de temps à commenter tout ce que l’on a oublié. Cette tâche étant rébarbative, elle est souvent abandonnée, et on se retrouve alors avec un code non commenté.

En réalité, ce n’est pas toujours possible. Parfois, on se rend compte qu’il y a une meilleure manière de faire, ou le cahier des charges a été subitement modifié, ce qui change totalement le prototype de nombreuses fonctions. On est alors obligé de réécrire la plupart des commentaires. C’est pourquoi certains préfèrent écrire leurs commentaires ultérieurement.

Controverse

Un courant de pensée très sérieux, prône de ne pas commenter son code du tout ! Même si je ne partage pas cette opinion, les arguments avancés sont constructifs.

En effet, les défenseurs de cette méthode avancent que lorsque l’on écrit un commentaire, c’est du travail supplémentaire. D’une part parce qu’il faut écrire le commentaire, d’autre part parce que si le code change, le commentaire doit être mis à jour aussi. C’est donc du double travail. De plus, si on change le code, mais qu’on oublie de modifier le commentaire associé, alors celui-ci devient trompeur !

Leur argument majeur est qu’un code bien écrit est compréhensible et ne nécessite pas de commentaire. Si un code nécessite un commentaire, c’est qu’il est mal écrit.

Une méthode réaliste

Au final, faut-il commenter ? Je pense que oui, mais avec intelligence.

Voici la méthode que je conseillerais pour écrire des commentaires efficacement.

  • Penser au sujet, et établir les besoins.
  • Architecturer le code, c’est-à-dire réfléchir à tous les modules nécessaires et leurs interactions.
  • Écrire les commentaires uniquement sur les fonctions qui ne sont pas simples à comprendre. Par exemple, il n’est pas nécessaire de documenter les getters et les setters. Le but est de ne commenter que ce qui nécessite vraiment une explication.
  • Avant de commenter un code, voir si on ne peut le réécrire d’une meilleure manière, de façon à ce qu’un commentaire ne soit plus utile.
  • Toujours essayer d’écrire le commentaire avant le code. Ce n’est bien évidemment pas toujours possible, mais c’est une bonne habitude à prendre. Une bonne solution est d’écrire le commentaire avant, puis le code et enfin de revenir compléter ou corriger le commentaire s’il ne convient plus.
  • Enfin, ne pas hésiter à faire relire ses commentaires par d’autres développeurs. S’ils ont du mal à cerner un code commenté, alors soit le code est mal pensé, soit le commentaire est mal écrit. Il existe un fossé entre ce que l’on croit communiquer et ce que les autres perçoivent, il n’y a pas de mal à se tromper. Dans ce cas, réécrire le code, le commentaire ou les deux.

Conclusion

J’espère vous avoir donné les bases sur la manière de bien commenter son code. Si ce sujet vous intéresse, il existe de nombreux ouvrages qui approfondissent ce sujet et qui sont aisément trouvable sur internet.

Commentez son code, c’est comme nettoyer sa salle de bain. Personne n’aime le faire, mais au final, c’est un vrai confort pour tout le monde.