Récemment, en faisant des revues de code avec mes développeurs, j’ai eu avec eux des discussions intéressantes que j’ai envie de partager sur ce blog.
L’un d’eux utilisait une pratique que j’appelle le code défensif. L’autre avait des idées d’optimisation de code qui étaient de la sur-optimisation. J’ai pris le temps de leur expliquer en quoi ces pratiques sont néfastes.
C’est quoi le code défensif ?
Ce que j’appelle du code défensif, c’est du code qui se défend contre le développeur qui l’a codé et qui va l’utiliser.
Je ne parle pas de l’écriture de bibliothèques qui sont destinées à être (ré-)utilisées dans des contextes variés et par des personnes très différentes. Dans ce cas-là, il faut au contraire prévoir le maximum de cas d’échec, et partir du principe que la bibliothèque est une « boîte noire » à laquelle on est susceptible de fournir des données complètement erronées.
Non, mon propos concerne du code qui « se connait » (si je puis dire). Un contrôleur qui appelle un objet métier dont il est le seul utilisateur. Une méthode publique qui appelle une méthode privée du même objet. Une fonction Javascript qui manipule un élément DOM de la page sur laquelle elle est présente.
C’est quoi le problème avec le code défensif ?
On pourrait se dire que c’est « mieux » de vérifier systématiquement les entrées d’une fonction. C’est plus « propre », c’est plus « sérieux ».
Mais il ne faut oublier que cela a deux impacts.
Le premier impact est peut-être le moins important : Toutes ces vérifications prennent du temps. Oh, pas grand-chose, bien sûr. Mais à force de vérifications inutiles, on finit par dégrader les performances d’une manière qui peut être perceptible.
Le second impact est bien plus grave : Ajouter des vérifications, cela veut dire ajouter du code. Plus de code veut dire plus de maintenance. Le code est moins lisible, il est plus difficile de faire évoluer la partie applicative, car il faut déjà la séparer du reste. Et quand on fait évoluer les fonctionnalités, il faut faire évoluer les vérifications initiales.
Encore une fois, il y a des situations où cela se justifie, et d’autres où c’est complètement inutile.
Quelques exemples
Imaginons un objet qui peut être utilisé de plusieurs manières. On va vérifier les entrées-sorties publiques, mais le développeur est censé savoir ce qu’il fait à l’intérieur de l’objet.
class AjouteEnBaseDeDonnees { public function ajoutEntier($i) { if (!isset($i) || !is_int($i)) throw new Exception("Mauvais paramètre."); $this->_ajoute($i); } public function ajouteFlottant($f) { if (!isset($f) || !is_float($f)) throw new Exception("Mauvais paramètre."); $this->_ajoute($f); } private function _ajoute($n) { // code défensif complètement inutile if (!isset($n) || (!is_int($n) && !is_float($n))) throw new Exception("Mauvais paramètre."); // ... ajoute la valeur en base de données } }
Dans un cas comme celui-là, on voit bien qu’il est inutile de remettre dans la méthode privée les vérifications qui doivent être faite au plus tôt lors des appels à cet objet.
Autre exemple, une fonction Javascript qui manipule les éléments d’une page.
<html> <head> <script type="text/javascript"> // on part du principe que jQuery est chargé function afficheDate() { var panel = $("#panel-date"); // code défensif inutile if (!panel[0]) { alert("Je ne peux pas écrire la date !"); return; } panel.html((new Date()).toDateString()); panel.show(); } </script> </head> <body> <div id="panel-date" style="display: none;"></div> <div onclick="afficheDate()">Affiche la date</div> </body> </html>
On est d’accord, personne ne code réellement ce genre de chose. On fait des objets Javascripts qui sont proprement rangés dans des fichiers, qui sont eux-même nommés en fonction du namespace de ces objets. Mais ça reste valable dans l’idée.
Dans cet exemple, le code défensif est doublement inutile. Non seulement le développeur qui code la page est censé savoir ce qu’il fait, mais en plus jQuery gère les erreurs silencieusement. Honnêtement, pour que ce code ne fonctionne pas, il faut vraiment que le développeur ne teste pas sa page une seule fois.
La sur-optimisation de code
L’autre cas intéressant est la sur-optimisation. On comprend tous instinctivement qu’il ne faut pas optimiser inutilement. Mais il semblerait qu’on ne réussisse pas tous à sentir quand quelque chose relève de la sur-optimisation.
Pour commencer, revenons rapidement sur les problèmes générés par la sur-optimisation. Ils peuvent être de deux ordres.
Le principal soucis est similaire à celui du code défensif. Écrire plus de code implique de maintenir plus de code ; cela entraîne des difficultés pour le faire évoluer. Le code est plus dur à débugguer et plus lent à améliorer. Ces inconvénients ne peuvent pas être négligés, car à moyens terme ils peuvent devenir particulièrement coûteux.
De manière plus anecdotique, il faut voir qu’une optimisation inutile peut se révéler être contre-efficace. De la même façon qu’un pattern peut se transformer en anti-pattern quand il est mal utilisé, une sur-optimisation peut réduire les performances si elle est mal appliquée ou si elle s’exécute dans un contexte différent de celui qu’on avait en tête quand on l’a codée.
Prenons deux exemples :
- Ajouter 15% de code en plus, correctement compartimenté, pour multiplier par 25 les performances d’affichage de toutes les pages d’un site web, c’est bien.
- Ajouter 20% de code en plein milieu de l’existant, pour gagner quelques poussières sur des pages qui représentent moins de 5% du trafic global, c’est inutile.
Mais alors, comment reconnaître une sur-optimisation ?
Posez-vous deux questions simples :
- Quel pourcentage de code va être modifié ou ajouté ?
- Quel pourcentage d’amélioration puis-je espérer, par rapport au projet global ?
Ensuite il suffit d’appliquer quelques règles :
- Plus de 33% de code modifié ou ajouté ? Pas bon.
- Moins de 50% d’amélioration ? Pas bon.
- Ça prend plus de temps d’optimiser que de coder la fonctionnalité ? Pas bon, sauf si le gain en performance est décuplé (x10 minimum).
Ensuite il ne reste plus qu’à adapter à chaque situation.
Optimiser trop tôt
Ceci est un cas particulier de la sur-optimisation. Comme le disait Donald Knuth :
Premature optimization is the root of all evil.
Les raisons pour lesquelles on peut chercher à optimiser trop tôt sont multiples :
- L’habitude. On applique une recette telle qu’on l’a déjà appliquée maintes et maintes fois. La force de l’habitude fait qu’on ne réfléchit pas.
- La volonté de bien faire. Après tout, vouloir optimiser un code pour le rendre plus performant, c’est une bonne chose, non ?
- La facilité. Quand on a terminé un développement, il est parfois tentant de le peaufiner encore et encore, plutôt que de se pencher sur un autre développement.
N’oubliez pas que ce n’est pas parce qu’on a fait les choses plusieurs fois d’une certaine manière que ça en fait une « bonne pratique » − même si c’était une bonne chose à chaque fois jusque-là.
Mais pourquoi optimiser prématurément est si gênant ?
Tout simplement parce qu’il est plus important de mener un projet à son terme que de l’améliorer. Chaque chose en son temps : développer le projet d’abord, l’affiner ensuite.
À chaque fois qu’un bout de code est complexifié par une optimisation prématurée, on rend plus difficile les ajouts et extensions. Tant qu’un développement n’est pas complet, chaque optimisation qui y est apportée revient à s’ajouter des obstacles dans une course de fond.
Salut! Comme d’hab, je participe à la défense du diable. Je fais partie de ces développeurs qui font du « code défensif ». Je ne connaissais pas l’expression d’ailleurs, c’est une vraie expression ou c’est une tentative de caler un néologisme pas bête ? Bon, au delà de ça, je suis globalement d’accord avec toi que en théorie, le développeur sait ce qu’il fait. Sauf que : tu ne codes pas toujours voire rarement tout seul. En ce qui me concerne, ça ne m’arrive que sur mes projets perso. Cela implique 2 choses : l’un des développeurs qui suivra est un bras cassé. Deuxième chose : l’un des développeurs qui suivra est un bras cassé.
J’explique. Dans le premier cas, il ne suivra pas tes recommandations, n’aura rien à faire de ta volonté pour ta fonction et la fera évoluer pour qu’elle change selon son besoin. C’est la loi de la vie, sauf que cette personne n’aura pas vérifié les impacts de ses modifications. C’est un bras cassé.
Finalement un code défensif aura tendance à lui dire d’arrêter ses conneries. Je suis d’accord que ça ne gênera pas les vrais bras cassés mais c’est un espoir de faire changer les moins pires. En plus de les former.
Je suis pour le code défensif. Même si il ne faut pas en abuser. Comme tu dis, jQuery gère les erreurs. A mon sens, c’est grave, parce que les développeurs ne les voient pas leur péter au nez. Et n’apprennent pas. On leur donne trop de permissivité.
On parle de «programmation défensive», mais sur un concept un peu plus large. Je n’essaye pas de créer un néologisme (même si je l’ai déjà fait).
Je vois ce que tu veux dire, mais ça ne rend pas les choses plus malines pour autant. Honnêtement, parce que tu travailles avec des crétins tu te mets à truffer tes méthodes privées de vérifications, au cas où elles seraient appelées d’une manière incorrecte ? Tu vérifies systématiquement la présence de tous les éléments DOM que tu manipules en Javascript, même si tu sais pertinemment qu’ils sont forcément là ?
Pour ma part, je préfère :
– Former mes développeurs, en prenant le temps qu’il faut pour les faire monter en compétence, en les accompagnant jusqu’à ce qu’ils soient capable de faire face aux situations de la « vie réelle ».
– Imposer des bonnes pratiques. Comme documenter correctement les méthodes, pour qu’on sache comment elles doivent être utilisées. Ou utiliser des normes de codage et de nommage qui permettent à tout le monde de s’y retrouver rapidement.
– Mettre en place une équipe dont les membres communiquent efficacement et savent travailler ensemble.
– Sensibiliser tout le monde (techniciens et fonctionnels) aux tests.
C’est infiniment plus valorisable sur le long terme. En tout cas, c’est mon rôle de mettre en place les bonnes pratiques de codage et de travail collaboratif. Pas de faire écrire du code sub-optimal qui tente de s’auto-protéger contre des mauvais développeurs.
Exactement ce que je prône à mes collègues depuis des années !
J’ajouterai une chose concernant le premier point (code défensif). Quand on applique ce genre de technique, on se pose souvent la question : Que dois-je faire si les contrôles sont négatifs (mauvais paramètre d’entrée, état incohérent) ? En Java, on a souvent trois choix :
– soit renvoyer une « checked exception », c’est-à-dire une exception que devra « gérer » le client de la méthode ; le problème, c’est que, en règle générale, si le client a mal utilisé la méthode, il ne saura rien faire de plus du cas exceptionnel ; on se retrouve alors avec du code inutile dans la méthode mais aussi dans le client.
– soit renvoyer une « runtime exception », c’est-à-dire une exception qui sera remontée « silencieusement » au client de la méthode ; là aussi, en règle générale, ne pas faire ce contrôle aurait de toute manière amené à une « runtime exception » (typiquement une exception de type NullPointerException) ; le contrôle n’a donc pas de grande valeur ajoutée ;
– soit renvoyer une valeur null ; dans ce cas, c’est le client de la méthode qui devra, à son tour, prendre en compte cette valeur « spéciale », ce qui demande encore du code supplémentaire, ou bien ignorer cette valeur et risquer d’engendrer par la suite une exception de type NullPointerException (ce qui revient au même constat que le deuxième choix).
Concernant l’optimisation, j’ajouterai également qu’elle s’associe très souvent avec une rupture du design de l’application. Le cas typique, c’est implémenter des règles métier dans du SQL alors que l’application est principalement écrite en Java et que le SQL ne devrait être utilisé que pour la récupération/persistance des données selon le design. Quand on commence à rompre avec le design, on rend inévitablement la maintenance de l’application plus difficile, et ce qu’on a pu gagner en optimisation, on le perd en terme de temps de développement et en terme de stabilité.
Merci pour cet article !
Je suis d’accord avec toi (même si je ne traiterai pas mes collègues de crétins, ils votn se vexer), il vaut mieux les former que de passer son temps à les maîtriser par le code. Cependant la formation prend vite du temps. D’autant plus quand la direction de la société n’a pas eu l’occasion d’être sensibilisé aux bonnes pratiques de développement. En l’occurrence, dans ma boite, on a un héritage d’une dizaine d’années d’apprentissage sur le tas. Mon chef a réussi à recruter plusieurs bons ingénieurs et les choses changent doucement mais surement. On a réussi à passer aux méthodes agiles en un an de bagarre « politique », faire entrer l’idée des tests unitaires à la direction, et plus récemment un contrôle systématique de la qualité des données. Chose sur laquelle je m’expliquerai bientôt sur mon blog.
Personnellement, je m’oppose à l’utilisation des valeurs de retours pour signaler un problème d’appel. Celles-ci ne sont là que pour l’information du déroulement des cas normaux de la fonction. Les erreurs d’appel devraient pour moi passer par des exceptions. Java, C#, ou même PHP sont relativement sévères avec le développeur. Contrairement à JS qui a une bien plus grande capacité de tolérance aux erreurs. Cette liberté est nocive quand l’équipe n’est pas encore au point. Dans l’idéal, je suis d’accord avec Amaury que le code défensif est une perte de temps, mais je ne travaille pas dans un monde utopique, il y a encore beaucoup de travail pour l’atteindre. Et donc dans l’immédiat, le code défensif me permet d’être tranquille sur l’idée qu’au moins coté code, je suis tranquille et je peux me concentrer sur la formation, la politique, etc.
Hello,
alors moi c’est sur le côté sur-optimisation que je ne suis pas d’accord.
Lorsqu’un market, un client ou ton patron te fournit un cahier des charges, le développeur doit avoir une idée de l’ambition du site. Combien de visites journalière sont prévus ? Quel est la volumétrie des données à stocker ?
Si je dois développer un site comme je l’ai déjà fait qui va prendre + de 500 000 visites par jour, mon développement ne sera pas le même que pour le site de madame michu.
Je pense donc que le développeur doit posséder un minimum d’informations pour pouvoir adapter son développement au produit final, si ce n’est pas le cas je préfère faire de la « sur-optimisation » que je vois plus comme de la prévention !!
Il ne faut pas voir la sur-optimisation comme dépendante de l’objectif du site. Il faut juste adopter certains réflexes salvateurs. Certaines micro-optimisations peuvent vite faire partie du code normal du mec et non demander un travail supplémentaire. Je pense bêtement à la comparaison stricte (parmi x exemples), un bon gain de performances, certes très localisé mais hyper efficace et ne demande pas de repasser après quand on a adopté le réflexe.
@MathRobin : Je ne vis pas non plus dans un monde utopique. Tout ce que j’écris fait suite à mon expérience personnelle. Et je persiste à dire qu’ajouter du code pour se protéger de ses collègues, c’est se tirer une balle dans le pied pour le jour où il faudra faire évoluer ce code.
@Guillaume : On est sur la même longueur d’onde. Ce que j’appelle de la sur-optimisation, ce serait appliquer exactement les mêmes recettes sur le site de madame Michu que sur le site qui va prendre 500 K visites par jour. Avec comme justification que « qui peut le plus peut le moins ». Ça c’est de l’optimisation prématurée complètement débile. Parce que toutes les évolutions du sites de madame Michu vont être plus complexes, plus longues et plus coûteuses, simplement parce les mauvaises solutions auront été appliquées.
Faire de l’optimisation « préventive » peut être une bonne idée si on a des indicateurs qui montrent que cela sera nécessaire. Mais honnêtement, j’ai vu des start-ups qui utilisaient des gros canons pour écraser des mouches minuscules : Vouloir faire du big-data et mettre en place des techniques capables d’absorber des pics de charges monstrueux, alors qu’ils n’ont même pas encore 10 visites par jour, c’est juste ridicule ! Ils perdent en souplesse et en réactivité, à un stade où cela devrait être leur priorité absolue.
Bon bah il ne me reste plus qu’à espérer ne pas avoir à vivre les mêmes problèmes que toi alors. Je suis encore au tout début de ma carrière. Peut-être que je m’en mordrai les doigts dans deux ou trois ans 😉
@MathRobien : Oui, je parle évidemment des optimisations qui ont un impact sur le code et son évolutivité.
Mettre du cache sur un site qui a 20 visites par jour et aucun problème de performance.
Utiliser du noSQL pour gagner en performance, alors qu’on manipule des données qui sont intrinsèquement relationnelles, et qu’on n’a aucun problème de perf.
Coder en assembleur un prototype de site (oui, celle-là est tirée par les cheveux, je l’avoue).
@MathRobin: Alors il y a bonne pratique et optimisation (le cas de la comparaison stricte fait partie des bonnes pratiques à mes yeux).
Quand je parle d’optimisation je pensais plus à la mise en place d’un système de cache, les traitements asynchrone, limiter les appels à la DB, ou même à PHP tout simplement via la pré-génération de pages …
@Amaury: « Faire de l’optimisation « préventive » peut être une bonne idée si on a des indicateurs qui montrent que cela sera nécessaire »
Je suis d’accord, mais est ce au développeur de prendre cette décision (un développeur consciencieux le fera certainement mais pour les autres) ?
@MathRobin : Oui, ne le prend pas mal mais le code défensif et la sur-optimisation font partie des erreurs qu’on voit quasi-systématiquement chez les jeunes développeurs. Ceux qui ont à cœur de « bien faire les choses », mais qui ont rarement eu à maintenir leur propre code pendant plusieurs années, et pour qui le travail en équipe est plus un fardeau qu’une opportunité.
Je le sais, je suis passé par là moi aussi 😉
Merci maître
YodaAmaury :pPlus sérieusement, si j’ai pas oublié d’ici là, je viens re-commenter cet article dans 3 ou 4 ans 😉
@Guillaume : Bonne remarque. Mais dans tous les cas, c’est rarement le client final qui va demander à ce qu’on mettre en place du cache, une file de messages ou une base noSQL.
C’est aux responsables techniques d’appliquer les bonnes solutions en fonction des problématiques qui sont exposées. Donc il faut que les développeurs et les responsables techniques sachent faire preuve de discernement quant à la mesure des moyens à employer, et ne pas toujours réappliquer les mêmes solutions par habitude.
@MathRobin : Ça roule 😉
La programmation défensive comme la sur-optimisation est un quotidien auquel nous sommes tous confrontés (débutants ou pas…bon plus les débutants). Je ne vois toutefois pas en quoi la volumétrie du projet influe sur le mode de programmation. On aborde le projet, selon notre niveau de connaissances de la technologie. Et tout bon développeur apprend de ses erreurs. Sans toutefois reproduire bêtement les solutions efficaces du passé.
De part mon expérience, une des techniques qui permet de ne faire ni l’un ni l’autre, est de commencer à faire du TDD. Durant les douze derniers mois, j’ai appliqué cette méthode sur tous mes projets, que ce soit de la maintenance évolutive, ou un nouveau projet. Quelque soit également la méthodologie (pour ma part Symfony 1.4, Symfony 2 et Node JS).
Avec le TDD, tester puis coder, permet de réfléchir en amont aux cas d’erreurs, aux exceptions, de coder en conséquence et finalement de coder en optimisant de manière intelligente.
« Je ne vois toutefois pas en quoi la volumétrie du projet influe sur le mode de programmation »
@Stephanie La volumétrie rentre en compte lorsque le développeur doit écrire ses requêtes SQL par exemple. L’attention ne doit pas être la même lorsqu’il interroge une table contenant 50 000 enregistrements et une table en contenant 50 millions !
Certaines requêtes doivent passer par des phases de profiling, comprendre pourquoi l’optimiseur a choisi un plan d’exécution plutôt qu’un autre, et parfois corriger son choix en forçant l’utilisation de certains index ou l’ordre de jointure (Choses qui arrivent régulièrement avec l’optimiseur de MySQL 5.1 par exemple).
Voilà pourquoi la volumétrie rentre en compte dans la phase d’optimisation.
Je suis complètement d’accord avec toi sur la programmation défensive, c’est une vraie plaie. C’est à ça que servent les Code Review, d’ailleurs, qui devraient etre obligatoire avant chaque release. Pour éviter qu’un « cretin » ne commence à faire , n’imp avec une méthode privé.
Je suis comme toi, complètement contre le fait que l’on doit alourdir du code pour se protéger des developeurs. Si c’est le cas, changez de dev, sérieusement. C’est qu’il y a un sacré problème, si vous ne pouvez pas faire confiance aux autres gens de l’équipe. Alors, oui, les méthodes public doivent avoir un minimum de protection, mais les privées, beaucoup, beaucoup moins.
Pour l’optimisation, pour moi il faut quand même se poser la question au début de la montée en charge etc pour démarrer avec un design qui ira dans la bonne direction, qui pourra évoluer et qui justement sera a terme optimisable facilement. Au début de chaque projet, nous nous posions la question: « Et si, hypothétiquement, ce composant ce prend 100* la charge actuelle, qu’est ce qu’on serait obligé de faire différement ». Parfois, le design est radicalement différent.
Je suis d’accord avec Amaury : faire du « code défensif » est souvent (mais pas toujours ! Comme d’hab, il n’y a pas d’absolus dans ce genre de trucs) nuisible. Mais comme le fait remarquer Stéphanie, c’est à ça que sert le TDD (que je ne pratique presque jamais, mais mon excuse c’est que je fais de la recherche et que le code de production, c’est pas pour moi — ahem…) . Si un dev commence à faire dériver une fonction/méthode au point qu’elle ne passe plus les tests, un de ses collègues devrait s’en apercevoir, non ? (Je « teste » peu, mais j’utilise une infrastructure de review de code avec Gerrit/Jenkins/etc. exactement pour ça).
Concernant l’optimisation prématurée, je vais citer Knuth, qui lui-même citait Hoare :
Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%
La dernière phrase est très importante : la partie critique d’une application est celle où 90% du temps est passé dans 10% du code. C’est pour ça qu’on a des fous furieux qui passent leur temps à optimiser certaines fonctions de bibliothèques (pour de l’algèbre linéaire, la gestion d’E/S, etc.) et à affiner celles-ci, au point de paraître ridicules car ils sont au cycle près : ça semble ridicule au premier abord, mais le jour où l’appli appelle cette bibliothèque pour faire du traitement du signal, etc., tout ira vite.
Le problème de l’optimisation prématurée est le fait de ne pas réfléchir à ce qu’on veut faire dès le départ. Amaury, je suis d’accord avec toi sur le fait que rester flexible/réactif devrait être la première chose lors du développement d’un projet. Cependant, si on ne tient pas compte dès le départ de certains aspects-clef de ce qu’on cherche à accomplir, alors on est condamné à devoir ré-écrire tout ou partie de l’application car elle ne passe pas à l’échelle. Si on parle de 100 lignes de code, évidemment on s’en fout. Si on parle de plusieurs milliers, ça commence à être problématique.
Il y a une différence entre la manière dont tu penses l’architecture de ton logiciel, et la manière dont tu le codes.
Quand tu développes un site web, tu ne vas pas mettre en place des architectures de malade alors qu’il ne fait même pas 100 visiteurs par jour. Ce serait se donner des contraintes qui vont ralentir les évolutions fonctionnelles.
Par contre, on est d’accord que si ton site s’effondre avec 3 visiteurs simultanés, c’est qu’il y a un soucis. Mais je pense que le problème est alors dans l’architecture, pas dans la mauvaise optimisation du code.
Tout est une question de mesure, comme toujours.
Ne pas oublier aussi que les méthodes agiles nous poussent à faire des développements qui sont améliorables (ou optimisables) de manière itérative. Commence par faire quelque chose qui fonctionne, puis utilise les outils qui te permettront de l’améliorer morceau par morceau. Il est au final assez rare d’avoir besoin de repartir sur un page complètement blanche ; la plupart du temps, on peut faire des refactoring qui opèrent sur des parties différentes du code, ce qui élimine tout besoin de vouloir sur-optimiser prématurément le code.
(encore une fois : optimiser le code != optimiser l’architecture)