Dans le développement informatique, il existe un proverbe, basé sur une version de la vie courante, et qui dit ceci :
Ne réinvente pas la roue.
Ce qui souvent signifie quelque chose comme :
Ne perds pas du temps à développer une fonctionnalité qui est déjà remplie par une bibliothèque open source que tu peux te contenter d’ajouter à ton projet.
Il y a aussi une variante qui dit :
Réinventer la roue carrée.
Et qui signifie que non seulement on a recréé quelque chose qui existe déjà, mais qu’en plus cette recréation ne fonctionne pas.
Mais comme tous les grands proverbes, citations et principes de l’informatique – au premier lieu duquel je mettrais KISS1, entre beaucoup d’autres –, sa signification réelle est peu claire et mal comprise. C’est pourquoi je vous propose une petite réflexion autour de ce proverbe et de tout ce qu’il implique.
- Je ne donnerai pas la signification de cette abréviation, parce que pour commencer il faudrait choisir l’une des 17 variantes à laquelle elle fait référence (ou l’une des 7 variantes selon Wikipédia EN), et même les plus courantes (« Keep it simple, stupid! » ou « Keep it simple and stupid ») peuvent donner lieu à des interprétations très différentes.↩
- Parfois, ça n’est pas grave – ou même intéressant – de réinventer la roue
- L’immense majorité des roues réinventées sont carrées, et bloquent les projets
- Comment savoir si je dois importer une bibliothèque ou réinventer la roue ?
Parfois, ça n’est pas grave – ou même intéressant – de réinventer la roue
Une question de besoin
Quand on y réfléchit, ce proverbe est un peu idiot et montre une limite quasi immédiate : si le concept de roue est universel dans l’humanité, sa réalisation peut être très différente selon ce dont on a besoin. Une roue de char, de voiture ancienne ou moderne, de formule 1, de draisine, de draisienne, de VTT ou de vélo de course, d’avion de chasse – de ligne – d’épandage, de train, de fauteuil de bureau, de tracteur – sans compter toutes les roues qui ne roulent, comme une roue à aubes, un volant, un rouet, une grande roue, une roulette…
Autant de situations très différentes qui ont mené à des solutions technologiques qui partagent un concept commun, mais n’ont pas grand-chose à voir dans la réalisation.
La métaphore est très pratique parce qu’elle directement transposable dans le monde du développement informatique : des besoins différents impliquent des solutions différentes, même si elles partagent un concept commun. (Je vous laisse ici faire votre propre digression vers la notion de convergence évolutive).
On en déduit immédiatement la première raison qui poussera à développer une nouvelle solution même s’il semble en exister une qui réponde au problème.
« Réinventer la roue » peut être nécessaire s’il n’y a pas de roue adaptée ; si les solutions disponibles ne répondent pas réellement au besoin, il faut en développer une nouvelle.
La taille compte
Aucune métaphore filée ici, mais un simple constat purement informatique.
Parfois, on a une bibliothèque qui fournit une simple fonction qui serait très pratique dans le projet… mais cette bibliothèque est gigantesque – ou vient avec une myriade de dépendances. Est-ce qu’on a besoin d’alourdir sensiblement le projet, avec tout ce que ça implique1 pour éviter d’avoir à réécrire une simple fonction ?
Probablement pas.
- Espace disque, téléchargements, temps de compilation, d’intégration continue, de déploiement, surface d’attaque, quantité de failles potentielles, risques de régressions lors des mises à jour, consommation en ressources dans un monde où l’écologie et l’économie d’énergie deviennent de plus en plus importantes, et j’en passe…↩
L’immense majorité des roues réinventées sont carrées, et bloquent les projets
Un déluge de roues carrées
Douze années d’expérience – dont une partie en SSII/ESN – et de pratique du logiciel libre m’ont mené à travers beaucoup de projets de toutes tailles et de toutes natures.
Dans ces projets, beaucoup de roues ont été réinventées – quelle qu’en soit la raison, ça n’est pas le sujet pour ce point – et pratiquement toutes avaient au moins l’un des défauts suivants, comparativement à des solutions standards et adaptées au problème qu’elles adressaient. Et souvent plusieurs.
- Problèmes graves de sécurité.
- Mauvaise gestion des cas aux limites – c’est quasiment systématique : le cas général est à peu près bien géré, mais dès qu’on s’en écarte c’est la catastrophe.
- Mauvaises performances.
- API peu claire, parfois devenue mensongère.
- Utilisation peu pratique – en particulier dès que l’on sort du cas nominal exact pour lequel elles ont été prévues.
- Documentation absente ou obsolète (interne ou externe au code).
- Mauvaise découvrabilité d’une partie ou de tout ce qui a été réécrit – ce qui conduit à des situations ubuesques où une même fonction est réimplémentée plusieurs fois dans le même projet.
Toutes ces mauvaises réimplémentations de fonctionnalités standards ont un impact important sur le projet, augmentent sa dette technique et à terme le rende difficile à maintenir.
Plus les fonctionnalités réinventées sont des standards « de base », plus elles sont utilisées et plus il devient compliqué de s’en débarrasser, ce qui n’aide pas au rétablissement du projet.
Vous n’êtes pas seul au monde, et vos moi-du-futur et moi-du-passé sont des personnes différentes
Les gros avantages des bibliothèques tierces « standard », c’est qu’elles sont généralement bien documentées et qu’elles ont une bonne communauté qui les utilise, prête à vous aider en cas de besoin.
Ce qui n’est pas le cas de l’alternative que vous avez écrit en pensant bien faire.
En cas de problème dans le code qui implique une bibliothèque commune, un tiers – potentiellement vous, dans six mois – peut facilement se référer à la documentation officielle, ou demander de l’aide sur StackOverflow ou tout autre canal de discussion.
Mais si le problème est dans du code réinventé, il faudra faire avec ce code seul, et rien d’autre. Ce qui peut énormément compliquer la résolution du problème, je parle d’expérience.
N’oubliez pas non plus le principe de moindre surprise et les connaissances communes. Si le standard de fait pour résoudre un problème X est d’utiliser la bibliothèque Y dans le type de projet sur lequel vous travaillez, on s’attendra à ce que vous aussi l’utilisiez. Et donc une personne qui reprendrait la maintenance devra commencer par comprendre que vous n’utilisez pas le standard, puis devra comprendre ce que vous utilisez, et comment l’utiliser – ça complique énormément les choses.
C’était fourni dans le standard (ou dans une bibliothèque déjà utilisée)
Un classique, hélas très – trop – souvent croisé en conditions réelles, surtout sur des fonctions assez simples : quelqu’un a croisé une situation qui demandait quelque chose de non trivial, mais presque, et s’est empressé de l’implémenter.
Sans se rendre compte qu’il y avait déjà une méthode qui remplissait exactement son besoin dans la bibliothèque standard ou dans l’une des bibliothèques déjà présentes dans le projet, et qui le faisait mieux.
Ce problème est tellement présent que c’est littéralement une question de la démonstration du système d’entretien technique via CodinGame en Java.
La situation est la suivante : vous avez un tableau d’entrées uniques triées en entrée, vous devez implémenter une fonction qui renvoie l’index de l’objet passé en argument, avec des performances raisonnables sur de gros tableaux (> 1 million d’entrées).
L’astuce ?
L’API standard de Java possède une fonction qui répond exactement à la demande – et l’appeler est la seule façon d’avoir tous les points à la question… et pourtant beaucoup d’utilisateurs se lancent dans sa réimplémentation, avec plus ou moins de succès.
La solution standard aurait fonctionné si vous aviez lu la documentation
Ici c’est un problème qui touche plutôt les réimplémentations de grosses fonctionnalités, et qui lui est trop classique.
Il arrive que l’on essaie d’utiliser une bibliothèque, qu’on la trouve trop complexe ou mal fichue, et qu’on décide de la réimplémenter, après une étude rapide ou un essai de quelques heures, « parce que c’est encore plus simple comme ça ». Ce qui donne naissance à un monstre qui finira par couter des dizaines de fois plus cher qu’un investissement à comprendre et utiliser correctement ladite bibliothèque.
L’exemple typique dans le monde de Java, c’est les innombrables entreprises qui ont réimplémenté leur propre ORM après avoir regardé et parfois essayé Hibernate. Je n’en ai jamais croisé un seul qui arrive à la cheville d’Hibernate ; en général c’est des usines à gaz avec de la génération de code et des API infiniment trop verbeuses qui rendent toute « requête » illisible et impossible à maintenir en un temps raisonnable. La documentation d’Hibernate a longtemps été atroce, c’est un fait ; mais l’effort fait pour la comprendre et utiliser correctement toutes les fonctionnalités de ce très puissant produit aurait toujours été très rentable. En fait, l’effort pour remplacer ce genre d’outil est généralement tel qu’il aurait été plus rentable de comprendre l’outil et réécrire complètement sa documentation.
Comment savoir si je dois importer une bibliothèque ou réinventer la roue ?
Un rappel sur ce qui est important dans un projet informatique
On ne fait pas un projet pour l’amour du code1. Un projet informatique doit remplir un besoin, et il doit le faire bien – c’est-à-dire qu’il doit répondre au besoin des utilisateurs de façon robuste, maintenable, avec des performances acceptables.
La conséquence, c’est qu’il n’y a pas tellement de place là-dedans pour du code qui n’existerait que pour faire plaisir au développeur qui l’aurait conçu. Non, le code doit être le plus efficace possible dans les missions susnommées ; et la valeur ajoutée du développeur est dans la réponse au besoin de l’utilisateur.
On en déduit directement que tout ce qui permet au développeur de se concentrer sur ces problèmes est le bienvenu. Le développeur doit choisir ses outils dans ce but : toute énergie perdue à se battre contre des outils inadaptés ou à en créer inutilement est de l’énergie perdue pour le projet.
Ici, je pourrais partir sur une grande dissertation sur les principes KISS, la gestion de projet en général et les considérations métaphysiques qui découlent de tout ça – mais ça n’est pas le sujet de ce billet. Non, le sujet de ce billet, c’est le suivant :
Est-ce que je implémenter ce truc moi-même ou prendre une bibliothèque – et laquelle ?
- Ne prenez pas la première bibliothèque venue « parce qu’elle a l’air de faire le boulot » ou « Parce que tout le monde l’utilise ».
- Étudiez votre besoin, présent et prévisible. Vraiment.
- Vérifiez que vous n’avez pas déjà un outil pour y répondre dans votre projet.
- Regardez ce qu’il y a comme bibliothèque pour y répondre.
- Vérifiez que les candidats sont sérieux en regard de la responsabilité que vous lui délèguerez et des contraintes projet : suivi des mises à jour, qualité de la documentation, communauté, problèmes connus…
- Vérifiez les impacts de cette bibliothèque (sa taille, entre autres).
- Enfin, et seulement maintenant, vous pouvez prendre votre décision : si vous pouvez utiliser une bibliothèque, vous savez laquelle et pourquoi ; si vous devez réimplémenter une fonctionnalité, vous savez aussi pourquoi.
Le temps passé à tout ça dépend de la complexité et de la criticité de la fonctionnalité en question : quelque chose de presque trivial peut se suffire d’une recherche rapide sur Internet ; mais vous devriez réfléchir encore avant de vous attaquer à cette implémentation maison d’un système cryptographique…
Dois-je remplacer ma bibliothèque par une implémentation personnalisée de la fonctionnalité ?
Tout dépend de pourquoi.
Parfois le besoin ou le contexte ont évolué et quelque chose qui fonctionnait bien ne marche plus. C’est alors le moment de refaire une passe d’évaluation (telle qu’au paragraphe précédent), qui peut déboucher sur un changement d’outil, une nouvelle implémentation, ou la décision de ne rien changer. Mais sans réflexion, aucune décision correcte n’est possible.
Par contre, « Je ne comprends plus ce que fait ma bibliothèque » n’est jamais une bonne raison de la virer. C’est un classique (là encore, je pense à Hibernate en Java, mais pas seulement), mais c’est surtout la garantie de se créer d’autres problèmes ensuite. Soit on a mal compris le problème, auquel cas on peut se diriger vers une mauvaise solution ; soit on a mal compris l’outil – ça arrive – et on ne peut pas l’exploiter correctement.
La seule attitude valable face à une situation où l’outil nous échappe, c’est de réussir à comprendre au moins pourquoi il nous échappe (même si on ne parvient pas à comprendre l’outil en lui-même).
- Sauf cas particulier, comme des projets d’étude, de test de langage, etc. qui ne rentrent par conception pas dans le cadre de ce billet.↩
Et donc :
Ne réinventez pas la roue !
Sauf si vous savez très exactement ce que vous faites et que vous avez une excellente raison de le faire.
Dans ce cas, documentez-le.
Icône d’après Fred.th, CC BY-SA 3.0.