Réusinage de code legacy JavaScript : niveau avancé
Travailler avec du code legacy est une réalité inévitable pour tout développeur, en particulier dans le domaine de Node.js. Ce code est souvent un « marécage » de variables globales, de callbacks imbriqués, de fonctions de centaines de lignes et d'une absence totale de structure modulaire. Les méthodes simples de réusinage ne fonctionnent pas ici : une approche systémique et la connaissance de techniques avancées sont nécessaires. Dans cet article, nous analyserons les stratégies qui permettent de transformer le chaos en une architecture maintenable sans arrêter la production.
1. Analyse et cartographie des dépendances
Avant de modifier le code, il faut le comprendre. Dans les projets legacy, la documentation est souvent absente ou obsolète. La première étape consiste à construire une carte des dépendances.
Outils :
- Madge — un utilitaire pour visualiser les dépendances entre les modules. Il montrera les dépendances cycliques et les fichiers « morts ».
- ESLint avec des plugins — aide à identifier les variables, fonctions et importations inutilisées.
- Déploiement dans un environnement isolé — exécutez les tests (s'ils n'existent pas, écrivez au moins des tests de fumée) et enregistrez les modules chargés.
Exemple : Supposons que nous ayons un fichier legacy.js qui contient 2000 lignes et utilise une variable globale config.
// legacy.js (avant réusinage)var config = { apiUrl: 'https://api.example.com', timeout: 5000};
function fetchData(callback) { // 50 lignes de logique avec des if et try/catch imbriqués // utilise la variable globale config if (config.timeout > 0) { // ... } callback(null, data);}Action : Avec Madge, nous voyons que legacy.js est importé dans 15 autres fichiers. C'est un « objet divin ». Notre tâche est de le diviser en modules atomiques.
2. Stratégie « Strangler Fig » (Figuier étrangleur)
Ce modèle est idéal pour un réusinage sans arrêter le système. L'idée : nous ne réécrivons pas tout d'un coup, mais remplaçons progressivement les parties de l'ancien système par de nouvelles, jusqu'à ce que l'ancien meure de lui-même.
Implémentation dans Node.js :
- Créer un nouveau module avec une architecture propre.
- Écrire une couche proxy (ou façade) qui redirige les appels vers l'ancien ou le nouveau module.
- Basculer progressivement les consommateurs vers le nouveau module.
Exemple : Remplacer l'ancien style callback par Promise/async/await.
// Ancienne API (basée sur les callbacks)function getUser(id, callback) { // code legacy callback(null, { id: id, name: 'John' });}
// Nouvelle API (basée sur les Promises)async function getUserAsync(id) { // nouvelle implémentation avec async/await const user = await database.findUser(id); return user;}
// Façade proxyasync function getUserFacade(id) { if (USE_NEW_IMPLEMENTATION) { return getUserAsync(id); } else { return new Promise((resolve, reject) => { getUser(id, (err, user) => { if (err) reject(err); else resolve(user); }); }); }}Le drapeau USE_NEW_IMPLEMENTATION peut être placé dans la configuration ou un feature toggle. Cela permet d'activer le nouveau code pour une partie des utilisateurs (canary release) ou de revenir en arrière sans déploiement.
3. Modularisation et isolation des effets de bord
Le code legacy souffre souvent d'effets de bord cachés : une fonction modifie l'état global, écrit dans un fichier ou appelle une API externe sans retourner de résultat. Le réusinage de ces fonctions consiste à extraire la logique pure.
Technique : « Extraction de fonction » (Extract Function) suivie d'une injection de dépendances (Dependency Injection).
Code problématique :