Trop long ; pas envie de tout lire (TLDR)
Afin d’améliorer la qualité de notre code en le restructurant, il y a un pré-requis :
- avoir un bon harnais de test.
et 2 stratégies :
- Appliquer chaque règle de “clean code” une par une sur l’intagralité du code avant d’en changer.
- Suivre l’arbre de vos envies de modification et commencez par les feuilles. C’est la technique du mikado.
La restructuration de code : bonnes pratiques et stratégies
J’aime beaucoup le beau code, l’agilité et le logiciel libre. Depuis plus de 20 ans, j’aide les équipes et les développeurs à produire un code plus maintenable, plus utile et surtout à produire ce code sereinement, paisiblement, sans le stress de la productivité ou l’angoisse des bugs.
Dans cet article, je vais vous parler de l’art du remaniement de code.· Le refactoring est l’une des 3 étapes du TDD (Test Driven Development).· Pour un bon développeur, c’est environ 1/3 de son temps de codage.· Toute la difficulté pour un développeur est de comprendre le métier et de le traduire dans un langage lisible par un ordinateur, mais aussi par les autres développeurs.· Dans le développement de logiciels, l’industrialisation a été faite depuis de nombreuses années, nous avons des compilateurs pour prendre le code lisible par les humains et produire du code exécutable par un ordinateur.
En résumé, le métier explique ses besoins au développeur, le développeur écrit du code lisible par d’autres développeur, le compilateur traduit ce code en langage machine et l’ordinateur fait ce que le métier voulait qu’il fasse.
Le code écrit par le développeur doit présenter de nombreuses propriétés :
- lisible et compréhensible par tous les développeurs,
- expliquer clairement tous les besoins du métier,
- être compilable pour être traduit en langage machine,
- être facile à mettre à jour,
- être facile à maintenir.
Le refactoring, c’est l’art de réécrire le code pour atteindre toutes ces propriétés.
Le problème le plus important quand on fait du refactoring, c’est la multitude de règles plus ou moins antinomiques, les bonnes et mauvaises odeurs, les bonnes et mauvaises architectures, le bon et le mauvais code. Quand un développeur lit du code, il repère beaucoup de choses à changer et quand il fait le changement, il repère encore plus de choses à changer et ainsi de suite. À la fin, le développeur est facilement perdu dans la liste de toutes les choses à modifier. Dans la suite de cet article, je vais énumérer quelques règles, et 2 stratégies pour réaliser du refactoring.
Le programme parfait
La première stratégie que je veux partager avec vous, est directement tirée de mon activité de coach agile. Lorsque j’anime une réunion, il est très important de concentrer toutes les personnes sur un seul objectif simple, comme par exemple ne collecter que les faits de la dernière rétrospective ou ne collecter que les nouvelles idées. Il est important de ne pas se perdre dans notre pensée arborescente.
Pour suivre cette idée dans un processus de refactoring, je prends une règle pour avoir un programme parfait, et je fais tous les changements possibles dans mon code pour suivre cette règle avant de changer de règle.
En XP (Extreme programming ou programmeurs de l’extrêmes en français), le programme parfait suit 4 règles :
- il est testé,
- il n’y a pas de code dupliqué,
- il est Lisible,
- et simple.
Voyons ensemble et en détail ce que sont ces 4 règles du programme parfait.
Testé
C’est la règle la plus intuitive, du code testé c’est du code testé par des tests automatisés. Mais, il n’est pas si courant d’avoir une très bonne couverture des tests.
Avant tout refactoring nous devons avoir un bon harnais de test, pour vérifier cela, je vous recommande d’utiliser un outil pour vérifier la couverture des tests. Mais, attention, tous ces outils ne peuvent vérifier que la couverture du code et non la couverture fonctionnelle. Vous devez vous rappeler qu’il est possible d’avoir une couverture technique de votre code complète et seulement une couverture partielle du métier.
En fait, il faut se rappeler, et c’est le sujet d’autres articles, qu’il est possible d’écrire un bon harnais de tests automatisés pour faire le refactoring sans stress. Je connais 3 stratégies pour le faire :
- les tests du canari,
- les tests générés,
- et le harnais de tests bleu-vert.
Pas de code dupliqué
Pour éviter le code dupliqué, il n’y a pas beaucoup de solutions de bas niveau :
- Factorisation dans une classe supérieure c’est une stratégie d’héritage.
- Factorisation dans une petite classe utilisée comme attribut, c’est une stratégie de composition.
- Factorisation dans une fonction.
Mais ce n’est qu’une solution technique, dans de nombreux cas, il est nécessaire d’avoir un autre niveau d’étude. Robert C. Martin, a introduit dans son article de 2000 “Design Principles and Design Patterns”, l’acronyme mnémotechnique [SOLID] (https://en.wikipedia.org/wiki/SOLID). J’utilise ces cinq principes de conception pour éviter la duplication du code. Juste pour mémoire :
- Le principe de responsabilité unique (Single-responsibility) : chaque classe ne doit avoir qu’une seule petite responsabilité.
- Le principe d’ouverture-fermeture (Open–closed) : Pour ajouter des fonctionnalités dans notre code, nous voulons faire des modifications dans une partie minimale du code et non dans toutes les fonctions et classes.
- Le principe de substitution de Liskov (Liskov substitution) : Peut se résumer en : éviter les instances-of ou les patterns similaires.
- Le principe de ségrégation des interfaces (Interface segregation) : Faire des interfaces spécifiques à chaque client.
- Le principe d’inversion de dépendance (Dependency inversion) : utiliser les interfaces et non l’implémentation réelle, la “mauvaise odeur” associée c’est l’utilisation d’un framework de mock.
Lisible
Les notions de lisibilité sont à première vue très personnelles, mais les recherches en neurosciences ont prouvés beaucoup de choses.
Tout d’abord, nous avons une toute petite mémoire pour la compréhension en première lecture, nous utilisons cette mémoire très très fréquement, lorsque nous lisons, lorsque nous voyons des images, etc. Cette mémoire est utilisée pour construire une pensée dans un flux. Mais cette mémoire qui ne coûte rien ne peut enregistrer que 7 ou 8 instructions. Pour construire une pensée plus complexe, nous devons utiliser des mémoires plus profondes, qui consomment beaucoup plus d’énergie et sont beaucoup lentes. La première conclusion de ceci, nous devons avoir des fonctions avec moins de 8 instructions. Cela explique de nombreuses règles classiques de lisibilité :
- pas de fonction avec plus de 8 lignes,
- limiter le nombre d’attributs dans les classes,
- pas plus de 3 paramètres par fonction,
- pas de nombre magique ou de chaîne magique.
Une autre règle que j’affectionne particulièrement, c’est de banir les commentaires. J’imagines déjà vos têtes surprises. Oui, pour moi, un indicateur de non lisibilité c’est la présence de commentaires. Prenons par exemple :
...
}
// add footer lines
result += "Amount owed is " + totalAmount + "\n";
result += "You earned " + frequentRenterPoints + " frequent renter points";
return result;
}
On peut aisément remplacer le bloc de 2 lignes avec le commentaire par une méthode retournant les lignes de pied de page.
...
}
result += addFooterLine();
return result;
}
public String addFooterLine() {
footerLines = "Amount owed is " + totalAmount + "\n";
footerLines += "You earned " + frequentRenterPoints + " frequent renter points";
return footerLines;
}
D’un autre côté, le rôle de l’ingénieur en dévelopemeent c’est de traduire le besoin métier dans un langage formel, le langage de programation. Pour s’assurer que tous les acteurs se comprennent bien, il est important d’utiliser le même vocabulaire dans le code et dans le métier. Je me rappel il y a quelques temps d’avoir une variable “niveau de nomenclature” dans mon code, mais me souvenir qu’elle correspondait à un “rayon” dans le métier demandait un certain effort. Utiliser le même vocabulaire dans tous les documents y compris le code, c’est ce que l’on appel l’ubiquitous language
Enfin, connaissez-vous la compléxité cyclomatique ? Comme tout le monde vous en avez entendu parlé, sans trop savoir ce que c’est ? Alors je vais vous montrer un exemple, prenons le pseudo code suivant :
read A
if A > 7
B = A
else
B = 12
Il est possible de construire le graphique des instructions suivant :
Pour aller de la case Start
à la case End
il y a exactement 2 chemins possibles. Un sur la droite, un sur la gaucche, et bien la complexité cyclomatique, c’est le nombre de chemin possible pour aller de l’entrée à la sortie. Ici c’est donc 2. Très bien, mais je ne me vois pas modéliser chaque fonction sous forme de graph. Effectivement, ce serait trop long. Il est possible d’utiliser une approximation, le nombre d’indentation du code, ici 2, l’indentation pricipale correspondant au coeur de la méthode et une indentation de plus pour aller dans la condition.
Réduire la complexité cyclomatique est très importante pour que tout relecteur puisse se faire une image memtale de l’algorithm le plus facilement possible.
En conclusion, pour obtenir du code lisible, j’ai 5 règles toute simple :
- supprimer les commentaires,
- limiter le nombre de lignes (maximum 8 pour les méthodes et les fonctions, maximum 200 pour les les classes et les fichiers),
- réduire le nombre d’arguments de mes fonctions (maximum 3),
- supprimer les nombre et les chaine de caractères magiques,
- réduire la complexité cyclomatique (maximum 3 par fonction).
Simple
Beaucoup de gens nous explique qu’il faut avoir le code le plus simple possible pour être maintenanble et évolutif. C’est vraie, mais à mon avis, il y a 2 voix de la simplicité à ne pas négliger. Cest 2 voix sont résumé par les 2 acronymes suivants :
- YAGNI : You ain’t gonna need it
- KISS : Keep it Simple Stupid
Le premier rappel qu’il ne faut pas anticiper les besoins non exprimé par le métier. N’implémentez jamais de fonctionalités en vous disans “Nous en aurons certainement besoins plus tard”. MAis attention, utiliser cette règle sans avoir de la restructuration de code permanente, ni d’intégration continue avec une base solide de tests unitaire automatique va probablement vous ammener à un code totalement désorganisé nécessitant une réécriture importante, ce que l’on appel courrement la dette technique.
Le deuxième acronyme nous rappel qu’un code techniquement simple apporte les mêmes propriétés qu’un système simple :
- Il sera plus simple à comprendre, réparer et entretenir
- Il aura moins de points faible, comme en plomberie lorsque l’on dit qu’une soudure c’est une fuite potentielle.
Mikado
Rappelez-vous ce jeux avec tout pleins de petites baguettes de bois dans lequel il faut en prendre une sans faire bouger les autres. Et bien, comme dans ce jeu, nous allons chercher à modifier le code sans jamais le casser. En faisant de toutes modifications sans jamais avoir à faire bouger le reste. C’est une tache difficile, principalement parceque nous avons une pensée arborescente. Je me rappel de mon dernier bricolage, objectif changer l’interrupteur du garage, première étape trouver un tournevis, en ouvrant la caisse à outils je me rappel que ça fait des mois que je dit qu’il faudrait huiler les charnières, je décide d’aller une burette d’huile, en démarrant la voiture je trouve qu’elle fait un drôle de bruit, je décide de regarder le moteur … Et le lendemain, obligé de prendre le vélo pour aller travailler car mon moteur est encore démonté. Pour arriver à suivre notre pensée arborescente et faire le choses dans le bon ordre sans en oublier, nous allons utiliser quelques postits ou un outils de “mind mapping”.
Démarrez avec votre première idée de restructuration puis ajouter les actions que vous devez faire avant, par exemple :
- Remplacer ces if / else par un switch / match sur les valeurs d’un enum,
- pour faire ça, je dois ajouter l’enum et remplacer les 3 constantes par les valeurs de l’enum
- mais avant ça je doit déclarer l’énum
- et changer le test “blabla”
- pour faire ça, je dois ajouter l’enum et remplacer les 3 constantes par les valeurs de l’enum
Puis je me dit que je peux commencer par ne changer qu’une des constantes, la moins utilisé, ce qui réjoute une étape intermédiaire dans mon arbre.
Après ça vous pouvez commencez par vous occupez des feuilles de l’arbre, ces modifications qui ne demandent pas d’autres modifications avant. N’oubliez pas de fermer ou supprimer les feuilles terminés ce qui vous fera apparaitre les prochaines étapes de votre travail. Un des avantage de cette technique, c’est que vous pouvez enregistrer votre travail à chaque fermeture de feuille, rentrer chez vous et ne reprendre le travail que le lendemain. Et à chaque commit, vous pouvez mettre en production.
Il n’est pas nécessaire d’énumérer toutes les étapes du refactoring, en revanche à chaque fois que vous vous dite : “Je devrais faire ça avant” ou “il faudrait aussi faire ça” ajoutez donc un nœud dans l’arbre.
À chaque modification de l’arbre, vous pouvez suivre les étapes suivantes :
- “revert” du travail en cours
- choix d’une feuille de l’arbre
- réaliser le changement
- enregistrer le changement (
git commit
) - fermer la feuille
- boucler à l’étape 2
À emporter
En conclusion, avec un bon harnais de test, une liste de bonnes pratiques pour produire du code propre et lisible et soit l’une des 2 stratégies présenté au dessus, vous pouvez restructurer votre code pour produire un code d’une grande qualitée.
N’hésitez pas à me partager vos retours d’expériences en la matière, vous trouverez tous les moyens de me contacter sur la page À propos.
Merci d'avoir pris le temps de lire ce texte. Vous pouvez soutenir l'écriture de ces billets et la réalisation des livecoding par de nombreux moyens. Mais le plus beau moyen de me remercier est de simplement partager ce texte autour de vous.
Sauf mention contraire, tout le contenu de ce site est sous licence