Comment j'ai compris que je faisais du "Software Crafting"

Retour d’expérience de quelques années de développement

Genèse

Cela fait maintenant plus d’une décennie que j’ai fait du développement mon métier. Mais j’ai commencé à coder bien avant cela.

J’ai commencé à créer des programmes en Basic au collège, sur ma calculette Casio (c’est fou ce que l’on pouvait faire faire avec des “goto” !). Puis, au lycée, j’ai rencontré un ami dont le père avait fait un site web (l’époque Dreamweaver et modem 56k) permettant facilement d’apprendre le C. Et je suis tombé dans la marmite. On peut dire que c’est la période où j’ai vraiment commencé à m'intéresser au code.

Quelques années plus tard, je me suis intéressé aux serveurs privés de certains MMORPG de l’époque (~ 2004/2005). Celui qui m’intéressait était principalement développé en Java (Java ? C’est quoi encore ce truc ?). Comme je voulais mon propre serveur privé, j’ai appris le Java en version accélérée (“I know Java”, Néo - Matrix). Enfin… j’ai appris la syntaxe de Java, et je codais comme en C, en procédural. C’est bien plus tard que j’ai vraiment compris le paradigme de la programmation orientée objet, et que je me suis mis à l’appliquer. N’empêche qu’entre temps, mon serveur fonctionnait avec mes modifications. Alors pourquoi aller plus loin ? Un programme qui fonctionne, c’est un programme qu’on peut oublier, non ?

La “vraie” vie

En 10 ans de développement en entreprise, j’ai quand même appris 2 ou 3 choses. En particulier, qu’il y a une vie après le dev. Si, si, je vous jure, le code que l’on écrit continue, en général, à être exécuté quelques années, et malheureusement, il ne se bonifie pas tout seul avec l’âge. Au contraire, on découvre en général de plus en plus de bugs. Bien entendu, c’est toujours la faute des utilisateurs qui font des choses que l’on n’avait pas prévues, n’est-ce pas ?

Quoi qu’il en soit, il faut se rendre à l’évidence, le code que l’on écrit doit être robuste. Il doit aussi être clair, concis, cohérent, etc., pour pouvoir y revenir facilement et le refactorer au besoin (Refactorer ? Mais ça coûte cher ça ! Ce serait quand même mieux de le faire correctement dès le début, non ?).

Alors, comment faire pour écrire du code de qualité ?

Mon approche de l’époque était surement très naïve - et je verrai surement ce billet de la sorte dans quelques années, ça s’appelle l’expérience : “c’est simple, il faut appliquer les bonnes pratiques de développement !”.
Par bonnes pratiques, j’entendais :

  • limiter le nombre de caractères par ligne (80, 100, peu importe tant que c’est fait),
  • avoir des noms de variables parlants,
  • bien documenter / commenter son code (Javadoc, mais aussi commentaires sur des bouts de code un peu complexes),
  • éviter les bugs communs (Findbugs peut aider, par exemple),
  • faire attention à avoir un couplage faible et une forte cohésion du code,
  • avoir une bonne couverture de tests,
  • et 1 milliard d’autres règles “bateau” que vous connaissez sûrement.

Mais tout cela ne me semble maintenant plus suffisant pour avoir du code de qualité. C’est juste une base.
On peut discuter du détail de certaines pratiques, mais globalement c’est bien d’avoir des guidelines comprises, acceptées, et suivies par tout le monde, qu’elles soient orales ou écrites. D’expérience, les conventions choisies dépendent des développeurs présents sur le projet, de leur philosophie, et de leur expérience. Certaines normes font débat (les commentaires par exemple). Ce n’est pas grave, l’important c’est que tout le monde, sur un même projet, “parle” le même langage, et aille dans la même direction. Et pour ça il faut faire quelques concessions parfois.

Bien entendu, au fil des projets, j’ai compris que ce n’était pas suffisant, et j’ai commencé à me poser d’autres questions. On peut écrire du code “propre”, sans que l’application soit de qualité. Si l’on prend une métaphore, ce n’est pas parce qu’une écriture est belle (au sens visuel du terme) que ce qui est écrit est intéressant et a du sens.
La suite de ce billet évoque quelques sujets de réflexion que je vous partage.

Ma méthode

Tout le monde n’apprend pas de la même manière.
Pas mal de personnes que je connais lisent beaucoup d’ouvrages techniques pour apprendre des choses, et les mettent en application par la suite. Ce n’est pas vraiment mon cas. J’apprends beaucoup mieux par de l’expérimentation et de la déduction : je fais quelque chose, je trouve un problème (ou me pose une question [philosophique]), et je cherche comment le résoudre.

La limite de cette méthode, c’est que je ne peux pas forcément me rendre compte tout seul des anomalies au moment du développement. Parfois les problèmes se posent au run, quelques semaines / mois plus tard. Parfois, un changement de besoin met en lumière un souci de conception. Ou encore, des discussions avec des collègues me font penser à de potentiels “trous dans la raquette”.

Quand un problème se présente, je réfléchis à des solutions. Si j’ai les moyens et le temps, je les expérimente, via des projets persos par exemple, pour en voir l’impact et comprendre dans quels cas elles pourraient être utiles.
Enfin, je me documente pour savoir comment “les autres” ont réglé ce problème. Et en général, on retombe sur des design patterns plus ou moins répandus. Même s’il n’y a pas une manière universelle de régler chaque problème, le nombre de solutions “acceptables” reste en général assez restreint, avec chacune ses avantages et inconvénients, bien entendu. Il serait sûrement assez cynique de dire qu’il existe déjà une solution à chaque problème, mais on se rend compte que les bonnes idées - qui font parfois des “buzz technologiques” - sont souvent tirées de concepts déjà anciens en informatique (les microservices par exemple, qui sont un cas particulier d’architectures orientées service - SOA). On les appelle autrement, on borne leur périmètre, on les adapte aux technologies et contraintes du moment, mais ce n’est pas forcément révolutionnaire. Toute la subtilité, c’est d’utiliser les bonnes idées dans le bon contexte, et de la bonne manière… tout en prenant en compte les coûts associés (formations, infrastructure, maintenance, etc.)

En testant moi-même avant de chercher une solution “toute prête”, j’apprends beaucoup, et je recommande cette pratique. Et si en plus votre idée est aussi le fruit de réflexions d’autres personnes, cela vous permet d’en avoir une vision plus complète, plus challengée… et pourquoi pas de la remettre en question. Mais faire des erreurs n’est pas grave, c’est comme cela que l’on apprend le mieux. Le tout est non seulement de les assumer, mais aussi de ne pas les reproduire (le fameux dicton “apprendre de ses erreurs”).

Tester une application

J’ai toujours “plus ou moins” écrit des tests sur mon code. Et comme plein de monde, j’ai souvent dit “nan mais c’est pas la peine de tester ça, ça se voit que ça fonctionne”.

J’ai mis longtemps avant de vraiment me poser la question “à quoi ça sert de tester son code ?”, car pour moi c‘était simple : on teste son code pour vérifier qu’il fonctionne. Là encore, c’est sûrement assez naïf. Je vous laisse méditer un peu là-dessus, et j’apporterai une explication dans la suite de cette partie.

Pendant des années, j’ai écrit mes tests une fois mon code développé, suivant le cheminement suivant :

  • j’ai un problème à résoudre (une fonctionnalité à développer par exemple),
  • je fais une conception (mentale ou écrite, suivant la taille du travail à effectuer),
  • je fais mon développement,
  • je teste manuellement, à coup de débugger si besoin,
  • j’écris quelques TU pour le code qui me semble le mériter (super objectif, non ?). Et parfois, j’écris aussi des tests pour faire passer le fameux critère de couverture de code (heureusement, la honte ne tue pas, et ce qui ne nous tue pas nous rend plus fort. Et là, j’étais hyper fort !).

Et voilà, je peux passer à la suite, un travail bien fait !

… sauf qu’il y avait quelque chose qui me dérangeait un peu : pourquoi écrire des tests sur un code que je savais correct (bah oui, je l’ai testé il y a 5 min en l’écrivant !). La seule raison valable que j’y voyais (autre que “c’est mal de ne pas faire de tests”, ou la fameuse couverture de code), c’est l’aspect automatisé des tests. C’est quand même plus confortable que de tester au débugger, et en plus, je sais si je casse quelque chose si je modifie mon code (ah tiens, on dirait le début d’une idée…).

Comme ça me dérangeait un peu, un jour je me suis dit : “Allez, je suis un fou, je vais écrire mon test en premier, et ensuite je vais faire mon code”. Et c’est comme ça que j’ai inventé le “Test Driven Development” (TDD) ! Enfin… pas vraiment. J’ai passé tellement de temps à refactorer mes tests au fur et à mesure que je faisais mon implémentation, que je me suis dit que l’idée n’était pas si géniale que ça. Et j’ai vite recommencé à écrire mes tests à la fin de mon développement.

Sauf qu’un jour, on m’a parlé de TDD (le vrai, cette fois-ci), en me disant que tout le monde devrait faire ça, car c’est la bonne manière de faire. Oui, oui, j’ai essayé, et bah non, c’est naze, j’ai perdu plus de temps qu’autre chose.
Comme je suis un minimum curieux, et que “quand même, c’est bizarre qu’on en parle comme ça alors qu’au final on ne gagne pas de temps”, j’ai fini par me documenter un peu. Et j’ai décidé de donner une nouvelle chance à cette pratique.

Avec le recul, même si je me suis amélioré dans l’exercice, j’ai continué pendant pas mal de temps à faire du “pseudo TDD”, c’est-à-dire à ne pas vraiment comprendre son intérêt, et donc le pratiquer de manière un peu bancale. Si vous pensez que la définition de TDD, c’est “écrire les tests avant le code”, alors vous avez une vision incomplète de cette pratique, comme moi à cette époque. Ce n’est pas faux, mais c’est réducteur. C’est une conséquence de la pratique, pas sa raison d’être.

Le TDD consiste à construire son application à partir de tests. Les tests représentent donc la spécification (le comportement que l’on doit développer). On écrit donc la spécification (i.e. les tests) avant le code. Et l’une des règles du TDD est d'implémenter le minimum pour que le test passe. S’il y a d’autres cas à traiter, il faut écrire des tests spécifiques. Un autre effet de bord de faire du TDD est donc d’obtenir une couverture de code proche de 100%*, par nature. Comme on écrit le minimum pour faire passer le test, la conception de l’application se dessine au fur et à mesure des cas de test. Il faut donc refactorer souvent. C’est une méthode qui est très compatible avec le développement agile.

Depuis que j’ai compris cela, j’utilise le TDD en commençant par écrire des tests fonctionnels en Gherkin (c’est ce qu’on appelle l’outside-in TDD : voiçi un très bon article sur le sujet), qui ne sont donc pas du tout liés à l’implémentation sous-jacente. En fonction de la difficulté de la fonctionnalité à produire, j’ai aussi parfois besoin de l’appliquer sous forme de TU (par exemple, si le contexte est trop compliqué à initier au niveau des tests fonctionnels pour tester un fonctionnement précis). Commencer par des tests fonctionnels permet donc d’appréhender le TDD beaucoup plus facilement, sans créer un couplage fort entre le test et le code, ce qui était mon erreur lors de mes premiers essais.

Pour résumer, je pense qu’écrire des tests permet :

  • de pouvoir refactorer souvent : de bons tests ne doivent en général pas être modifiés si l’implémentation change (mais pas la fonctionnalité),
  • de gagner du temps sur le debug : un bon test en erreur indique clairement ce qui ne fonctionne pas,
  • de documenter les fonctionnalités dans l’application : à un niveau plus ou moins technique, suivant les tests.

Architecturer une application

Un autre aspect intéressant du développement est l’architecture. Depuis mes débuts en Java, j’ai appris qu’il fallait faire du développement en couches : le fameux MVC, ou n-tiers, ou toute autre déclinaison de ce principe de couches (architectures en oignon). Du coup, j’ai mis longtemps à me poser les questions de “pourquoi ces couches ?” et “est-ce la seule manière de faire ?”.

J’ai appris beaucoup de choses de manière très scolaire, comme le fait de faire des interfaces partout “au cas où l'implémentation change”. Sur le principe Ok, mais quand c’est sur des services qui ne font que de l’algo, par quoi ça risque d’être remplacé ? Mais c’est pas grave, c’est comme ça qu’il fallait faire, donc je l’ai fait… pendant un moment.

Au cours de mes projets, j’ai commencé à écrire de moins en moins d’interfaces “juste-au-cas-où”, voire même de ne plus en faire du tout là où je n’en avais pas explicitement besoin. Et personne n’est mort, et l’internet fonctionne toujours. Comme quoi…
Alors peut-être que cela va faire sauter certains d’entre-vous au plafond, mais au final, est-ce qu’on ne fait pas trop souvent d’interfaces pour se donner bonne conscience et faire joli (comme la salade avec un burger) ?

Et bien, je me suis mis à reconsidérer les interfaces il y a quelques mois, quand je me suis intéressé à l’architecture hexagonale. Je ne vais pas expliquer ici en quoi ça consiste, mais simplement rappeler, de manière très simplifiée, un de ses principes de base : on a un cœur d’application (core ou domain) représenté par un modèle, qui expose des fonctionnalités (exposition), et qui dépend d’autres éléments du système (infrastructure). Du coup, le domaine traduit ce dont il a besoin sous forme d’interfaces, pour que l’implémentation, présente dans l’infrastructure, soit injectée au runtime (inversion de dépendance). La raison d’être de cette couche infrastructure est justement de contenir des choses qui sont variables (base de données, services externes, etc.). L’intérêt d’utiliser ce type d’architecture est qu’il est possible de développer et tester toute l’application, sans connaître à l’avance l’infrastructure. Et c’est logique : si je fais une application qui gère des contacts, ai-je vraiment besoin de savoir si je vais utiliser une base relationnelle, NoSQL, ou fichier plat pour la persistance de mes données ? Et là, les interfaces trouvent tout leur sens, on ne les écrit plus simplement “car c’est une bonne pratique !”.

Quelle que soit la manière de “ranger” votre code, il est important de bien connaître les avantages et inconvénients de chaque méthode. Sur certains micro-services (eh oui, le mot est lancé !) de quelques dizaines de lignes de code, on pourrait développer n’importe comment : pas de couches, d’interfaces, de packages bien découpés etc. Et alors ? Ca fonctionnerait quand même, pour un temps de dev très court. Mais il faut penser aux personnes qui vont lire et/ou reprendre votre code.

N’appliquez pas “bêtement” certaines règles parce qu’on vous dit que c’est bien. Essayez de comprendre à quoi elles servent, et faites-vous votre avis.

Conclusion

Dans ce billet, je n’ai pas employé une seule fois le mot “craft”, et pour une bonne raison : il se trouve que ma manière de développer (rechercher comment produire efficacement, et surtout qualitativement, privilégier la maintenabilité par rapport à la réponse instantanée à un problème, en mode “quick and dirty”, etc.) se rapproche fortement de la philosophie des “Software Crafters”, dont voilà un résumé des principes :

  • Well-crafted Software : fournir du code de qualité, afin d’assurer le meilleur fonctionnement possible de l’application
  • Steadily adding value : toujours faire en sorte que les développements apportent quelque chose de valeur. Par exemple, on ne remanie pas toute une application simplement pour le plaisir d’utiliser le dernier framework à la mode.
  • A community of professionals : la qualité du code passe par la qualité des relations. Former les gens, partager et challenger ses idées, assumer la responsabilité de ses actions, tout cela fait d’une personne un professionnel dans son métier (et pas seulement le craft d’ailleurs)
  • Productive partnership : on ne fait pas que collaborer avec ses clients, on a un but commun. On partage, on ne cache rien et on n’embellit pas les choses. En gros, on se fait confiance.

Je ne fais pas du craft parce que c’est beau ou à la mode, mais parce que ça répond à de vraies problématiques. Je fais du craft car j’applique naturellement un ensemble de pratiques associées au craft, qui me semble utiles et nécessaires. Je ne me suis pas levé un matin en me disant “à partir d’aujourd’hui, je fais du craft !”. C’est plutôt un cheminement d’idées, qui m’ont fait passer de “Comment répondre rapidement au besoin de mon client ?” à “Comment proposer une solution pérenne au besoin de mon client ?”. En d’autres termes, comment augmenter la qualité de ce que je produis.

Et la qualité, ce n’est pas forcément d’utiliser tous les derniers frameworks / solutions à la mode, ou de faire de l’over-engineering. La qualité nécessite de se poser les bonnes questions, et réaliser un travail en choisissant une stratégie adaptée aux risques (changement d’infra, de périmètre, de développeurs, etc.). Et cela s’acquiert principalement par l’expérience, et l’envie de tester de nouvelles méthodologies pour faire son choix en connaissance de cause.

Est-ce que le craft me permet de développer plus rapidement ? Je ne pense pas. Mais ce n’est pas forcément plus long, et surtout, je sais que mon code est beaucoup plus robuste qu’avant. Il contient moins de bugs (c’est rare de ne jamais en faire), et j’aurai donc moins besoin d’y revenir. Cette expérience me permet aussi de “sentir” les contextes compliqués, et prendre mes dispositions avant de me retrouver dans une “usine à gaz” de code.

Et vous ? Pensez-vous que le craft de logiciel soit quelque chose à défendre ? D'expérience, plusieurs personnes pensent qu’ils n’ont pas le niveau pour faire du craft, ou alors que le craft est inutile. Etes-vous d’accord avec ces affirmations ? Merci de vos commentaires !


*Attention, 100% de couverture de code ne veut pas dire que le code est parfaitement sûr. Il peut tout simplement manquer des assertions (on est bien passé dans le code, mais on n’a pas vérifié l’output),