“Il faut tout réécrire !” Non. Voici comment améliorer progressivement une codebase legacy sans Big Bang rewrite (qui échoue 80% du temps).
Le mythe de la réécriture
La proposition classique
"Ce code est pourri, on va tout réécrire en 6 mois."
Réalité :
- 6 mois → 18 mois
- Budget x3
- Features manquantes
- Bugs nouveaux
- Parfois : Abandon du projet
Exemples célèbres d’échecs :
- Netscape (réécriture → mort de l’entreprise)
- Basecamp (réécriture abandonnée)
Strangler Fig Pattern : La solution
Principe
Remplacer progressivement l’ancien système par du nouveau code, fonctionnalité par fonctionnalité.
Old System ████████████ (100%)
↓
Old ██████████░░ (80%) + New ░░ (20%)
↓
Old ████░░░░░░░░ (40%) + New ░░░░░░ (60%)
↓
Old ░░░░░░░░░░░░ (10%) + New ██████████ (90%)
↓
New ████████████ (100%)
Nom : Inspiré du figuier étrangleur qui pousse autour d’un arbre existant.
Stratégie : Par où commencer ?
Matrice de priorisation
Impact Business ↑
│
🟢 │ 🟢 ← Start here (High impact, easy)
High │
│
🟡 │ 🔴 ← Avoid (High impact, hard = risky)
│
🟢 │ 🟡 ← Quick wins
Low │
│────────────→ Effort
Easy Hard
Identifier les quick wins
Exemples :
- Formatting : Prettier, ESLint (1 jour, impact énorme sur lisibilité)
- Dead code removal : Supprimer code inutilisé (Safe, libère mental)
- Dependencies update : Upgrade libs obsolètes (Sécurité)
Pattern 1 : Add, Don’t Modify
Principe
Ajouter du nouveau code plutôt que modifier l’ancien.
Exemple : Ajouter validation
// ❌ Modifier fonction legacy (risqué)
function createUser(data) {
// 200 lignes de legacy code
// Modifier ici = potentiel de bugs
}
// ✅ Wrapper avec validation
function createUserV2(data) {
// Nouvelle validation propre
validateUserData(data);
// Appeler legacy
return createUser(data);
}
// Migrer progressivement les appels
- createUser(data)
+ createUserV2(data)
Avantages :
- Legacy intact (pas de régression)
- Nouveau code testé indépendamment
- Rollback facile
Pattern 2 : Branch by Abstraction
Principe
Créer une abstraction, puis migrer les implémentations.
Exemple : Changer de DB
// Étape 1 : Créer interface
interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
}
// Étape 2 : Implémenter ancien système
class LegacyUserRepository implements UserRepository {
async findById(id: string) {
// Appeler ancien code MySQL legacy
return legacyDb.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// Étape 3 : Nouvelle implémentation
class PostgresUserRepository implements UserRepository {
async findById(id: string) {
return this.pg.query('SELECT * FROM users WHERE id = $1', [id]);
}
}
// Étape 4 : Feature flag pour switcher
const userRepo: UserRepository =
featureFlags.isEnabled('use-postgres')
? new PostgresUserRepository()
: new LegacyUserRepository();
// Étape 5 : Rollout progressif, puis supprimer legacy
Pattern 3 : Seam Model
Principe
Identifier les “coutures” (points d’entrée/sortie) pour isoler le legacy.
Exemple : Legacy payment processing
// Legacy code (200 lignes, complexe)
function processPayment(order, card) {
// Mélange de logique business, DB, external calls
// Impossible à tester
}
// Identifier seam = point d'entrée
// Créer facade
class PaymentService {
process(order: Order): PaymentResult {
// Nouvelle logique propre
const validated = this.validate(order);
const charged = this.charge(validated);
const recorded = this.record(charged);
return recorded;
}
private validate(order: Order) {
// Nouvelle validation
}
private charge(order: Order) {
// Appel legacy temporairement
return processPayment(order, order.paymentMethod);
}
private record(result: PaymentResult) {
// Nouveau code de persistence
}
}
// Progressivement, remplacer charge() par nouveau code
Refactoring tactiques
1. Boy Scout Rule
“Laisser le code plus propre qu’on l’a trouvé”
// Avant de fixer un bug
function calculateTotal(items) {
var total = 0; // var 😱
for (var i = 0; i < items.length; i++) { // for loop 😱
total = total + items[i].price; // pas de validation
}
return total;
}
// Après fix + mini refactoring
function calculateTotal(items: Item[]): number {
if (!Array.isArray(items)) {
throw new Error('Items must be an array');
}
return items.reduce((total, item) => {
if (typeof item.price !== 'number') {
throw new Error('Invalid item price');
}
return total + item.price;
}, 0);
}
// 5 minutes de refactoring, impact énorme
2. Extract Method
// Avant : God function
function handleCheckout(cart, user, payment) {
// 150 lignes de code
// Validation
// Calcul prix
// Taxes
// Shipping
// Payment
// Email
// ...
}
// Après : Extraction progressive
function handleCheckout(cart, user, payment) {
validateCheckout(cart, user);
const pricing = calculatePricing(cart);
const order = createOrder(cart, user, pricing);
processPayment(order, payment);
sendConfirmationEmail(user, order);
return order;
}
// Chaque fonction < 20 lignes, testable
3. Introduce Parameter Object
// Avant : Trop de paramètres
function createInvoice(
customerName,
customerEmail,
customerAddress,
items,
discount,
taxRate,
shippingCost
) {
// ...
}
// Après : Regrouper
interface InvoiceData {
customer: Customer;
items: LineItem[];
pricing: PricingInfo;
}
function createInvoice(data: InvoiceData) {
// Beaucoup plus clair
}
Tests : Sécuriser le refactoring
Characterization Tests
Tests qui documentent le comportement actuel (même s’il est bugué).
// Legacy function (comportement bizarre mais utilisé)
function formatPrice(price) {
return '$' + price.toFixed(2).replace('.00', '');
}
// Characterization test
describe('formatPrice (legacy behavior)', () => {
it('removes .00 from whole numbers', () => {
expect(formatPrice(10)).toBe('$10'); // Pas '$10.00'
});
it('keeps decimals for non-whole numbers', () => {
expect(formatPrice(10.50)).toBe('$10.50');
});
it('formats negative numbers', () => {
expect(formatPrice(-5)).toBe('$-5'); // Bug ? Mais c'est l'existant
});
});
// Maintenant, refactorer en sécurité
Golden Master Testing
// Capturer outputs legacy
const inputs = loadTestInputs();
const goldenOutputs = inputs.map(input => legacyFunction(input));
// Après refactoring
const newOutputs = inputs.map(input => newFunction(input));
// Comparer
expect(newOutputs).toEqual(goldenOutputs);
Mesurer la dette technique
Metrics à tracker
// Code complexity (Cyclomatic)
// Tools: ESLint complexity rule
function complex() {
if (a) {
if (b) {
if (c) {
if (d) {
// Complexity = 4 (trop élevé)
}
}
}
}
}
// Code churn (changements fréquents)
git log --format=format: --name-only | \
sort | uniq -c | sort -rg | head -10
// Files with most bugs
git log --grep="fix\|bug" --name-only | \
sort | uniq -c | sort -rg
Dashboard dette technique
┌────────────────────────────────────┐
│ Technical Debt Dashboard │
├────────────────────────────────────┤
│ Code Coverage: 45% → 68% (+23%) │
│ Complexity: 8.2 → 5.4 (-34%) │
│ Duplicated Code: 18% → 9% (-50%) │
│ Security Vulnerabilities: 23 → 3 │
│ │
│ Most Complex Files: │
│ 1. legacy/checkout.js (CC: 42) │
│ 2. legacy/pricing.js (CC: 38) │
│ 3. api/orders.js (CC: 28) │
└────────────────────────────────────┘
Cas réel : E-commerce legacy (150k LOC)
Situation initiale
- Monolithe PHP : 8 ans d’âge
- Tests : 5% coverage
- Deployment : Manuel, 2h
- Bugs : 40/mois
Stratégie (12 mois)
Mois 1-3 : Stabiliser
- Setup CI/CD (automatiser deploy)
- Tests critiques (checkout, payment)
- Documentation architecture
Mois 4-6 : Quick wins
- Formatter (PSR-12)
- Dead code removal (-15k LOC)
- Dependencies update
Mois 7-9 : Extract services
- Service “Inventory” extrait (nouveau code)
- Service “Notifications” extrait
- Monolithe appelle via API
Mois 10-12 : Refactoring continu
- Boy Scout Rule systématique
- Coverage : 5% → 45%
- Complexity réduite
Résultats (12 mois)
- Bugs : 40/mois → 8/mois (-80%)
- Deploy : 2h → 15min (-87%)
- Coverage : 5% → 45%
- Velocity : +60% (features plus rapides)
- Team happiness : 4/10 → 8/10
Coût : 0.5 ETP dédié = ~$50k ROI : Économie bugs + velocity = ~$200k/an
Erreurs à éviter
Erreur 1 : Big Bang rewrite
❌ "On réécrit tout en 6 mois"
✅ "On extrait 1 service en 1 mois, on évalue, puis on continue"
Erreur 2 : Refactoring sans tests
❌ Refactorer code sans tests = roulette russe
✅ Tests d'abord, refactoring ensuite
Erreur 3 : Refactoring sans business value
❌ "Je vais refactorer ce module qu'on n'utilise plus"
✅ Refactorer code critique / fréquemment modifié
Erreur 4 : Perfectionnisme
❌ "Je vais rendre ce code parfait"
✅ "Je vais rendre ce code 20% meilleur"
Checklist refactoring safe
Avant de refactorer :
✅ Tests existants passent ✅ Nouveau test ajouté (si possible) ✅ Change small (< 400 lignes) ✅ Rollback plan (feature flag ?) ✅ Monitoring en place
Pendant :
✅ Tests passent à chaque étape ✅ Commit fréquents ✅ Code review
Après :
✅ Tests passent ✅ Performance maintenue ✅ Monitoring OK (pas de spike errors)
Conclusion
Refactorer legacy n’est pas un luxe.
C’est un investissement :
- Vélocité améliorée
- Bugs réduits
- Team happier
Approche pragmatique :
- Commencer petit (formatting, dead code)
- Tests de sécurité (characterization tests)
- Refactoring progressif (Strangler Fig)
- Mesurer l’impact
En 12 mois : Codebase transformée, sans Big Bang.
Et vous, comment gérez-vous votre legacy ?