Techniques de Refactorisation de Code Qui Marche Vraiment
Résumé
La refactorisation améliore la qualité du code existant sans ajouter de fonctionnalités. Elle rend le code plus facile à maintenir, plus lisible, et plus facile à tester. Voici les vraies techniques qui marchent: extract method, composing, et branch-by-abstraction. Comment faire sans casser la prod, et quand l'ignorer complètement.
Les techniques de refactorisation de code que tu dois vraiment connaître: extract method, composing methods, et branch-by-abstraction. Ce qui est moins clair c'est quand les utiliser, comment savoir quand arrêter, et ce qui vraiment indique qu'une codebase a besoin d'un nettoyage versus une réécriture complète. Voilà ce qu'une année de travail sur un système legacy de recommendation m'a enseigné.

Les signaux qui disent que la refactorisation est vraiment nécessaire
Ce n'est pas chaque fonction qui a l'air dégueulasse qui mérite d'être touchée. Trois signaux qui valent vraiment le coup qu'on s'arrête dessus:
Duplication qui revient partout. Si tu écris la même transformation de données dans trois contrôleurs différents, ce n'est pas une question de style. C'est une bombe à retardement. Un changement en amont veut dire trois endroits à mettre à jour, et tu en oublieras un. La première fois ça passe. La deuxième fois tu te rappelles. La troisième, trop tard.
Un changement qui casse des trucs pas liés. Si fixer un bug dans le flux de paiement touche à la façon dont les profils utilisateurs s'affichent, le couplage est trop serré. La codebase traite des concerns séparés comme un seul bloc. C'est le signe qu'il est temps de séparer les responsabilités.
Les nouveaux devs ne trouvent pas leurs marques en moins de 30 minutes. C'est un signal qu'on sous-estime. Si tu ne peux pas expliquer ce qu'un fichier fait sans le lire en entier, il fait probablement trop de choses à la fois. Chez OVHcloud, on a commencé à mesurer le temps d'onboarding. Les fichiers qui prenaient plus de 20 minutes à expliquer finissaient dans le backlog de refactorisation.
Asymétrie de churn du code. Lance git log --stat et regarde quels fichiers sont touchés le plus souvent. Si les mêmes 3 fichiers représentent 70% de tes commits, c'est un signal. Beaucoup d'agitation plus haute complexité égale le 80/20 à cibler en priorité. Ces fichiers c'est le cœur qui a besoin d'une transplantation.
Skipe la refactorisation si: le code marche, personne le touche, et tu n'as pas de raison immédiate de l'étendre. La Boy Scout Rule (laisser le code plus propre qu'on l'a trouvé) c'est un bon principe, mais ne nettoie pas un terrain de camping que tu n'utilises pas.
Extract Method: la technique que tu vas utiliser 80% du temps
Extract Method c'est sortir un bloc de logique meaningful d'une longue fonction et lui donner un nom. Ce nom, c'est la documentation. C'est simple, c'est efficace, et c'est applicable à peu près partout.
Avant:
def process_order(order):
# validate
if not order.get('user_id'):
raise ValueError('Missing user_id')
if order.get('amount', 0) <= 0:
raise ValueError('Invalid amount')
# apply discount
if order.get('coupon') == 'LAUNCH20':
order['amount'] = order['amount'] * 0.8
# save
db.save(order)Après:
def process_order(order):
validate_order(order)
apply_discount(order)
db.save(order)
def validate_order(order):
if not order.get('user_id'):
raise ValueError('Missing user_id')
if order.get('amount', 0) <= 0:
raise ValueError('Invalid amount')
def apply_discount(order):
if order.get('coupon') == 'LAUNCH20':
order['amount'] = order['amount'] * 0.8La version "après" c'est plus long, c'est vrai. Ça veut pas dire moins bon. C'est meilleur. Chaque fonction a maintenant une raison d'exister, une responsabilité unique. Et quand la logique de discount change (et ça change toujours), tu sais exactement où aller.
À faire si: la fonction originale fait plus que 20-25 lignes, ou si tu te retrouves à écrire un commentaire comme # validate pour expliquer ce qu'un bloc fait. Ce commentaire c'est déjà un nom de fonction qui attend juste d'être extrait. Quand tu écris # apply discount au-dessus d'un bloc, tu as déjà nommé la méthode, tu as juste pas encore sortie du code.
Une règle pratique: si tu as l'envie d'ajouter un commentaire inline qui explique ce que les cinq lignes suivantes font, c'est un bon candidat pour l'extraction. Les commentaires sont un debt: si tu dois expliquer, c'est que le code n'est pas assez clair.
Composing Methods: quand Extract Method ne suffit pas
Composing va plus loin. Au lieu de extraire une méthode, tu décomposes une classe entière ou un module en composants plus petits et focalisés. C'est ce qu'il te faut quand un fichier a grandi à 600 lignes et sert cinq purposes pas liées.
Le pattern: identifier les responsabilités distinctes, donner à chacune sa propre classe ou module, les assembler à un niveau plus élevé. Martin Fowler appelle ça transformer une classe dégonflée en un cluster de colaborateurs. Chacun fait son truc, et ils se parlent via interfaces claires.
En pratique, la partie hard c'est pas les mécaniques. C'est de trouver où une responsabilité finit et l'autre commence. Une heuristique utile: si tu peux décrire ce qu'une fonction ou classe fait sans utiliser le mot "et", elle a probablement une seule responsabilité. Si tu dois dire "et", divise-la. "Cette classe gère les utilisateurs et envoie les emails" = deux classes.
Une autre façon de trouver les coutures: regarde les imports en haut du fichier. Si un module importe depuis six domaines pas liés (database layer, email service, logging, payment client, reporting), c'est qu'il en fait trop. Chaque cluster d'imports c'est un candidat pour son propre module. Les imports racontent l'histoire des dépendances cachées.
Composing methods aide aussi à la testabilité. Une classe de 400 lignes c'est dur de la unit-tester. Quatre classes de 100 lignes avec des interfaces claires c'est beaucoup plus facile à mocker et isoler. Plus les unités sont petites, plus elles sont testables.

Branch-by-Abstraction: pour refactor en restant déployé
Celle-là c'est plus architecturale, mais c'est la technique qui rend la refactorisation grande taille survivable dans les environnements prod. C'est comment tu refuses le choix binaire entre "déployé cassé" et "pas déployé du tout".
L'idée: au lieu de faire un swap du big-bang d'une implémentation à l'autre, tu introduis une couche d'abstraction (généralement une interface ou protocol) autour du code que tu veux changer. L'ancienne implémentation et la nouvelle satisfont toutes les deux l'interface. Tu peux shipper la nouvelle implémentation derrière un feature flag, lancer les deux en parallel, et retirer l'ancienne une fois que la nouvelle est stable.
Pourquoi c'est important pour les side projects: si tu es en train de rebuilder une intégration de paiement ou migrer un ORM, branch-by-abstraction veut dire que tu peux garder la version actuelle qui marche en prod pendant que la nouvelle cuit. Pas de "refactoring branch" qui drifts de sync avec main pendant trois semaines.
Le coût: plus de setup upfront pour définir une interface propre. Ça vaut le coup quand le truc que tu remplace c'est risqué (auth, billing, data access) et tu peux pas te permettre une fenêtre full outage. Quand le truc que tu fais touche à l'argent ou à l'identité, branch-by-abstraction c'est pas du luxury. C'est de la prudence.
Un exemple concret: on avait un legacy event-tracking module qui écrivait directement dans une table PostgreSQL. Passer à un pipeline Kafka basé veut dire toucher 40+ call sites. Avec branch-by-abstraction, on a introduit une interface EventTracker, on a changé l'implémentation derrière un flag, on a lancé les deux targets en parallel pendant deux semaines, puis on a supprimé l'ancienne. Zéro downtime, zéro déploiement d'urgence.
Simplifier les Conditionals: le cleanup qu'on sous-estime
La logique conditionnelle c'est où la complexité se cache. Une fonction avec cinq if imbriqués c'est plus hard à tester, plus hard à lire, et plus hard à étendre que cinq conditions distinctes exprimées comme guard clauses ou polymorphism. Les nested if c'est comme des poupées russes de la confusion. Une fois que tu as trois niveaux d'imbrication, c'est déjà trop tard.
Remplace les conditionals imbriquées avec des guard clauses:
Avant:
function getDiscount(user) {
if (user) {
if (user.isActive) {
if (user.plan === 'pro') {
return 0.20;
} else {
return 0.05;
}
} else {
return 0;
}
} else {
return 0;
}
}Après:
function getDiscount(user) {
if (!user) return 0;
if (!user.isActive) return 0;
return user.plan === 'pro' ? 0.20 : 0.05;
}La version guard-clause c'est plus court et plus facile à checker mentalement. Le happy path n'est pas indented quatre fois profond. Chaque guard clause dit "si cette condition n'est pas remplie, sort". C'est clair, c'est direct.
Utilise guard clauses à chaque fois que tu as une condition qui devrait sortir tôt. C'est plus lisible et ça rend le test du behavior principal plus direct. Au lieu de tester tous les chemins imbriqués, tu testes les early exits et le cas nominal.
Le testing avant de refactor
La refactorisation sans tests c'est s'envoyer des balles. Tu dois avoir des characterization tests d'abord: les tests qui capturent le comportement exact actuel du code. Ces tests c'est pas des tests idéaux. C'est des tests de "voilà ce que le code fait maintenant, même si c'est un peu bizarre". L'idée c'est de documenter le comportement actuel, même s'il est imparfait.
Pourquoi? Parce que sans eux, refactoriser c'est deviner: tu peux casser quelque chose et ne pas le savoir jusqu'en prod. Pire, tu peux casser quelque chose dans un cas limite que tu n'utilises jamais dans ta tête mais que les utilisateurs utilisent tous les jours. Les characterization tests te protègent des surprise déplaisantes.
L'ordre c'est: write characterization tests (de sorte que le code passe tel qu'il est), refactor, fais passer les tests. Les tests c'est ta sécurité. Sans eux, une refactorisation est juste un rearrangement délicat qui peut introduire des bugs silencieux. Tu peux refactoriser de jour et casser quelque chose sans le savoir.
Quand arrêter
La refactorisation se termine quand:
Le code est lisible par quelqu'un d'autre en une passe. Pas deux passes. Une.
Les changements sont testés et le test passe. Toujours.
Tu n'as pas d'autres "et" à éliminer des descriptions. Les responsabilités sont séparées.
La prochaine personne qui touche ce code ne va pas jurer contre toi, ou du moins va jurer moins.
Ça ne veut pas dire parfait. Ça veut dire assez bon pour que tu puisses revenir dedans sans mal de tête. Ça veut dire bon-enough-pour-vivre-avec. Et c'est amplement suffisant.
La refactorisation n'est jamais terminée. C'est un processus continu. Chaque fois que tu touches du code, tu as une chance de le laisser un peu plus propre. Pas radical. Pas parfait. Juste un peu plus propre. C'est comme ça que les codebases restent maintenables à long terme.
La route à suivre
Tu as maintenant les techniques de base: extract method pour les fonctions qui font trop de choses, composing methods pour les classes/modules dégonflés, branch-by-abstraction pour les changements risqués en prod, et guard clauses pour simplifier les conditionals. Combine-les. Mesure ton progrès par la capacité à comprendre du code, pas par les lignes que tu as changées. Un dev nouveau devrait pouvoir orienter en 30 minutes. C'est ta métrique.