Pourquoi chercher à ne pas réinventer la roue ?

Bon, je vais sembler en contradiction avec un post précédent dans lequel j’indiquais qu’un bon développeur devait être une feignasse et ne pas réinventer la roue [cf. Le CRUD]; en effet, ce n’est pas la même chose de ne pas réinventer la roue et de chercher à ne pas la réinventer.

La contradiction n’est, bien sûr, qu’apparente; bien sûr, d’abord, parce que je suis quelqu’un de consistant [au sens topologique du terme], et ensuite parce que, comme vous l’allez voir, c’est assez évident. Voici un exemple réel auquel j’ai été confronté sur un projet réel. J’ai pris soin de changer l’ensemble des noms de variables ou de fonctions qui pourraient permettre de retrouver le nom du projet au du développeur qui était, certes une feignasse, mais une mauvaise feignasse.

Connais ton ennemi

Lorsque Sun Tzu le dit, dans son « art de la guerre », il souhaite indiquer qu’une bataille se gagne plus facilement lorsqu’on connaît correctement son ennemi. Considérons une application en Java dont une des opérations consiste à chercher l’ensemble des données d’une table dans MySQL et d’en fabriquer un fichier plat CSV (fichier texte dont chaque ligne correspond à une ligne de la table, et dont les champs sont séparés par des « ; »).

La mauvaise feignasse

La mauvaise feignasse va rapidement entrer dans son code et écrire quelque chose de ce genre :

PreparedStatement ps = jeGenereUnStatementDeMaRequete(query);
ResultSet resultset = preparedstatement.executeQuery();
BufferedWriter out = new BufferedWriter(new FileWriter(csvFile));
while(resultset.next()) {
    String line = fonctionQuiGenereLaLigneDeResultat(resultset);
    out.write(line);
}
resultset.close();
out.flush();
out.close();

Je ne parle pas ici de la gestion des exceptions, ni du contenu des fonctions de génération du PreparedStatement ou de la génération de la ligne de résultat. Indiquons simplement que la requête exécutée est un bête « SELECT * FROM my_table », éventuellement agrémenté d’un ORDER BY.

Le code fonctionne, mais il pose plusieurs problèmes :

  • la gestion de la mémoire (le résultat est intégralement chargé, puis modifié ligne par ligne et écrit enfin ligne par ligne);
  • la performance (encore une fois, le résultat est intégralement chargé une fois – en mémoire – puis écrit dans un fichier);
  • le risque (tout traitement sur des données équivaut à un risque d’erreur; ici, le risque est minime, mais il existe, de commettre une erreur dans la fonction générant la ligne de résultat sous forme CSV – ceci dit, je n’en tiendrai pas compte sur la suite de ce post).

Le développeur a résolu le problème, certes; de façon efficace, certes; il s’avère que beaucoup de développeurs se contentent sans problème de ce genre de code (et cela ne pose d’ailleurs pas de problème majeur). Mais la différence entre le développeur et l’architecte est que l’architecte, qui a une vue d’ensemble, se doit d’être une vraie bonne feignasse.

La vraie bonne feignasse

La vraie bonne feignasse préfère se fatiguer une fois à chercher une solution pratique, une roue, à un problème posé classiquement, et part souvent du principe qu’une telle solution existe déjà. En effet, un export d’une table sous forme de fichier CSV, c’est un problème qui semble très classique (notamment de par la lecture que propose nativement des logiciels tels que Ms Excel et Apple Numbers desdits fichiers).

Il s’avère que la plupart des systèmes de bases de données proposent nativement des fonctions permettant ce genre d’export; ainsi, l’exécution de la requête suivante, sous MySQL :

SELECT
    champ_1, champ_2,...champ_n
INTO OUTFILE
    'cheminCompletDeMonCsvFile'
FIELDS TERMINATED BY
    ';'
LINES TERMINATED BY
    '\r\n'
FROM
    my_table;

permet de réaliser ce que nous demandons, avec les avantages suivants :

  • gestion de la mémoire (le SGBD va écrire le fichier au fur et à mesure de sa lecture des résultats, évitant ainsi le risque de surcharger la mémoire avec des millions, parfois des centaines de millions, de lignes de résultat sans aucune raison valable);
  • gestion de la performance (encore une fois, le SGBD va écrire le fichier au fur et à mesure de sa lecture);
  • transfert de risque : ça, c’est un vrai truc de bonne feignasse – vous transférez le risque de votre code sur du code existant auparavant, testé et éprouvé depuis longtemps.

Conclusion

J’ai conscience que l’exemple cité dans ce post n’est pas particulièrement spectaculaire; mais en l’occurrence, l’exemple que j’avais rencontré provoquait de nombreux problèmes de mémoire (notamment par l’utilisation, dont je vous ai fait grâce car il s’agissait là, selon moi, d’une vraie erreur de programmation, de Vector dans lequel l’intégralité du résultat était d’abord chargé; sur une simple table d’un million de lignes, cela provoquait un exception); ce qu’il faut retenir est que vous devez consolider une vraie culture générale de programmation, de méthodologie, de nomenclature, d’usages, de design patterns, etc.

Le jour où j’avais rencontré cet exemple, je ne connaissais moi-même pas cette fonction sous MySQL, et je l’ai cherchée partant du principe qu’elle existait, ce qui m’a permis de découvrir également que le même type de fonction (LOAD FROM INFILE) existe pour le chargement d’un fichier CSV en base de données. Et ça, ça m’a rappelé que je n’ai pas toujours été une bonne feignasse (j’utilisai alors des scripts pour transformer mes fichiers CSV en INSERT sql).

Aussi, je vous invite, face aux problèmes apparemment classiques, à d’abord chercher des solutions classiques, et à en faire part au plus grand nombre.

5 réponses à to “Pourquoi chercher à ne pas réinventer la roue ?”

  • GreG:

    La façon dont l’API JDBC est faite permet la récupération des données (fetch) sur ResultSet.next() et pas sur PreparedStatement.executeQuery(), donc dire que l’on charge toute les données en mémoire me semble un peu rapide.
    Je ne doute pas qu’il y ait de mauvais drivers JDBC mal écrits, mais quand même…

    Du coup je ne suis pas d’accord avec ton raisonnement, le code de la mauvaise feignasse est sûrement insuffisamment fainéant s’il n’y a aucune adhérence à d’autre bout de code (pourquoi donc écrire les exports CSV en Java pour le plaisir?); mais peut très bien avoir du sens si on réutilise des traitements, par exemple si la sortie n’est pas aussi plate que du CSV, ou s’il y a des transformations au passage sur les données.

    Sinon la non gestion des exceptions me pique les yeux, mais comme tu dis qu’il ne fallait pas regarder…

    ++

  • Comme je connais tes qualités d’architecte, je suis forcé d’être d’accord avec toi; mais comme c’est moi qui ai écrit ce post, je suis forcé de te contredire (j’ai dit plus haut qu’un bon architecte devait être consistant, mais là c’est le blogueur qui parle, et il est le maître de son royaume).

    Concernant les drivers JDBC, je suis tout à fait d’accord avec toi, notamment sur le fait qu’il en existe de mal écrits (encore qu’il y a « mal écrit » et « mal écrit »); en revanche, lorsque je dis que l’on charge toutes les données en mémoire, je ne dis pas qu’on le fait en une fois, mais le fait est que chaque appel à ResultSet.next() va, dans le pire des cas, charger une ligne de résultat complète en mémoire, et dans le meilleur charger une référence vers cette ligne (d’ailleurs, selon ce qu’on souhaite en faire, le meilleur et le pire des cas ne sont pas toujours ceux que l’on croit).

    Ensuite, concernant le fait d’écrire un export CSV en Java, pour répondre à ta question :
    - oui, je fais ça pour le plaisir, tout à fait;
    - ensuite, il arrive qu’on se retrouve à bosser sur du code existant, et il arrive (malheureusement très souvent) que ce code soit écrit en Java (ceux qui me connaissent savent que je crache ici un peu dans la soupe);
    - enfin, il arrive qu’on doive faire un export CSV pour plein de raisons différentes :
    – sortie standard vers un tableur de type Excel (pour le coup tu auras certainement du mal à me dire que le format xls est plus propre que le CSV et être de bonne foi en même temps);
    – sauvegarde de fichiers plats;
    – envoi sur un flux réseau;
    – plus pragmatiquement, parce que ça a été écrit quelque part, dans un document de spécifications.

    Enfin, sur la gestion des exceptions (notamment, je pense que IOException doit vraiment te démanger, peut-être plus que SQLException), on est bien d’accord qu’utiliser le code tel quel serait non seulement une erreur, mais je suis à peu près certain que le compilateur, non seulement ne compilerait pas, mais mettrait un taquet au développeur qui copierait/collerait ce truc.

  • GreG:

    Sur mon histoire de charger toutes les données en mémoire je voulais dire « d’un seul coup ». En effet tu ne me contrediras sûrement pas si je dis qu’il existe un anti-pattern très classique pour ce type de code consistant à tirer toute la table en un coup en RAM puis à écrire le CSV. Résultat il faut 2Go de RAM pour faire une tâche faisable avec 4ko (remplacer « 4ko » par l’empreinte mémoire de la plus grosse ligne). C’est comme cela que j’avais compris ton billet et c’est ce que je voulais contredire (les ResultSet.next() étant fait au fur et à mesure). Maintenant je veux bien croire que j’avais compris de travers. :-)

    Sinon, j’avais pas pensé à te dire que faire de l’XLS était plus propre que du CSV, sinon sois sûr que j’aurai mis toute la mauvaise foi du monde à l’écrire. Je m’en veux de ne pas avoir eu l’idée. J’aurai pu à loisir remarquer la faiblesse de MySQL à ne pas savoir faire les exports directement dans ce beau format. Voire rappeler que les logiciels libres sont écrits par des étudiants communistes, contrairement aux vrais trucs sérieux comme Oracle (hein, comment ça MySQL appartient à Oracle?).

    Pour les exceptions la seule chose qui me pique les yeux c’est de ne pas voir les close() dans un bloc finally savoir ce que tu catches et ce que du throw je manque de contexte. Mais bon, encore une fois tu avais dit de ne pas regarder, je le respecte.

  • GreG:

    Tiens et puis il manque le ps.close() aussi dans le bloc finally qui n’existe pas et que je ne devrais pas commenter.

  • mea culpa, en effet; ceci étant dit, ton commentaire soulève une question que je ne me suis pas assez posé, aussi je dois m’engager, à l’avenir, à donner des exemples complets, à défaut de quoi il vaut mieux que je les écrive en pseudo-code.