En effet, dans ce contexte, les tests obligent le développeur à suivre de manière plus formelle les préceptes de base de la programmation orientée objet regroupés sous l’acronyme SOLID.
Les tests écrits dans le cadre du TDD sont donc, en plus d’être un signal d’alarme en cas de problème, des indicateurs qui indiquent au développeur le chemin à suivre pour écrire un code plus lisible et plus facile à réutiliser, maintenir et à faire évoluer.
Si les tests n’améliorent pas la qualité du code, ils permettent donc cependant au développeur qui les rédige de le faire tel un guide de haute montagne montrant à l’alpiniste pourtant chevronné le chemin à la fois le plus efficace et le plus sécurisé lui permettant d’atteindre le sommet de la montagne de la manière la plus sure.
Car malgré son expérience et son intelligence, le développeur reste tout de même un être humain et il est donc par nature faillible et surtout fainéant.
Il est en donc parfois pour lui très tentant d’emprunter des raccourcis ou de prendre des chemins de traverse, car à première vue le chemin à parcourir pour atteindre l’objectif sera plus facile, plus rapide ou moins fastidieux.
Il oublie alors les bonnes pratiques, car il ne veut pas prendre le temps de faire les choses correctement, non pas parce qu’il ne veut pas faire bien son travail, mais parce qu’il a la pression de sa hiérarchie ou bien parce qu’il pense que cela sera sans conséquence, oubliant par la même le sacro-saint précepte de Edward Aloysius Murphy.
Ou bien alors, il pense tout simplement qu’il n’a pas forcément besoin de suivre le chemin plus fastidieux imposé par le respect de SOLID pour obtenir le résultat voulu : peu importe la forme, l’important est que ça fonctionne.
Or, les tests ne permettent pas cela, car un code écrit de cette façon devient très difficile à tester, et c’est d’ailleurs souvent ce qui les rend si détestable aux yeux des développeurs.
D’ailleurs, un bon indicateur empirique de la qualité d’un code est la facilité d’écriture des tests associés.
Si ces derniers sont simples à rédiger, alors il y a de fortes chances que le code correspondant soit de qualité.
Dans le cas contraire, c’est un indicateur très fort de la nécessité d’une refonte de l’architecture du code.
Arrivé à ce point de mes réflexions, j'ai réalisé que le mot indicateur
y revient régulièrement, et j'en suis donc arrivé à la conclusion que les tests sont donc de mon point de vue en tout point similaire à la signalisation routière.
En effet, tout comme les tests pour le développeur, certains panneaux routiers indiquent au conducteur la direction à suivre parmi tous les chemins possibles pour arriver correctement à destination, tandis que d’autres l’avertissent d’un danger plus ou moins imminent ainsi que sur sa nature.
Et tout comme un test ne peut vérifier que les comportements pour lesquels il a été conçu, les radars routiers sont capables de détecter automatiquement de par leur conception uniquement certaines infractions spécifiques et ignore totalement toutes les autres.
Pour la gestion de ces dernières, l’intervention humaine sous la forme d’un humain coiffé d’un képi reste incontournable, tout comme le travail en binôme, la revue de code et la recette manuelle sont incontournables pour le développeur même lorsqu’il met en œuvre des tests automatisés.
De là à dire que le développement revient à arpenter un chemin semé d’embuches diverses que les tests nous permettent plus ou moins d’anticiper efficacement…
5 réactions
1 De Bouh - 10/03/2014, 23:20
Bonjour,
Tout d'abord, comme toujours, l'article est apprécié!!
Via hackernews je suis tombé sur ce texte de James Coplien qui contient la même citation : http://www.rbcs-us.com/documents/Wh...
J'ai trouvé ce texte assez intéressant, il parle, entre autre, de la finalité des tests, et de l'information et la valeur que procure un test. D'un point de vue personnel, il m'a permis de prendre un peu de recul vis à vis des tests unitaires. Car la période suivant ma découverte de cette démarche j'avais tendance à repousser la majeur partie de ma réflexion à plus tard : lors de l'écriture des tests.
Or c'est peu le sentiment que j'ai quand en ayant lu vos derniers billets, notamment : "Le développement piloté par les tests m’oblige en effet, pour que je puisse rédiger mes tests, à réfléchir à l’architecture de mon code avant de l’écrire, au lieu de laisser émerger cette architecture de mon code et éventuellement de la corriger à postériori, au risque d’y introduire des bogues ou de provoquer des régressions."
Cela donne l'impression que vous misez tout votre processus de développement sur les TDD, y compris la phase de conception. J'ai le sentiment que c'est seulement parce que vous voulez mettre en place des tests "faciles" à maintenir (à savoir, invariant lors de changement de code à périmètre fonctionnel constant) que vous vous intéressez à comment vraiment mettre en place une architecture flexible.
Et pourtant, d'après moi, la phase de conception de l'architecture devrait être faite au préalable (même dans une démarche agile) afin justement de fournir le support permettant l'ajout / modifications de fonctionnalités sans une refonte drastiques des API. Une fois cette étape suivie, alors naturellement les tests seront plus stables au changement.
Bien à vous
2 De mageekguy - 11/03/2014, 09:34
@Bouh :
Exactement !
Les tests sont pour moi une barrière qui me permet de me canaliser dans mon développement (car par nature, je suis plutôt du style bulldozer), et cela d'autant plus que même avec une phase de réflexion en amont, il est très difficile pour moi de penser à tout sans avoir un minimum de code fonctionnel à ma disposition.
Et comme l'expérience m'a montré que les tests sont, par rapport à ma façon de développer, bien plus efficace pour faire émerger une architecture correcte que toutes les minutes/heures/jours que je pourrais passer à y réfléchir, je mise effectivement tout sur mes tests, même si pour les choses "originales", je code bien souvent un POC "à l'arrache" pour avoir une idée relativement précise du flux d'information et des enchaînements, POC qui me sert ensuite à la rédaction de mes tests et donc au développement du code "propre".
J'ai de cette manière un feedback beaucoup plus rapide et précis par rapport à une "intellectualisation" de l'architecture du code et je suis donc plus productif.
C'est une approche empirique et pragmatique (tiens tiens…) mais dans mon cas, elle fonctionne très bien notamment car le TDD permet de coder de façon à avoir un code modulaire, donc facile à faire évoluer sans les changements drastiques d'API dont tu parles (Si tu fais du SOLID, les changements drastiques d'API ne sont plus nécessaire, et comme le TDD force le développeur à suivre SOLID, car pour être testable, un code doit suivre ces règles, la boucle est bouclée).
3 De ouarzy - 14/03/2014, 09:17
Pour ajouter un peu de sel à votre discussion: @Bouh je te recommande cette histoire sur la création d'un avion propulsé par l'homme par Paul MacCready: https://signalvnoise.com/posts/2861...
En très résumé, c'est un exemple montrant qu'en cas de forte incertitude (ce qui est la définition même d'une phase de conception architecturale), il est plus rentable d'avoir une approche empirique itérative plutôt qu'une approche conceptuel et théorique qu'on ne valide qu'en fin de parcours.
C'est un point fondamental de l'agilité. Cela étant dit ça n'interdit à personne en aucun cas de réfléchir, comme l'explique bien l'oncle bob: http://blog.8thlight.com/uncle-bob/...
Simplement on réfléchi à condition d'avoir un feedback court. Si le feedback qu'on a sur notre décision n'est que dans plusieurs mois, alors on risque un grave problème.
4 De bouh - 17/03/2014, 13:55
Bon de toute façon @mageekguy, si ta méthode te convient et marche très bien pour toi il n'y a rien à redire !
Pour ma part elle ne me convient que moyennement, et cela pour plusieurs raisons :
- J'aime bien coder mais ce n'est pas non plus ma grande passion : je tire peu de satisfaction du processus lui même, ce qui me plait avant tout c'est ce qu'il permet et aussi le fait d'avoir mis en œuvre une solution efficace et élégante. Du coup coder un premier truc "sale" puis ensuite en refaire un "propre" m'embête un peu. Si il y a des parties pour laquelle je ne suis pas certain de la faisabilité, je préfère tester les limites de ma plateforme à côté.
- Des plateformes avec lesquelles je travaille : VBA (sic) et Scilab (re-sic). Cette dernière ne fournit aucun modèle objet (et ne permet pas de bien l'émuler), pas d'auto-complétion ni d'outils de débogage pratique (pas de points d'arrêts, etc). En conséquence de quoi moins j'ai à refactoriser, moins j'ai à déboguer et mieux je me porte.
En fait j'ai pour le développement à peu près la même idée que la composition de dissertation. Pour moi la phase de rédaction ne doit venir que lorsque les idées s'articulent bien. Tout comme sur un brouillon ne figure que le plan des idées et non pas la dissertation entièrement rédigée, il n'est pour moi pas nécessaire d'aller jusqu'à "la mise en production de sa première itération". Du coup j'ai un peu le fantasme de parvenir à faire des itérations "virtuelles" d'architecture afin d'arriver à stabiliser mon "plan" avant de commencer à rédiger -à savoir déboguer et intégrer.
C'est peut être irréaliste, mais je veux m’entrainer à cet exercice qui je pense ne peut que m'être bénéfique. Je me suis trop souvent maudis à devoir réécrire du code par précipitation. Précipitation à vouloir coder pour d'obtenir des fonctionnalités (la gratification) mais le problème c'est que sans architecture on contraint l'ensemble des solutions et il arrive qu'on se retrouve face à une impasse lorsqu'on souhaite rajouter des choses.
Du coup j'essaie de me trouver un processus qui me permettrait d'arriver à une architecture stable le plus rapidement possible. Pour l'instant voilà comment j'essaie :
- Je définis les spécifications avec quelques user stories associées.
- J'étudie l'impact des contraintes d'ergonomie, de performance, de sécurité et d'évolutivité sur chaque spécifications.
- Je définis un premier modèle objet avec une première API.
- J'essaie de réaliser (toujours sur papier / rapport) mes user stories avec l'API (cela revient en fait à réaliser des diagrammes de séquence UML) et je met à jour mon API.
Après j'aimerais bien essayer de définir des "tests d'architecture". Ils consisteraient à faire des scénarios où j'évalue l'impact d'un changement de fonctionnalité ou l'ajout d'une nouvelle et à voir si il faut tout réécrire ou pas mais j'ai pas encore d'idée bien précise là dessus.
@ouarzy : Je n'ai pas tout lu encore mais, dans ton text il est dit "He came up with a new problem that he set out to solve: How can you build a plane that could be rebuilt in hours, not months?" Il a donc réfléchit en amont à comment créer un avion dont les caractéristiques soit évolutives. En d'autre terme, il faut fournir un cadre propice au changement (je parle au niveau du code), et c'est bien là le rôle de l'architecture comme le mentionne la phrase suivante : "So what’s the lesson? When you are solving a difficult problem, re-frame the problem so that your solution helps you learn faster. Find a faster way to fail, recover, and try again."
D'ailleurs comme je l'ai dis plus haut, je veux avoir des retours rapides sur mon architecture, la question étant encore de savoir comment.
(comment on fait des sauts de ligne ? xD)
5 De Ouarzy - 20/03/2014, 15:33
Pour commencer, on peut te retourner la remarque: "si ta méthode te convient et marche très bien pour toi il n'y a rien à redire !"
A la limite j'ajouterais: "si ta méthode ... marche très bien pour toi ET POUR TES COLLEGUES ..."
"moins j'ai à refactoriser, moins j'ai à déboguer et mieux je me porte"
Ceci montre une limite intrinsèque des outils de ton langage, qui sont de faite peu adapté pour du TDD, qui lui prone un refacto continu.
"Pour moi la phase de rédaction ne doit venir que lorsque les idées s'articulent bien"
Oui mais le tout est de savoir où placer le curseur? Je te recommande l'exercice d'écriture de billet de blog pour le voir. Trouver une idée à exprimer est assez simple. L'exprimer de façon clair et agréable est beaucoup plus compliqué. Et il faut souvent plusieurs itération et beaucoup de relecture (y compris de tiers) pour arriver à quelque chose de "satisfaisant".
"il faut fournir un cadre propice au changement"
Oui, oui et mille fois oui On est tout à fait d'accord!
"et c'est bien là le rôle de l'architecture "
C'est notre avis, mais dans un projet Waterfall standard, c'est tut l'inverse. L'achitecture va rigidifier autant que possible l'environnement, de façon à garantir le suivi d'un plan dans lequel on aura anticiper tous les problèmes
"je veux avoir des retours rapides sur mon architecture, la question étant encore de savoir comment"
Notre réponse c'est: en l'implémentant. Mais le problème je pense c'est qu'on peut mettre bcp de chose derrière "architecture". De mon point de vue, une bonne architecture c'est seulement avoir une structure souple. Bon pour ça pas besoin d'y passer une plombe: on respecte SOLID, on fait de l'injection de dépendance, on tests, et c'est gagné.
Par contre quand on parle d'architecture "fonctionnel", c'est à dire qu'on sort du cadre technique et qu'on se demande comment bien gérer les problèmes du métier pour lequel on code, il n'ya pas de pattern tout fait. C'est spécifique à chaque métier. Et c'est là qu'on trouve qu'il est plus rentable d'itérer sur de petites tentatives, plutôt que de passer des mois à théoriser sur ce qui doit être fait.
Cela demande deux choses importantes:
- être capable de changer son code sans régression (architecture souple + TU)
- ne pas considérer une réécriture comme un échec, mais comme une étape de plus vers la production d 'un code satisfaisant