Retour aux fondamentaux du craft : trois exemples

Il y a quelques années, j’avais déjà 20 ans d’expérience en développement, et j’avais vu énormément de sujets. Je savais bien qu’il y avait aussi de très nombreux sujets sur lesquels je ne connaissais rien, et j’en avais de plus en plus conscience. Mais quelque chose de plus me trottait dans la tête : et si j’avais aussi besoin de réviser les bases ? Est-ce que j'avais besoin de revoir les choses que je connaissais déjà ?

J’avais la chance de travailler régulièrement avec des personnes expérimentées qui m’apprenais encore des choses dans des domaines que je pensais avoir parfaitement explorés. Et en plus de ça, ces personnes me rappelaient des choses que j’avais connues, mais oubliées. Des choses parfois impactantes, sur des sujets comme le Refactoring, Test-Driven Development, ou la conception logicielle.

J’avais bien besoin de réviser ces notions que je croyais connues. Cette idée m’a plu, et je me suis retroussé les manches. J’ai ressorti des livres que j’avais lu il y a longtemps, eu de nouvelles discussions sur des sujets anciens. J'ai découvert de nouvelles et de vieilles choses à apprendre. Voilà quelques exemples choisis de ce que j’ai (ré-)appris depuis.

Avant de me lancer dans le sujet, je préfère prévenir que je ne vais pas entrer dans les finesses de ce que j’ai appris. Mon intention est avant tout de vous donner envie de réviser vos classiques à votre tour en donnant ces trois exemples qui me sont propres. Si vous découvrez ces sujets, vous aurez besoin d’un peu plus que cet article pour bien les cerner. Cependant, si les sujets vous sont déjà familiers, vous verrez probablement pas mal de pointeurs clairs et quoi travailler exactement.

Refactoring, Chapter One

Le livre Refactoring de Martin Fowler est sorti en 1999, et je crois l’avoir lu vers 2007 environ. J’avais été très intéressé à l’époque. Je l’ai lu du début à la fin en prenant le temps de comprendre les exemples de code. Je pensais en avoir tiré le maximum. Ensuite, IntelliJ est arrivé, et je me suis dit que je n’avais plus besoin de revenir à ce livre. Jusqu’à ce que…

Jusqu’à ce que j’observe une personne effectuer une série de refactorings sans IntelliJ, à la vitesse de la pensée, avec des tests toujours verts du début à la fin, pour aboutir à un meilleur design. J’ai été étonné de voir que, outil ou pas, je ne maîtrisais pas les bases du refactoring à ce point. Je n’étais pas capable d’aller à cette vitesse de manière inconsciente pour me concentrer sur le design. Et cette personne avait travaillé sans IntelliJ. Donc ce n’est pas qu'une question d’aller à toute vitesse grâce à un outil. Ce que j’ai observé et qui est atteignable, c’est d’intégrer à fond ces gestes de base jusqu’à les rendre automatiques.

J’ai donc repris le livre, qui venait de sortir dans sa deuxième édition, et j’ai suivi avec attention le premier chapitre. Après l’avoir compris à nouveau, j’ai créé un repository git pour pouvoir le refaire plusieurs fois à partir du début. Puis je l’ai répété cinq ou six fois avec attention, et avec intention. Mon objectif était d’intégrer des morceaux de geste, des étapes précises afin de pouvoir les jouer sans réfléchir.

Exemple d’un geste simple que je n’avais pas l’habitude d’utiliser et auquel je ne pensais jamais : “Split Loop”. Le geste consiste à séparer une boucle qui fait deux traitements en deux boucles qui font chacune un traitement, comme dans le code ci-dessous.

Avant la séparation :

  for (let perf of invoice.performances) {
    volumeCredits += volumeCreditsFor(perf);
    // print line for this order
    result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\\n`;
    totalAmount += amountFor(perf);
  }

Après la séparation :

  for (let perf of invoice.performances) {
    volumeCredits += volumeCreditsFor(perf);
  }
  for (let perf of invoice.performances) {
    // print line for this order
    result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\\n`;
    totalAmount += amountFor(perf);
  }

On peut voir que ça n’a rien de techniquement compliqué dans ce cas. Il suffit de dupliquer l’instruction de boucle “for” pour séparer le traitement en deux phases. Le seul "outil" nécessaire est Ctrl+C Ctrl+V. Mais bien que ça n’ait l’air de rien, c’est assez puissant car ça introduit de la souplesse dans le design. Et dans l’exemple de Martin Fowler c’est cette séparation qui permet ensuite de ranger ces deux choses au bon endroit. Puis d’appliquer à nouveau d’autres améliorations de design qui vont se cumuler. Cette notion de cumul est très importante. C’est grâce à ce cumul d’améliorations que même un petit détail comme Split Loop est à la fois peu coûteux et permet des choses puissantes à l’arrivée.

Il y a d’autres exemples inspirants dans ce chapitre, comme l’augmentation temporaire de la duplication en début de refactoring, avec l’exemple ci-dessus mais aussi “Replace Temp With Query”, que je vous invite à découvrir en lisant (et en pratiquant) au moins ce premier chapitre.

J’ai alors bien vu que non, je n’avais pas tiré le maximum de ma lecture de ce livre. Et si je suis très content d’utiliser les refactorings automatisés de mon IDE favoris (et je vous encourage à le faire), je suis aussi content d’apprendre à les connaître directement à la source. Ça m’évite de ne rester que sur les deux-trois refactorings auxquels je me limite par défaut. (Extract variable ou Extract method, Rename, Change Signature...)

TDD : Alors, tu mockes ou tu stubes ?

(Tout d’abord, si vous ne connaissez pas la pratique Test-Driven Developpment, je vous invite à lire cette introduction plutôt que la section ci-dessous. Vous en tirerez probablement plus de valeur : https://martinfowler.com/bliki/TestDrivenDevelopment.html.)

Un autre exemple de question de base qui était restée un peu floue pour moi sans que je m’en rende compte, c’est : dans un test unitaire, quand utiliser un Mock et quand utiliser un Stub ? Dans ma pratique de Test-Driven Development je fonctionnais par imitation sur ce point, sans trop faire attention. Il faut dire que la nomenclature chaotique et non standardisée des framework de Mocks et autres Stubs ne m’a pas aidé.

Dans mes recherches de réapprentissage, j’ai eu l’occasion de trouver pas mal de réponses pertinentes sur cette question. Il se trouve que j’avais déjà lu ces réponses au fil des années et que je n’avais pas pris le temps d’en mémoriser les subtilités ni de les intégrer. J’avais besoin d’y passer plus de temps, et je suis content de l’avoir fait.

Pour commencer, Gerard Meszaros a proposé une nomenclature qu’on pourrait qualifier de référence sur tous les Test Doubles (*), que Martin Fowler rappelle et utilise dans son article Mocks Aren’t Stubs. Son article montre aussi l’intérêt d’une approche classiciste et l’intérêt d’une approche mockiste. C’est un bon point de départ riche en nuances, mais il laisse la question ouverte : Mock ou Stub ?

(*) : Test Double est un nom générique, dans le contexte de tests automatisés, pour les objets qui vont remplacer un autre objet le temps d’un test. On y trouve les Stubs, les Fakes, les Spies, les Mocks par exemple. Je vous invite à lire l’article de Martin Fowler ci-dessus pour plus d’informations.

J’ai ensuite trouvé des éléments tout aussi subtils et qui donnent une bonne guideline qui me convient très bien dans beaucoup de cas dans la présentation The Magic Tricks of Testing de Sandi Metz. Je vous invite à voir sa présentation et à chercher à bien saisir ce qu’elle propose dans chaque contexte. Pour vous donner envie d’en savoir plus, son approche est résumée par ce slide tiré de sa présentation :

Précision de vocabulaire pour comprendre les colonnes Query et Command du slide ci-dessus : dans sa présentation, Sandi Metz définit une Query comme une méthode qui renvoie quelque-chose et qui ne change rien (pas d’effet de bord observable). Et une Command comme une méthode qui ne renvoie rien et qui change quelque-chose, qui a un effet de bord observable. (Sauvegarde en base de données, envoie de mail par exemple).

C'est un peu mystérieux quand c'est condensé en un seul slide. Mais pour vous rassurer et mieux comprendre, allez voir sa présentation. Tout y est expliqué pas à pas, avec des exemples de code et des arguments logiques. Par exemple, la case "Outgoing Command", qui se situe en bas à droite, signifie que quand je veux vérifier une commande que l'objet que je suis en train de tester délègue à un de ses collaborateurs que j'ai besoin de substituer le temps de mon test (parce-que cette commande est une écriture dans une base de donnée par exemple), il est pertinent de le faire par un Expect to send sur mon test double, c'est à dire d'utiliser un Mock.

L’article Mocks for Commands, Stub for Queries de Mark Seeman va dans le même sens et analyse en détails pourquoi à l'inverse, c’est une mauvaise idée d’utiliser un Mock pour vérifier le comportement d’une Query. Son titre pourrait être un résumé du slide ci-dessus.

Après toutes ces révisions, un résumé rapide de ma règle par défaut pour un test unitaire serait :

  • Je n’utilise de Test Double que pour les collaborations inconfortables (appels bases de données par exemple)
  • Pour substituer une collaboration responsable d’une écriture ou d’un effet sur un système (sauvegarde en base de donnée, envoie de mail par exemple), j’utilise un Mock, et dans le test je vérifie que la bonne méthode a été appelé avec les bons arguments
  • Pour substituer une collaboration responsable d’une lecture, j’utilise un Stub

Maintenant j’ai une vision claire de quoi choisir et pourquoi. Et c’est un des points auxquels je porte attention dans mes revues de code. Et si Sandi Metz nous indique dans sa présentation “But sometimes break the rules if it saves $$”, avant de sortir des règles, c’est mieux d’avoir bien intégré ce qu’elles apportent et de savoir ce qu’on perd quand on les contourne. C’est justement ce que mes révisions m’ont permis de travailler.

Pour aller plus loin, on peut aussi utiliser des techniques de design pour avoir moins besoin de test double dans notre code. Des exemples sont décrits dans ces excellents articles de J. B. Rainsberger :

Legacy Code Techniques

Enfin, dernier exemple, lors d’une formation récente avec Michael Feathers, j’ai eu l’occasion de réapprendre les techniques Sprout Method et Wrap Method. Je les avais déjà vues plusieurs fois, mais sans les mémoriser. J'en confondais toujours la réalisation et l’intention.

Pour en savoir plus sur ce sujet, j’ai écrit un article qui décrit ces techniques et leurs intentions :

Bénéfice inattendu : une meilleure analyse des sujets plus avancés

Cette démarche d’approfondir des connaissances, comme vu dans ces trois exemples, m’a aussi apporté un bénéfice supplémentaire. Cette exigence sur les choses simples m’a remis en condition. Un peu comme un sportif ou une sportive a besoin de faire des exercices de base régulièrement pour entretenir sa condition physique, j’ai besoin d’apprendre et de réviser régulièrement pour rester pointu sur mes apprentissages, même sur des sujets simples.

J’ai également observé que suite à cette période, j’avais moins peur des sujets complexes. Je trouvais plus facilement un angle d’attaque ou un moyen de simplifier le problème. Je pourrais faire l’hypothèse que, un peu comme des gammes pour un musicien, être exigeant sur les fondamentaux au quotidien me permet d’aborder des sujets plus complexes avec moins d’effort quand ils se présentent.

Conclusion

Et si les “techniques avancées” commençaient par une connaissance approfondie des “techniques de base” ? Comme j'ai essayé de le montrer avec ces trois exemples, j'ai trouvé un grand bénéfice à revenir à des notions de base. Ce que j'aime à dire, c'est que ce sont des concepts simples qu'on apprend tout au long de sa carrière. Ce retour aux fondamentaux est quelque-chose que j'ai maintenant intégré à mes pratiques. Il prépare aussi le terrain pour les tâches difficiles. Pour finir, plus généralement, j'espère que ce retour d’expérience sera un encouragement à apprendre de nouvelles choses, mais aussi à approfondir les choses que vous connaissez déjà.