4869433260_5f8a080519_z.jpg
< < Articles

Bien commenter son projet web

-/-/-

On parle de quoi ?

On parle des bons et des mauvais commentaires ou, plus simplement, de comment utiliser les commentaires à bon escient au sein de projets collaboratifs.

On parle aussi du concept node-machine ou d’un concept de librairie contenant, au sein même de son code et sans utilisation des commentaires, tous les éléments nécessaires à la création d’une documentation complète.

Des bons et des mauvais commentaires

Bon y’a le bon commentaire, il est là, il commente le code et c’est un bon commentaire … et y'a le mauvais commentaire, il est là, il commente le code mais bon c’est un mauvais commentaire quoi …

Plus concrètement il est difficile de qualifier un commentaires de bon ou de mauvais sans prendre en considération le contexte d’utilisation. C’est pourquoi la liste suivante – exemples de bons et de mauvais commentaires – n’est pas exhaustive et peut être sujet à discussion.

Les mauvais commentaires

Commentaires de “meublage”

public void loadProperties() {
    try {
        String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
        FileInputStream propertiesStream = new FileInputStream(propertiesPath);
        loadedProperties.load(propertiesStream);
    } catch (IOException e) {
        // No properties files means all defaults are loaded
    }
}

Rôle escompté : Je ne sais pas quoi mettre dans cette portion de code mais je me dis que peut-être un jour ce sera utile.

Toutefois le rôle d’un commentaire n’est pas de remplacer une logique bancale ou un trou algorithmique. Souvent, il s’agit d’une partie à développer (TODO), à corriger (FIXME) ou qui ne devrait simplement pas exister.

Commentaires redondants

/**
 * The Manager implementation with which this Container is
 * associated.
 */
protected Manager manager = null;
/**
 * The parent Container to which this Container is a child.
 */
protected Container parent = null;
/** The day of the month. */
private int dayOfMonth;
/**
 * Returns the day of the month.
 *
 * @return the day of the month.
 */
public int getDayOfMonth() {
    return dayOfMonth;
}

Rôle escompté : Permet d’expliquer dans un commentaire ce que représente telle variable ou méthode.

Toutefois il n’existe aucun intérêt à répéter ce qu’un code explicite dit déjà. La documentation type Javadoc reste un exception à discuter.

Commentaires de fermeture

public class wc {
    public static void main(String[]args) {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String line;
        int lineCount = 0;
        int charCount = 0;
        int wordCount = 0;
        try {
            while ((line = in.readLine()) != null) {
                lineCount++;
                charCount += line.length();
                String words[] = line.split("\\W");
                wordCount += words.length;
            } //while
            System.out.println("wordCount = " + wordCount);
            System.out.println("lineCount = " + lineCount);
            System.out.println("charCount = " + charCount);
        } // try
        catch (IOException e) {
            System.err.println("Error:" + e.getMessage());
        } //catch
    } //main
}

Rôle escompté : Permet de mieux se repérer dans un code trop gros (ici, pour faciliter la lecture, l’exemple est léger).

Toutefois, c’est plutôt révélateur d’une mauvaise conception et pourrait être résolu avec un meilleur découpage en plusieurs fonctionnalités. Il est alors normalement suffisant de s’aider de l’indentation.

Commentaires de journal

/*
 * Changes (from 11-Oct-2001)
 * --------------------------
 * 11-Oct-2001 : Re-organised the class and moved it to new package com.jrefinery.date (DG);
 * 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate class (DG);
 * 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate class is gone (DG); Changed getPreviousDayOfWeek(), getFollowingDayOfWeek() and getNearestDayOfWeek() to correct bugs (DG);
 * 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG);
 * 29-May-2002 : Moved the month constants into a separate interface (MonthConstants) (DG);
 * 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
 * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
 * 13-Mar-2003 : Implemented Serializable (DG);
 * 29-May-2003 : Fixed bug in addMonths method (DG);
 * 04-Sep-2003 : Implemented Comparable. Updated the isInRange javadocs (DG);
 * 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG);
*/

Rôle escompté : Permet d’avoir un suivi des modifications réalisées dans le code (parfois le journal est intégré devant les déclarations de méthodes ou variables).

Toutefois, ce n’est pas le rôle des commentaires que de servir de système de versionning. Des outils dédiés comme SVN ou GIT remplissent très bien ce rôle. Si le but est de créer un changelog, il devrait figurer dans un fichier indépendant type CHANGELOG.md (et mieux être généré automatiquement).

Commentaires d’attribution

/* Added by Rick */

Rôle escompté : Identifier la personne responsable de la fonctionnalité ou de la création du fichier.

Toutefois, le commentaire n’a aucune pérennité sur un projet de plusieurs années (changement de l’équipe) et lorsque le but est en plus d’identifier un responsable, c’est symptomatique d’un manque de partage de connaissances au sein du projet.

Commentaires “artistique”

/*
                              ,_-=(!7(7/zs_.
                             .='  ' .`/,/!(=)Zm.
               .._,,._..  ,-`- `,\ ` -` -`\\7//WW.
          ,v=~/.-,-\- -!|V-s.)iT-|s|\-.'   `///mK%.
        v!`i!-.e]-g`bT/i(/[=.Z/m)K(YNYi..   /-]i44M.
      v`/,`|v]-DvLcfZ/eV/iDLN\D/ZK@%8W[Z..   `/d!Z8m
     //,c\(2(X/NYNY8]ZZ/bZd\()/\7WY%WKKW)   -'|(][%4.
   ,\\i\c(e)WX@WKKZKDKWMZ8(b5/ZK8]Z7%ffVM,   -.Y!bNMi
   /-iit5N)KWG%%8%%%%W8%ZWM(8YZvD)XN(@.  [   \]!/GXW[
  / ))G8\NMN%W%%%%%%%%%%8KK@WZKYK*ZG5KMi,-   vi[NZGM[
 i\!(44Y8K%8%%%**~YZYZ@%%%%%4KWZ/PKN)ZDZ7   c=//WZK%!
,\v\YtMZW8W%%f`,`.t/bNZZK%%W%%ZXb*K(K5DZ   -c\\/KM48
-|c5PbM4DDW%f  v./c\[tMY8W%PMW%D@KW)Gbf   -/(=ZZKM8[
2(N8YXWK85@K   -'c|K4/KKK%@  V%@@WD8e~  .//ct)8ZK%8`
=)b%]Nd)@KM[  !'\cG!iWYK%%|   !M@KZf    -c\))ZDKW%`
YYKWZGNM4/Pb  '-VscP4]b@W%     'Mf`   -L\///KM(%W!
!KKW4ZK/W7)Z. '/cttbY)DKW%     -`  .',\v)K(5KW%%f
'W)KWKZZg)Z2/,!/L(-DYYb54%  ,,`, -\-/v(((KK5WW%f
 \M4NDDKZZ(e!/\7vNTtZd)8\Mi!\-,-/i-v((tKNGN%W%%
 'M8M88(Zd))///((|D\tDY\\KK-`/-i(=)KtNNN@W%%%@%[
  !8%@KW5KKN4///s(\Pd!ROBY8/=2(/4ZdzKD%K%%%M8@%%
   '%%%W%dGNtPK(c\/2\[Z(ttNYZ2NZW8W8K%%%%YKM%M%%.
     *%%W%GW5@/%!e]_tZdY()v)ZXMZW%W%%%*5Y]K%ZK%8[
      '*%%%%8%8WK\)[/ZmZ/Zi]!/M%%%%@f\ \Y/NNMK%%!
        'VM%%%%W%WN5Z/Gt5/b)((cV@f`  - |cZbMKW%%|
           'V*M%%%WZ/ZG\t5((+)L'-,,/  -)X(NWW%%
                `~`MZ/DZGNZG5(((\,    ,t\\Z)KW%@
                   'M8K%8GN8\5(5///]i!v\K)85W%%f
                     YWWKKKKWZ8G54X/GGMeK@WM8%@
                      !M8%8%48WG@KWYbW%WWW%%%@
                        VM%WKWK%8K%%8WWWW%%%@`
                          ~*%%%%%%W%%%%%%%@~
                             ~*MM%%%%%%@f`
                                 '''''
*/

Rôle escompté : Une envie de se faire plaisir, de placer un genre d’easter egg.

Sans commentaire.

le code mort …

InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));

Rôle escompté : Garder visible l’existant pour facilement revenir en arrière.

Toutefois, le commentaire ne doit pas cacher les hésitations du développeur ou servir de système de versionning rapide. Il est souvent important de comprendre l’existant pour mieux transformer la fonctionnalité.

Les commentaire neutres

Commentaires légaux

// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.

Rôle escompté : Affichage de certaines raisons légales (licences notamment) obligatoires.

On préférera toutefois l’utilisation (si possible) d’un fichier externe présentant la licence.

Commentaires TODO/FIXME

// TODO-MdM these are not needed
// We expect this to go away when we do the checkout model
protected VersionInfo makeVersion()throws Exception{
    return null;
}

Rôle escompté : Identifier des zones de code à terminer (développement en cours) ou à revoir ultérieurement (améliorations à faire).

On associera souvent un traceur des tags TODO et FIXME pour ne pas en oublier la résolution.

Les bons commentaires

Commentaires de clarification

public void testCompareTo()throws Exception{
    WikiPagePath a = PathParser.parse("PageA");
    WikiPagePath ab = PathParser.parse("PageA.PageB");
    WikiPagePath b = PathParser.parse("PageB");
    WikiPagePath aa = PathParser.parse("PageA.PageA");
    WikiPagePath bb = PathParser.parse("PageB.PageB");
    WikiPagePath ba = PathParser.parse("PageB.PageA");
    assertTrue(a.compareTo(a) == 0); // a == a
    assertTrue(a.compareTo(b) != 0); // a != b
    assertTrue(ab.compareTo(ab) == 0); // ab == ab
    assertTrue(a.compareTo(b) == -1); // a &lt; b
    assertTrue(aa.compareTo(ab) == -1); // aa &lt; ab
    assertTrue(ba.compareTo(bb) == -1); // ba &lt; bb 
    assertTrue(b.compareTo(a) == 1); // b &gt; a
    assertTrue(ab.compareTo(aa) == 1); // ab &gt; aa
    assertTrue(bb.compareTo(ba) == 1); // bb &gt; ba
}

Rôle escompté : Facilite la lecture et la compréhension du code dans le cas d’une syntaxe verbeuse.

La difficulté ici est que le commentaire sera jugé bon ou mauvais en fonction de s’il est suffisamment lisible naturellement pour les participants du projet (possédant parfois différents profils et niveaux). A vous de placer/connaître la position du curseur acceptable.

Commentaires d’avertissement/amplification

public static SimpleDateFormat makeStandardHttpDateFormat() {
    //SimpleDateFormat is not thread safe,
    //so we need to create each instance independently.
    SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
    df.setTimeZone(TimeZone.getTimeZone("GMT"));
    return df;
}

String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting
// spaces that could cause the item to be recognized
// as another list.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));

Rôle escompté : Met l’accent sur un comportement peu facile à comprendre.

Il s’agit ici de faire ressortir certains comportements liés au choix de l’algorithme, du modèle de données ou encore du langage qui peuvent être difficiles à cerner à la première lecture.

Et si on faisait autrement ?

Et si on pouvait se passer de commentaires ?

Et si le code parlait de lui-même ?

Et si on avant des exemples d’implémentation directement dans le code ?

Dans le monde Javascript, avec NodeJS, certains proposent une solution : La node machine !

Qu’est-ce qu’une node machine ?

The machine specification is an open standard for Javascript functions. Each machine has a single, clear purpose—whether it be sending an email, translating a text file, or fetching a web page. But whereas traditional functions are supposed to have only a single result, machines are aware of multiple possible outcomes. Machines are self-documenting, quick to implement, and simple to debug." Mike McNeil

La spécification des machines est un standard ouvert pour les fonctions JavaScript. Chaque machine a un unique et clair but—que ce soit l’envoi de mail, la traduction d’un fichier texte ou le parcours d’un site web. Mais là où les fonctions traditionnelles sont sensées avoir un seul résultat, les machines ont la possibilité d’avoir plusieurs sorties. Les Machines sont auto-documentées, rapide à améliorer et simple à debogguer." Mike McNeil

Une machine Node c’est donc :

  • Une librairie auto-documentée
  • Une librairie maîtrisant ses “sorties”
  • Une librairie au format universel

Une Node machine répond donc à ces besoins lorsque l’on souhaite utiliser une librairie :

  • Comment être sûr que la documentation est à jour ?
  • Comment être sûr de l’utilisation si je n’ai pas accès aux sources ?
  • Quels sont les paramètres de cette méthode ?
  • Quel est le genre de retours proposés par cette méthode ?

Solution Node-Machine

Classic usage paradigme

Voici un exemple classique d’utilisation d’une méthode fournie par une librairie externe :

var library = require('theLib');
var output = library.myFunction(paramA, paramB);
if (output) {
    resultCallback(output);
} else {
    faultCallback(output);
}

En découle certaines questions :

  • La méthode myFunction existe-t-elle vraiment ?
  • Quels sont les paramètres qu’elle utilise ? Combien de paramètres utilise-t-elle ?
  • Quelle est la forme du retour positif/négatif ?

Les paramètres

Afin de répondre aux problèmes de l’utilisation des paramètres propres à une méthode, nous pouvons :

  • Les regrouper et en faire une structure exploitable par notre méthode
  • Ajouter du contenu – comme le caractère obligatoire du paramètre, une description voire un exemple de valeur…

Cette structure permet ensuite de générer directement une documentation complète sur la librairie, voire de répondre directement à un appel console (affichage des paramètres et d’un exemple complet d’appel).

inputs : {
    appId : {
        example : '215798311808508',
        description : 'The unique identifier for your Facebook app [...]',
        required : true
    },
    appSecret : {
        example : 'dsg4901g0123456',
        description : 'The developer "secret" for your Facebook app [...]',
        required : true
    },
    code : {
        example : 'AQDvCav5zRSafS795TckAerUV53xzgqRyrcfYX2i_PJFObCvACVRP-[...]',
        description : 'The OAuth `code` generated by Facebook and sent to [...]',
        required : true
    },
    callbackUrl : {
        example : 'http://localhost:1337/user/facebook/login',
        description : 'The redirect URL that was used when generating [...]',
        required : true
    }
}

Les callbacks

Pour répondre au besoin de connaître le comportement de retours des méthodes de la librairie, nous pouvons :

  • Les intégrer dans la librairie
  • Les décrire dans la libraire

Il sera alors plus facile de manipuler et d’anticiper les différents retours possibles prévus par la librairie sur une méthode donnée, voir d’avoir un exemple concret de données retournées.

exits : {
    error : {
        description : 'Triggered when the Facebook API returns an error [...]'
    },
    success : {
        description : 'Returns the access token itself, as well as the [...]',
        example : {
            token : 'CA2Emk9XsJUIBAHB9sTF5rOdNmAXTDjiHxZaZC1GYtFZCcdYGVnLYZB[...]',
            expires : '2014-11-20T20:34:26.632Z'
        }
    }
}

La méthode

Finalement, sur le besoin de définition d’une méthode de la librairie, il suffit de la considérer comme :

  • Configurable via les inputs
  • Définie par ses exits

En pratique une méthode c’est donc :

fn : function (inputs, exits) {
    var doJSONRequest = require('../lib/do-request');
    // GET projects/ and send the api token as a header
    doJSONRequest({
        method : 'get',
        url : '/oauth/access_token',
        data : {
            'redirect_uri' : inputs.callbackUrl,
            'client_id' : inputs.appId,
            'client_secret' : inputs.appSecret,
            'code' : inputs.code,
        },
        headers : {}
    }, function (err, responseBody) {
        if (err) {
            return exits.error(err);
        }
        // Parse Facebook Access Token from request Body
        var token;
        try {
            return exits.success({
                token : responseBody.match(/access_token=([a-z0-9]+)[^a-z0-9]{0,}/i)[1],
                expires : (function getExpirationDateAsISOString() {
                    var now = new Date();
                    var secondsFromNowToExpiry =  + (responseBody.match(/expires=([0-9]+)[^0-9]{0,}/i)[1]);
                    var expirationDate = new Date((now.getTime() + (secondsFromNowToExpiry * 1000)));
                    return expirationDate.toJSON();
                })()
            });
        }
        [...]

La machine et les packs de machines

Il suffira ensuite d’englober nos différentes méthodes au sein d’une librairie transformée en Node machine, en y ajoutant du contenu propre à la machine, voire aux machines packs (regroupement de plusieurs machines).

Par l’exemple pour une machine :

module.exports = {
    friendlyName : 'Do something',
    description : 'Do something with the provided inputs that results in one of the exit scenarios.',
    extendedDescription : 'This optional extended description can be used to communicate caveats, technical notes, or any other sort of additional information which might be helpful for users of this machine.',
    moreInfoUrl : 'https://stripe.com/docs/api#list_cards',
    cacheable : true,
    sync : true,
    inputs : {
        apiKey : {
            description : 'The api key to be used.',
            required : true,
            example : 'foo',
            whereToGet : {
                url : 'https://dashboard.stripe.com/account/apikeys',
                description : 'Copy either "Test Secret Key" or "Live Secret Key" from your Stripe dashboard.',
                extendedDescription : 'You will first need to log in to your Stripe account, or create one if you have not already done so.'
            }
        }
    },
    defaultExit : 'success',
    exits : {
        success : {
            example : 'myApiKey',
            description : 'Returns the API key for your totally fake account',
            variableName : 'rateLimitMetadata'
        },
        error : {
            description : 'Unexpected error occurred.'
        }
    },
    fn : function (inputs, exits) {
        // ...
        // your code here
        var result = 'foo';
        // ...
        // ...and when you're done:
        return exits.success(result);
    };
}

Par l’exemple pour une machinepack :

{
    "name" : "machinepack-foobar",
    "description" : "Interact with the foobar API; assign foos, list bars, etc.",
    "version" : "0.0.1",
    "keywords" : ["machinepack", "machines"],
    "dependencies" : {
        'machine' : '^1.2.0'
    },
    "machinepack" : {
        "friendlyName" : "Foobar",
        "machines" : [
            "assign-foos",
            "list-bars",
            "add-something-to-whatever",
            "do-something-else",
            "list-other-things"
        ],
        "machineDir" : "machines/"
    }
}

Documentation générée

Le but est bien sûr de pouvoir non seulement utiliser facilement la librairie, mais aussi de générer une documentation exploitable par tout le monde. Par rapport à la structure même de la librairie, il est alors facile d’automatiser la génération de la documentation (en utilisant une machine Node par exemple…).

Ici, des exemples de la documentation générée pour le machine Facebook et sa méthode getAccessToken() sur le site de node machine.

[Documentation Node Machine][1]

Utilisation

Pour utiliser une machine dans l’environnement Node, rien de plus simple :

  • Utilisation directe :

    require('machinepack-facebook') .getAccessToken({ appId : '215798311808508', appSecret : 'dsg4901g0123456', code : '[...]' }) .exec(console.log);

  • Utilisation avec callbacks :

    require('machinepack-facebook') .getAccessToken({ appId : '215798311808508', appSecret : 'dsg4901g0123456', code : '[...]' }) .exec( success : function (token) { /.../ }, error : function (err) { /.../ }, );

  • Utilisation avec configuration préalable des inputs :

    var getFacebookAccessToken = require('machinepack-facebook').getAccessToken(); getFacebookAccessToken.configure({ appId : '215798311808508' }); getFacebookAccessToken.configure({ appSecret : 'dsg4901g0123456' }); getFacebookAccessToken.configure({ code : '[...]' }); getFacebookAccessToken.exec( success : function (token) { /.../ }, error : function (err) { /.../ });

Les spécifications

Toutes les spécifications, ainsi que la liste des librairies aujourd’hui disponible en tant que node machine, sont recensés sur le site node machine by Mike McNeil.

[Qu'est-ce qu'une node machine][2]

[Spécifications d'une node machine][3]

http://youtu.be/72jI0dQx7pw

Conclusion

Il est aujourd’hui possible d’imaginer d’autres façons d’exploiter les commentaires ou de remplacer leurs utilisations par une description plus précise des comportements d’une libraire.

Il faudra bien entendu statuer sur la séparation entre le contenu et la documentation : le concept de Node machine a décidé de merger les deux efficacement pour faciliter la vie de tous les développeurs.

À vous d’imaginer d’autres possibilités et d’en partager les limites ;)

Références :

  • Image "à la une" de Quinn Dombrowski distribué sur Flickr sous licence Creative Commons CC BY-SA
  • Screenshots à partir du site http://node-machine.org
  • Exemples commentaires à partir de l’excellent livre Clean Code: A Handbook of Agile Software Craftsmanship de Robert Cecil Martin (ISBN : 978-0132350884)

    [1]: //images.ctfassets.net/95wnqgvmhlea/2chcTltMvS0g4IWSesoioi/b810213b68d285b36a0881a8b3af69db/node-machine_doc-.png?fm=png

    [2]: //images.ctfassets.net/95wnqgvmhlea/4PBSUWHTcQ4EE8g04Ekmiq/52d962df13caae005c2a2a0ecfe407b7/node-machine_what-.png?fm=png

    [3]: //images.ctfassets.net/95wnqgvmhlea/5OLKewIh8c2sMMKWgcGQoA/8241d9bc89622bf874a920a1a74919a8/node-machine_spec-.png?fm=png