Accélérer le développement : une histoire de plomberie
Moi: Après avoir passé 5 jours dans l'équipe de développement, je pense qu'il serait judicieux de former et accompagner les développeurs à la mise en place de [la pratique X]. (remplacer [la pratique X] par : Test-Driven Development (TDD), Pair/Mob programming, Tres Amigos, ...)
Le DSI: [La pratique X] ?
Moi: Oui, [la pratique X], tu sais celle qui consiste à faire gnagnagni et gnagnagna.
Le DSI: Cela me semble très coûteux, et… on a vraiment pas le temps !
Moi: Pourtant, au vu de ce qu'on observe dans l'équipe, c'est ce qui semble le plus pertinent pour réduire le TTM (Time To Market)…
Le DSI: Démontre-moi le ROI (Return On Investment) de [la pratique X] dans l'équipe et on en reparle.
...le jour d'après…
Lead de l'équipe: Tu sais ce que m'a demandé le DSI ?
Moi: Non ?
Lead de l'équipe: Comme on risque de dépasser la deadline, il veut que je me mette ASAP à chercher deux développeurs supplémentaires à recruter dans l'équipe…
Ce dialogue est fictif. En réalité, j'ai assisté à ce type d'échanges dans plusieurs entreprises qui, pour aboutir aux mêmes conclusions, ont nécessité plusieurs jours et de nombreuses interactions entre différents niveaux de la pyramide hiérarchique. Ceci étant, ce qu'il est important de noter dans ce type de situation est le fait que la décision d'ajouter deux développeurs à une équipe ne pas fait l'objet d'un calcul de ROI alors que celle de mettre en pratique TDD, Pair/Mob Programming ou la revue de code semble requérir un examen économique approfondi. Je me suis donc posé la question de savoir pourquoi. Je pense que c'est une question de cadre logique. Pour ce manager, c’est parfaitement logique que l'ajout d'un développeur permette à l'équipe d'avancer plus vite. Calculer le ROI d'une telle décision lui semble aussi superflu que de prouver qu’un plus un font deux. Dans son référentiel cependant, le lien logique entre l'adoption de [la pratique X] et l'amélioration de la productivité de l'équipe n'est pas aussi évident. Cela représente à ses yeux une prise de risque. Il lui faut un argument béton qui le rassure : le ROI.
Très bien. Sauf que voilà, calculer le ROI d'un changement de pratique dans un système comprenant plusieurs dizaines de personnes, tout autant de briques techniques et des boucles de rétroaction à retardement c'est (presque) mission impossible. Pour pouvoir arriver à donner un tel chiffre, il faudrait lisser tout ça en faisant de nombreuses approximations, et, même comme ça, il serait nécessaire d'avoir accès à des grandeurs dont la plupart des entreprises n'ont pas conscience, préfèrent ignorer ou ne souhaitent pas divulguer : le coût de la non-qualité (taux de rework) et la valeur des features produites. Entre ces grandeurs inconnues et les simplifications de la réalité faites à la hache, un calcul de ROI, même fait consciencieusement, serait tellement bancal qu'il ne réussirait pas à convaincre.
Dans cet article, je vais tenter d'illustrer l'activité de développement telle qu'on peut l'observer de l'intérieur et d'expliquer les leviers logiques qui font de Test-driven development, Pair/Mob programming et Tres Amigos des pratiques pertinentes pour améliorer la productivité. Avec un peu de chance, dans ce nouveau référentiel, ces pratiques deviendront "logiques" et l'impossible calcul de leur ROI superflu.
Point d’étape:
- Certaines décisions sont prises immédiatement alors que d’autres semblent requérir un calcul de viabilité économique approfondi.
- Dans de nombreux cas, le calcul de viabilité économique est très compliqué voire impossible.
- Ce qui différencie ces deux modes de prise de décision est le caractère logique ou intuitif de la décision à prendre aux yeux du décideur (i.e. le risque perçu)
- Ce qui fait que telle ou telle proposition paraît logique ou intuitive est la représentation mentale que je me fais du système sur lequel je raisonne (cadre logique / référentiel logique).
La vue d’hélicoptère
Depuis la “vue d'hélicoptère” que peuvent avoir certains managers en lien avec une équipe de développement (n+2, n+3, DSI), il est possible de voir un processus de développement qui semble sous-optimal comme un tuyau encombré :
Avec cette représentation, on aura tendance à considérer que les gisements de productivité résident dans la vitesse d'écriture du code et la réduction du nombre de bugs (billes rouges, ci-dessus).
On pensera alors que l'équipe est sous-dimensionnée et/ou que les développeurs composant l’équipe ne sont pas suffisamment compétents (i.e. il semble possible d’implémenter les solutions plus rapidement en créant moins de bugs au passage).
Aussi, dans ce paradigme, il existe deux leviers logiques :
- Premier levier : Recruter davantage de développeurs
Avec plus de personnes disponibles à l’écriture du code, on imagine aisément que la capacité de la section correspondante s'élargira et que le goulet qu’elle représente finisse par se résorber:
- Second levier : Avoir des développeurs ”plus compétents”
Grâce aux compétences adéquates, le code est écrit plus rapidement (le goulet se résorbe de la même façon que sur le diagramme ci-dessus) et le backlog contient de moins en moins de bugfix (billes rouges).
Cependant, le recrutement de développeurs dans la situation actuelle du marché n’est pas toujours chose aisée. C’est un levier compliqué à actionner (long, coûteux et incertain). C’est pourquoi on voit souvent dans ce cadre logique, le recours à ce troisième levier :
- Maximiser le temps que les développeurs passent à écrire du code
En gardant le même nombre de développeurs dans l’équipe, on va chercher à maximiser le temps où ceux-ci sont devant leur poste à programmer. On va par exemple chercher à ne pas les solliciter dans les phases de conception et de cadrage en amont de la réalisation et limiter les rituels d’équipes. Ici aussi, on cherche à élargir la section créant le goulet d’étranglement en y consacrant le plus de temps possible.
Point d’étape:
- A partir d’une “vue de haut”:
- L’activité de développement se résume à la production/écriture de code.
- Dans cette hypothèse, les gisements de productivité sont : la réduction du nombre de bugs et la vitesse de codage.
- Les solutions intuitives sont alors : recruter davantage de développeurs (qui soient aussi, si possible, plus compétents) et maximiser le temps que les développeurs passent à écrire du code.
Une vue plus détaillée
Quand on zoome sur l’activité de développement, il devient possible de décomposer celle-ci en plusieurs phases : compréhension du problème, recherche de solutions, implémentation de la solution, mise en conformité et l’intégration/test/déploiement.
Bien sûr la réalité est encore plus subtile que ça : il peut y avoir des micro-boucles entre les phases compréhension du problème, recherche de solutions et implémentation de la solution.
C'est d’ailleurs la nécessité de ces micro-boucles qui fait du développement une activité complexe la différenciant des activités dites compliquées ou simples (voir Cynefin). Dans la pratique d’une activité complexe, il est nécessaire d’avancer par tentatives. On confirme ou infirme une première bribe de compréhension du problème en en concevant et implémentant un début de solution. Celle-ci, par son succès ou son échec, permet de comprendre davantage le problème à résoudre et de concevoir/implémenter une deuxième version de solution, qui à son tour donnera lieu à une troisième et ainsi de suite jusqu’à compréhension/résolution satisfaisante du problème. Les activités dites compliquées ou simples en revanche sont les activités qui permettent, moyennant plus ou moins d’efforts, de comprendre un problème et d’en concevoir une solution viable et exhaustive en amont de la phase d’implémentation (ex. une chaîne de montage industrielle ou la construction d’un meuble en kit). Pour les besoins de l'article, la représentation ci-dessus en 5 étapes suffira.
Point d’étape:
- En zoomant sur l’activité de développement depuis la “vue de haut”, il est possible de décomposer celle-ci en 5 différentes sous-activités que sont : la compréhension du problème, la recherche de solution, l’implémentation de la solution, la mise en conformité et l’intégration/test/déploiement. - Cette représentation en 5 étapes est imparfaite car elle gomme certains aspects d’apprentissage (micro-boucles) nécessaires au bon déroulement de l’activité de développement. Elle est cependant suffisante pour les besoins de cet article.
Goulet d'étranglement et débit
Chaque équipe étant unique, la capacité des 5 différentes sections varient d’une équipe à l’autre. Une chose est sûre cependant: c’est l’activité pour laquelle la capacité est la plus réduite (i.e. la section la plus étroite) qui détermine le débit de sortie de l'équipe**1****.** C’est d’ailleurs assez intuitif. Tout comme dans le cas d’un sablier, c'est la section la plus étroite qui dicte la vitesse d'écoulement. Peu importe la forme des parties supérieure et inférieure du sablier. Cette activité dont la capacité est la plus réduite est appelée goulet (d’étranglement). Pour augmenter la vitesse à laquelle le sable traverse le sablier, il est inutile d'augmenter la capacité des sections non-goulet : cela ne produira aucune accélération. Il en va de même avec le goulet d’une équipe.
En fait, élargir les sections en amont du goulet aggraverait même la situation : acheminer encore plus de matière vers le goulet va créer des pertubations qui diminueront encore sa capacité et par conséquent ralentiront le débit global.
Pour illustrer cette réaction, imaginons que je tienne un restaurant. Mon process enchaîne trois activités : servir une assiette à un client, laver l’assiette sale, essuyer et ranger l’assiette lavée.
Imaginons que l'activité goulet soit "laver l'assiette sale" : je ne pourrais pas essuyer plus d'assiettes que je ne suis capable d'en laver et ce, même si j'affecte plus d'employés au poste d'essuyage.
En revanche, si je double le nombre de cuisiniers augmentant ainsi le nombre d'assiettes servies aux clients, le poste de lavage devra, en plus de laver les assiettes, gérer le sur-afflux d’assiettes sales (empiler les assiettes, prioriser leur lavage, etc) ce qui réduira sa capacité et entraînera une diminution du débit global.
Point d’étape:
- Dans un process, la section la plus étroite est appelée goulet.
- Le goulet contraint le débit global à hauteur de son propre débit.
- Élargir les sections en aval du goulet n'a aucun effet sur le débit.
- Élargir les sections en amont du goulet, n’a, au mieux, aucun effet sur le débit et a, au pire, un effet négatif sur le débit.
Sollicitations multiples du goulet
Le goulet étant la section du process qui dicte le débit global, il est primordial qu’il soit sollicité à bon escient. Il faut donc, à tout prix, éviter de le solliciter plus d’une fois pour le même but.
Reprenons l’exemple du restaurant. Imaginons que les cuisiniers soient tête en l’air et se trompent régulièrement dans les commandes. Par exemple, pour un client donné ayant commandé un bœuf bourguignon, ils cuisinent un poulet frites. Le plat est retourné en cuisine, l’assiette sale contenant le poulet qui n’a pas pu être servie, et ne sera pas facturée au client, doit être lavée. Dans le même temps, une autre assiette est utilisée pour cuisiner le bœuf bourguignon que le client a effectivement commandé. Le poste de lavage, goulet de mon système, sera sollicité deux fois pour servir le même client.
Une façon efficace d’augmenter le débit du système consiste donc à m’assurer que les assiettes sales (input du goulet) soit lavées à bon escient. Autre cas de figure : il est fréquent que les assiettes arrivant au poste d’essuyage aient été mal lavées, on les retourne donc au poste de lavage pour être lavées de nouveau. Pour la même assiette, le poste de lavage, goulet du système, est donc sollicité deux fois, ralentissant d’autant le débit du système.
Une deuxième façon efficace d’augmenter le débit du système consiste donc à m’assurer que les assiettes lavées, soient correctement lavées dès leur premier lavage.
Point d’étape:
- Il est primordial que le goulet soit sollicité à bon escient. - Le fait de solliciter plusieurs fois le goulet a un impact immédiat sur le débit global du système. - Un goulet peut être sollicité plusieurs fois à cause:
- d’une tâche mal effectuée en amont du goulet
- d’une tâche mal effectuée au niveau du goulet
- Pour éviter cela, il est nécessaire de mettre en place des mécanismes qui garantissent la qualité :
- des activités générant les entrées du goulet
- de l’activité effectuée au goulet
Qu’est ce que ça change pour mon équipe de développement ?
Le plus souvent, quand on descend au niveau de l'équipe, on se rend compte assez vite que ce qui mine la productivité de l'équipe réside dans :
- La compréhension du problème (communication par tickets interposés entre le PO et les développeurs, absence d'exemple dans les spécifications, imprécisions, absence de spécifications en cas de parcours marginaux, décalage entre le vocabulaire métier utilisé dans les spécifications et le vocabulaire présent dans le code)
- La recherche de solution (difficulté à définir une stratégie d'approche du problème, difficulté de faire émerger un design applicatif, difficultés de nommage et de définition des responsabilités, navigation et réutilisation compliquées de code spaghetti)
- La vitesse de mise en conformité du code (latence entre la demande de revue et la revue, latence et incompréhension dues au mode de revue par commentaires interposés, nombre de retours en l'absence de standards d'équipe, lenteur de consensus sur les retours à apporter, traitement des retours, validation des retours, conflits de merge)
- L'intégration (dépendances externes indisponibles, absence ou instabilité de l'environnement de recette, absence de jeux de données, contrats d'interface inconsistants)
- Le testing (absence de vérifications automatisées, scénarii de tests indéfinis/injouables)
Vis à vis des activités citées ci-dessus, la section “implémentation” (écriture du code) apparaît souvent en très large surcapacité. Avec une moyenne de 50 lignes de code écrites par jour par développeur**2****, elle n’est pas le facteur limitant d’une équipe.**
I can type ~30 words per minute. My average line of code is ~10 words. The average developer delivers 10-40 lines of working code a day, depending on which study you look at. At 30 wpm, that's 3-12 minutes of typing code a day. TYPING CODE IS NOT THE BOTTLENECK. pic.twitter.com/xd92NZEC9N
— jasongorman (@jasongorman) February 15, 2020
Or, les leviers :
- Ajout de développeurs dans l'équipe
- Maximisation du temps que les développeurs passent à écrire du code
ne fonctionnent que si le goulet se trouve effectivement au niveau "implémentation".
En effet, et c’est parfois contre-intuitif, le développement logiciel est une activité qui, pour être soutenable dans la durée, demande un haut degré de cohésion et de collaboration entre les membres d’une équipe. La plupart des phases évoquées ci-dessus se basent sur la bonne communication et la capacité de l’équipe à se mettre d’accord et prendre des décisions efficacement. Or, ajouter un membre à une équipe démultiplie le nombre de relations interpersonnelles au sein de celle-ci.
Ce faisant, la communication entre les membres de l’équipe devient beaucoup plus coûteuse : se mettre d’accord et prendre des décisions demandent plus d'énergie et de temps. Ainsi, augmenter le nombre de développeurs dans une équipe dont le goulet n’est pas la phase d’écriture du code revient à accentuer ses problèmes.
À ce niveau de détail, on s'aperçoit non seulement que le goulet n'est pas celui que l'on pourrait croire mais on constate aussi que celui-ci est sollicité à mauvais escient. Le software qui est produit comporte :
- Des erreurs de programmation (il ne fonctionne pas comme le pensaient les développeurs)
- Des erreurs de compréhension (il ne fonctionne pas de la façon dont le PO l'aurait voulu)
- Des erreurs de conception (il ne répond pas correctement au besoin des utilisateurs ou aux contraintes d’architecture de son écosystème)
Ces erreurs sont trop souvent détectées en phase d'intégration, de recette ou même en production (c’est à dire une fois que le goulet a déjà été sollicité). Afin de corriger ces erreurs, le goulet devra donc être sollicité de nouveau ce qui aura pour effet de ralentir le débit global du système.
Si l’ajout de développeurs est donc rarement la solution, qu’en est-il du fait de chercher à maximiser le temps que des développeurs passent à produire du code ?
Tout comme pour l’ajout de développeurs, cette solution revient à ajouter de la capacité à la section “implémentation” qui est très probablement déjà en surcapacité. On observera alors, dans le meilleur des cas, aucun effet sur le débit global. Dans de nombreux cas cependant on observe les effets indésirables suivants :
Les développeurs sont écartés des phases de conception et de spécification. Cela impacte négativement la qualité de celles-ci et nuit à la bonne communication entre le PO et les développeurs : le nombre d’erreurs de compréhension augmente.
Les développeurs sont invités à limiter le travail en collaboration et les moments de partage. Cela impacte négativement l'émergence de standards d’équipe (mise en conformité plus lente), les réflexions concernant le design applicatif (lenteur de recherche de solution).
Les développeurs sont invités à ne pas investir “trop” de temps en refactoring. Ce qui limite la correction d’erreurs de design passées et laisse libre cours à l’entropie dans le code. Ceci entraîne des lenteurs au niveau de la recherche de solution.
Point d’étape:
- Le goulet se trouve très rarement au niveau “implémentation”
- L’ajout de développeurs augmente la capacité au niveau d’un non-goulet ayant, au mieux, aucun effet et, au pire, un effet négatif sur la section goulet existante
- Le goulet est sollicité à mauvais escient par des erreurs de programmation et des erreurs de compréhension
- Chercher à maximiser le temps que les développeurs passent à écrire du code augmente la capacité au niveau d’un non-goulet. Ainsi, tout comme le fait d’ajouter des développeurs, c’est le plus souvent inefficace et impacte de surcroît négativement la qualité des tâches traversant le système et sollicitant le goulet à mauvais escient.
Vous disiez [la pratique X] donc ?
Pour accélérer le cycle de développement il existe donc deux leviers pertinents :
- Augmenter la capacité au goulet
- Empêcher que le goulet soit sollicité à mauvais escient (réduire le nombre d’erreurs de programmation et de compréhension le traversant)
Tres Amigos / Example Mapping
L’atelier Tres Amigos vise à réunir PO, Développeur(s) et Testeur(s) autour d’une User Story (ou spécification) avant que le développement ne débute afin de s’assurer de sa compréhension par les développeurs et les testeurs. C’est l’occasion pour le développeur de poser des questions sur des cas limites qui auraient pu être oubliés (“Que se passe-t-il si l’utilisateur n’existe pas en base de donnée?”) et de se projeter quant aux briques techniques impactées. Le testeur quant à lui peut se projeter sur la testabilité de l’US et demander la mise en place des jeux de données et des bouchons nécessaires au test.
Grâce à l’Example Mapping, on passe de l’explication d’un comportement à la formalisation d’exemples concrets d’entrées/sorties attendues (ex: le comportement “Appliquer 10% de remise au prix des articles de mode” est enrichi d’exemples tels que “Le prix d’une paire de chaussures à 20€ passe à 18€ alors que le prix d’un ordinateur à 100€ reste à 100€”). Ces pratiques ont pour effet de valider la qualité des entrants avant leur passage dans le goulet pour ne pas qu’il soit sollicité inutilement. Elles améliorent la capacité des sections “compréhension du problème” et “test”.
Tests unitaires / Test-Driven Development (TDD)
Les tests automatisés permettent de vérifier que le code écrit par le développeur se comporte conformément à l’intention qu’il en avait (c’est à dire, qu’il n’a pas commis d’erreur de programmation). Les tests unitaires visent à effectuer cette vérification sur des portions très réduites de code : ces tests sont individuellement moins coûteux que les tests d’intégration, ils s'exécutent plus rapidement et permettent d’identifier plus finement la source d’une erreur. De proche en proche, ils aident le développeur à vérifier au fil de l’eau que son code se comporte correctement et ce, avant que les erreurs prolifèrent jusqu’en recette ou pire, en production.
Test-Driven Development (TDD) est une pratique qui vise à écrire un test unitaire avant même d’écrire le code testé. C’est une façon de faire qui favorise l'émergence d’un meilleur design applicatif et permet le micro-refactoring au fil de l’eau. Ainsi, en plus d’être débarrassé des erreurs de programmation, le code est plus lisible et il est plus simple de le comprendre, le réutiliser et le faire évoluer.
Pair / Mob programming
Le pair programming est une pratique qui vise à travailler à deux développeurs sur le même poste. L’un des développeurs a le rôle de “navigator”, il guide la programmation et dit à l’autre, le “driver”, qui tient le clavier ce qu’il doit faire. Le but du pattern driver/navigator est d’obliger l’idée d’un développeur à passer par le cerveau d’un autre par communication orale avant que celle-ci ne puisse se retrouver dans le code. Le pair programming provoque donc une communication au fil de l’eau entre développeurs.
Cette communication permet :
- de confronter des idées et faire émerger des standards
- d’identifier les erreurs de compréhension vis-à-vis d’une tâche avant de commencer à coder
- de proposer, en un minimum de temps, davantage de solutions potentielles au problème à résoudre
- d’écarter certaines des solutions candidates avant même de commencer à les coder
- d’identifier les erreurs de programmation au moment où elles sont écrites
- de mieux designer le code qui est écrit (il est compris par au moins deux personnes)
- de limiter le nombre de développements en parallèle (ce qui est une bonne chose lorsque le goulet se trouve au niveau de la mise en conformité ou de l’intégration)
Le mob programming ressemble de très près, dans sa philosophie, au pair programming à ceci près qu’il ne concerne plus seulement deux développeurs mais toute l’équipe ! A l’aide d’un rétroprojecteur, on projette l’écran du driver et toute l’équipe est maintenant navigator. Le mob programming, c’est du pair programming à la puissance n. On y retrouve donc, de manière décuplée les bienfaits du pair programming (voir ci-dessus) avec, en prime, une mise en conformité au fil de l’eau.
Points d’étape
- Les leviers pertinents pour accélérer le cycle de développement sont :
- soulager le goulet en augmentant sa capacité
- faire en sorte que le goulet soit sollicité à bon escient
- Pour ce faire, c’est auprès des pratiques de développement qu’il est le plus pertinent d’intervenir.
- L’atelier Tres amigos et l’Example mapping diminuent fortement les erreurs de compréhension de traverser le cycle de développement, augmentent la capacité des phases de compréhension du problème et de test.
- Les Tests unitaires diminuent fortement les erreurs de programmation de traverser le cycle de développement. Test-Driven Development favorise de surcroît l'émergence d’un meilleur design ce qui augmente la capacité de la phase de recherche de solution.
- Pair et Mob programming augmentent la capacité de toutes les phases hormis celle de l’implémentation (qui est non-goulet dans la grande majorité des cas) grâce à l’efficacité accrue de la communication dans l’équipe et l’accélération de la prise de décision.
Conclusion
Calculer le ROI de [la pratique X] dans un écosystème aussi riche qu'une équipe de développement évoluant dans un SI décennal est mission impossible. Pour un décideur, "donnez moi le ROI de [la pratique X]" est donc une façon courtoise de refuser de promouvoir cette pratique (“soft no”). Cela exprime “merci mais non merci, j’ai un mauvais pressentiment par rapport à cette proposition, je ne suis pas prêt à prendre ce risque et mettre ma crédibilité en jeu sur ce coup de poker”. Ce qui, dans le modèle logique utilisé, est tout à fait compréhensible.
Or, en adoptant un autre modèle logique du fonctionnement d'une équipe de développement, deux choses deviennent rapidement très intuitives et logiques :
- Augmenter la taille d’une équipe ou réduire toutes les activités satellites à celle de l’écriture du code est contre-productif.
- Les pratiques telles que Tres Amigos, Example mapping, TDD et Pair/Mob programming permettent d’agir précisément sur le goulet de l’équipe tout en redirigeant son effort sur les tâches porteuses de valeur.
Hâte de commencer ? Commencez par vous poser les questions suivantes :
- Où se trouve, selon vous, le goulet de votre équipe ?
- Celui-ci est-il sollicité à bon escient ?
- Que faire pour que celui-ci ne soit pas sollicité inutilement ? Quelle est l’origine du rework ? Des erreurs de programmation ? Des erreurs de compréhension ?
- Qu’est-il possible de faire pour augmenter la capacité au goulet et réduire le rework ?
A lire également :
- Theory of Constraints 101 - Tiago Forte
- The Goal - Eliyahu M. Goldratt
- The Principles of Product Development Flow: Second Generation Lean Product Development - Donald G. Reinertsen
Notes:
[1] - On se place ici dans le cadre d’une équipe en capacité de gérer la demande entrante de façon à ne pas se retrouver en situation de saturation globale.
[2] - Capers Jones - Programming Productivity