Refactorisation sans risques : Comment extraire des modules en préservant le comportement
Lors du transfert d’un module d’un monolithe vers un service séparé, l’objectif principal est de préserver le comportement tel quel, bugs connus et particularités non documentées inclus. Les tentatives de « nettoyer » le code à ce stade mènent souvent à des régressions. Nous vous montrons comment éviter les pièges grâce à une stratégie de changements minimaux.
Pourquoi l’approche « ennuyeuse » vous protège des régressions
Les systèmes de production ont un contrat implicite forgé par des années d’exploitation : intégrations clients, gestion d’erreurs, délais de temporisation et adaptations aux API externes. Ce contrat n’est pas documenté — il est enfoui dans les logs, les commits et l’expérience de l’équipe. La refactorisation ne doit pas altérer le comportement, même s’il inclut des bugs. Par exemple, si l’ancien code renvoie une chaîne vide au lieu de null, le changer cassera les clients qui se sont déjà adaptés à cette logique. Principe clé : l’extraction change la forme mais préserve la sémantique. Toute déviation du comportement original transforme la refactorisation en une mise en production aux conséquences imprévisibles.
Un type de changement à la fois : frontières avant structure interne
Décomposez les tâches en étapes :
- Étape A : Extraire le module en préservant le contrat (HTTP, queue, bus) et introduire un drapeau de fonctionnalité.
- Étape B : Mettre à jour la pile, les bibliothèques ou le style de code.
- Étape C : Corriger les bugs.
- Étape D : Optimiser les performances.
Mélanger les étapes dans une seule PR garantit des diffs illisibles, des rollback laborieux et des disputes sur « est-ce un bug ou une nouvelle fonctionnalité ? ». À la première étape, enveloppez l’ancien code : nouvelle frontière de service, logique ancienne à l’intérieur. Par exemple, lors de l’extraction d’un module de routage, conservez la fonction route() originale mais enveloppez-la dans un point de terminaison HTTP. Cela assure un comportement identique, même si le code paraît « laid ».
Drapeau de fonctionnalité : Votre interrupteur d’urgence
Faire tourner l’ancien et le nouveau code en parallèle en production nécessite un interrupteur contrôlé via config ou variables d’environnement. Cela offre :
- Rollback instantané sans redéploiement en cas d’incident.
- Déploiement progressif sur une portion de trafic (déploiement canari).
- Conservation à long terme de l’ancienne implémentation pour comparer les comportements.
Critiquement, l’interrupteur doit fonctionner sans redémarrer le service. Sinon, un incident hors heures se transforme en appel d’urgence au lieu d’un simple ajustement de config. Implémentez le drapeau au niveau de la logique métier, pas d’infrastructure — pour éviter des dépendances complexes sur les orchestrateurs.
Copiez le comportement, ne le réécrivez pas
Règle pour une extraction sûre : copiez les fichiers « tels quels », en changeant seulement ce qui est nécessaire pour la construction. Exemple concret — déplacer le routage d’un actor vers une fonction pure :
object UpdateRouting {
def route(update: Update): Either[RoutingError, RouteTarget] =
if (update.message.exists(_.chat.isGroupOrSupergroup)) Right(GroupFlow)
else if (update.callbackQuery.isDefined) Right(CallbackFlow)
else Right(PrivateFlow)
}
Lors du mappage des modèles, préservez même les détails subtils. Si l’ancien code convertissait un channelId manquant en chaîne vide, faites de même :
def toBusMessage(in: PlatformInMessage): BusIncoming =
BusIncoming(
correlationId = newCorrelationId(),
channelId = in.channelId.getOrElse("")
)
Tout changement sémantique (comme remplacer une chaîne vide par null) rompt le contrat avec les autres services. Votre objectif : comportement identique bit pour bit, même si le code paraît daté.
Ce qu’il faut absolument éviter pendant l’extraction
Tant que le drapeau de fonctionnalité est actif, évitez :
- Nouvelles bibliothèques. Chacune ajoute des dépendances transitives et une surface d’attaque. Les mises à jour de pile sont une tâche séparée.
- Changements de style de code. Mélanger l’ancien code métier avec les nouveaux wrappers est le coût temporaire de la sécurité.
- Correction des bugs découverts. Loggez un ticket et traitez après stabilisation. Sinon, chaque incident sera imputé à la refactorisation.
- Optimisations « pendant qu’on y est ». Profilage et accélération sont un cycle séparé.
Ces restrictions peuvent sembler excessives, mais six mois plus tard dans git blame, vous verrez une histoire claire : « module extrait, bus connecté », pas « tout réécrit ».
Tests : Verrouiller le contrat, pas les KPIs
Les tests unitaires sur les fonctions pures (routage, mappage) doivent capturer le comportement avant l’extraction. Un test vert post-extraction garantit une logique inchangée. Pour les scénarios d’intégration (réseau, queues, DB), utilisez des tests E2E, mais gardez leurs limites en tête :
- Les mocks des systèmes externes perpétuent des hypothèses erronées du code.
- Les API réelles ont souvent des particularités subtiles (comme un préfixe
/api/) non couvertes par les tests.
Incluez toujours une exécution manuelle dans un environnement de staging proche de la production. L’automatisation ne remplace pas les vérifications en conditions quasi réelles.
La croissance du code n’est pas un bug, c’est une étape du processus
Après la première étape, le volume de code gonfle souvent : adaptateurs, drapeaux de fonctionnalité, duplication monolithe. C’est normal — vous payez pour des rollback sûrs et des déploiements échelonnés. Le nettoyage et la réduction viennent à l’étape deux, une fois la frontière stable, l’ancien code supprimé et les tests confirmant la correction. N’optimisez pas en avance — assurez-vous d’abord que ça marche.
Points clés
- Objectif d’extraction — transférer le comportement « tel quel », pas une amélioration globale.
- Drapeau de fonctionnalité obligatoire pour rollback sûr sans déploiement.
- Tests verrouillent le contrat avant que l’extraction commence, pas après.
- Interdiction des changements parallèles sur pile, corrections de bugs et optimisations.
- Le code peut grossir — c’est partie de la stratégie, pas une erreur.
Refactoriser du code legacy n’est pas le lieu d’expérimentations. Plus le processus est ennuyeux, moins il y a de chances d’un réveil à 3 h du matin pour une panne en production. Suivez ces règles pour transformer une extraction risquée en opération prévisible.
— Editorial Team
Aucun commentaire pour le moment.