Refactor
Overview
Améliorer la structure et la lisibilité du code sans modifier le comportement externe. La refactorisation est une évolution graduelle, non une révolution. À utiliser pour améliorer le code existant, non pour une réécriture complète.
When to Use
Utiliser cette skill quand :
- Le code est difficile à comprendre ou à maintenir
- Les fonctions/classes sont trop volumineuses
- Des code smells doivent être corrigés
- Ajouter des fonctionnalités est difficile en raison de la structure du code
- L'utilisateur demande « nettoie ce code », « refactor ceci », « améliore cela »
Refactoring Principles
The Golden Rules
- Le comportement est préservé - La refactorisation ne change pas ce que fait le code, seulement comment
- Petites étapes - Faire de minuscules changements, tester après chacun
- Le contrôle de version est ton allié - Committer avant et après chaque état sûr
- Les tests sont essentiels - Sans tests, ce n'est pas de la refactorisation, c'est de l'édition
- Une seule chose à la fois - Ne pas mélanger refactorisation et changements de fonctionnalités
When NOT to Refactor
- Code qui fonctionne et ne changera plus (si c'est pas cassé...)
- Code critique en production sans tests (ajouter les tests d'abord)
- Quand tu es sous une deadline serrée
- « Juste parce que » - besoin d'un objectif clair
Common Code Smells & Fixes
1. Long Method/Function
# BAD: Fonction de 200 lignes qui fait tout
- async function processOrder(orderId) {
- // 50 lignes: fetch order
- // 30 lignes: validate order
- // 40 lignes: calculate pricing
- // 30 lignes: update inventory
- // 20 lignes: create shipment
- // 30 lignes: send notifications
- }
# GOOD: Divisée en fonctions ciblées
+ async function processOrder(orderId) {
+ const order = await fetchOrder(orderId);
+ validateOrder(order);
+ const pricing = calculatePricing(order);
+ await updateInventory(order);
+ const shipment = await createShipment(order);
+ await sendNotifications(order, pricing, shipment);
+ return { order, pricing, shipment };
+ }
2. Duplicated Code
# BAD: Même logique à plusieurs endroits
- function calculateUserDiscount(user) {
- if (user.membership === 'gold') return user.total * 0.2;
- if (user.membership === 'silver') return user.total * 0.1;
- return 0;
- }
-
- function calculateOrderDiscount(order) {
- if (order.user.membership === 'gold') return order.total * 0.2;
- if (order.user.membership === 'silver') return order.total * 0.1;
- return 0;
- }
# GOOD: Extraire la logique commune
+ function getMembershipDiscountRate(membership) {
+ const rates = { gold: 0.2, silver: 0.1 };
+ return rates[membership] || 0;
+ }
+
+ function calculateUserDiscount(user) {
+ return user.total * getMembershipDiscountRate(user.membership);
+ }
+
+ function calculateOrderDiscount(order) {
+ return order.total * getMembershipDiscountRate(order.user.membership);
+ }
3. Large Class/Module
# BAD: Objet-dieu qui en sait trop
- class UserManager {
- createUser() { /* ... */ }
- updateUser() { /* ... */ }
- deleteUser() { /* ... */ }
- sendEmail() { /* ... */ }
- generateReport() { /* ... */ }
- handlePayment() { /* ... */ }
- validateAddress() { /* ... */ }
- // 50 autres méthodes...
- }
# GOOD: Une responsabilité par classe
+ class UserService {
+ create(data) { /* ... */ }
+ update(id, data) { /* ... */ }
+ delete(id) { /* ... */ }
+ }
+
+ class EmailService {
+ send(to, subject, body) { /* ... */ }
+ }
+
+ class ReportService {
+ generate(type, params) { /* ... */ }
+ }
+
+ class PaymentService {
+ process(amount, method) { /* ... */ }
+ }
4. Long Parameter List
# BAD: Trop de paramètres
- function createUser(email, password, name, age, address, city, country, phone) {
- /* ... */
- }
# GOOD: Regrouper les paramètres connexes
+ interface UserData {
+ email: string;
+ password: string;
+ name: string;
+ age?: number;
+ address?: Address;
+ phone?: string;
+ }
+
+ function createUser(data: UserData) {
+ /* ... */
+ }
# EVEN BETTER: Utiliser le pattern builder pour la construction complexe
+ const user = UserBuilder
+ .email('test@example.com')
+ .password('secure123')
+ .name('Test User')
+ .address(address)
+ .build();
5. Feature Envy
# BAD: Méthode qui utilise les données d'un autre objet plus que les siennes
- class Order {
- calculateDiscount(user) {
- if (user.membershipLevel === 'gold') {
+ return this.total * 0.2;
+ }
+ if (user.accountAge > 365) {
+ return this.total * 0.1;
+ }
+ return 0;
+ }
+ }
# GOOD: Déplacer la logique vers l'objet propriétaire des données
+ class User {
+ getDiscountRate(orderTotal) {
+ if (this.membershipLevel === 'gold') return 0.2;
+ if (this.accountAge > 365) return 0.1;
+ return 0;
+ }
+ }
+
+ class Order {
+ calculateDiscount(user) {
+ return this.total * user.getDiscountRate(this.total);
+ }
+ }
6. Primitive Obsession
# BAD: Utiliser les primitives pour les concepts du domaine
- function sendEmail(to, subject, body) { /* ... */ }
- sendEmail('user@example.com', 'Hello', '...');
- function createPhone(country, number) {
- return `${country}-${number}`;
- }
# GOOD: Utiliser les types du domaine
+ class Email {
+ private constructor(public readonly value: string) {
+ if (!Email.isValid(value)) throw new Error('Invalid email');
+ }
+ static create(value: string) { return new Email(value); }
+ static isValid(email: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }
+ }
+
+ class PhoneNumber {
+ constructor(
+ public readonly country: string,
+ public readonly number: string
+ ) {
+ if (!PhoneNumber.isValid(country, number)) throw new Error('Invalid phone');
+ }
+ toString() { return `${this.country}-${this.number}`; }
+ static isValid(country: string, number: string) { /* ... */ }
+ }
+
+ // Utilisation
+ const email = Email.create('user@example.com');
+ const phone = new PhoneNumber('1', '555-1234');
7. Magic Numbers/Strings
# BAD: Valeurs inexpliquées
- if (user.status === 2) { /* ... */ }
- const discount = total * 0.15;
- setTimeout(callback, 86400000);
# GOOD: Constantes nommées
+ const UserStatus = {
+ ACTIVE: 1,
+ INACTIVE: 2,
+ SUSPENDED: 3
+ } as const;
+
+ const DISCOUNT_RATES = {
+ STANDARD: 0.1,
+ PREMIUM: 0.15,
+ VIP: 0.2
+ } as const;
+
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
+
+ if (user.status === UserStatus.INACTIVE) { /* ... */ }
+ const discount = total * DISCOUNT_RATES.PREMIUM;
+ setTimeout(callback, ONE_DAY_MS);
8. Nested Conditionals
# BAD: Code en flèche
- function process(order) {
- if (order) {
- if (order.user) {
- if (order.user.isActive) {
- if (order.total > 0) {
- return processOrder(order);
+ } else {
+ return { error: 'Invalid total' };
+ }
+ } else {
+ return { error: 'User inactive' };
+ }
+ } else {
+ return { error: 'No user' };
+ }
+ } else {
+ return { error: 'No order' };
+ }
+ }
# GOOD: Clauses de garde / retours précoces
+ function process(order) {
+ if (!order) return { error: 'No order' };
+ if (!order.user) return { error: 'No user' };
+ if (!order.user.isActive) return { error: 'User inactive' };
+ if (order.total <= 0) return { error: 'Invalid total' };
+ return processOrder(order);
+ }
# EVEN BETTER: Utiliser le type Result
+ function process(order): Result<ProcessedOrder, Error> {
+ return Result.combine([
+ validateOrderExists(order),
+ validateUserExists(order),
+ validateUserActive(order.user),
+ validateOrderTotal(order)
+ ]).flatMap(() => processOrder(order));
+ }
9. Dead Code
# BAD: Code inutilisé qui persiste
- function oldImplementation() { /* ... */ }
- const DEPRECATED_VALUE = 5;
- import { unusedThing } from './somewhere';
- // Commented out code
- // function oldCode() { /* ... */ }
# GOOD: Le supprimer
+ // Supprimer les fonctions inutilisées, les imports et le code commenté
+ // Si tu en as besoin plus tard, l'historique git l'a
10. Inappropriate Intimacy
# BAD: Une classe accède profondément à une autre
- class OrderProcessor {
- process(order) {
- order.user.profile.address.street; // Trop intime
- order.repository.connection.config; // Bris d'encapsulation
+ }
+ }
# GOOD: Demander, ne pas imposer
+ class OrderProcessor {
+ process(order) {
+ order.getShippingAddress(); // Order sait comment l'obtenir
+ order.save(); // Order sait comment se sauvegarder
+ }
+ }
Extract Method Refactoring
Before and After
# Avant : Une fonction longue
- function printReport(users) {
- console.log('USER REPORT');
- console.log('============');
- console.log('');
- console.log(`Total users: ${users.length}`);
- console.log('');
- console.log('ACTIVE USERS');
- console.log('------------');
- const active = users.filter(u => u.isActive);
- active.forEach(u => {
- console.log(`- ${u.name} (${u.email})`);
- });
- console.log('');
- console.log(`Active: ${active.length}`);
- console.log('');
- console.log('INACTIVE USERS');
- console.log('--------------');
- const inactive = users.filter(u => !u.isActive);
- inactive.forEach(u => {
- console.log(`- ${u.name} (${u.email})`);
- });
- console.log('');
- console.log(`Inactive: ${inactive.length}`);
- }
# Après : Méthodes extraites
+ function printReport(users) {
+ printHeader('USER REPORT');
+ console.log(`Total users: ${users.length}\n`);
+ printUserSection('ACTIVE USERS', users.filter(u => u.isActive));
+ printUserSection('INACTIVE USERS', users.filter(u => !u.isActive));
+ }
+
+ function printHeader(title) {
+ const line = '='.repeat(title.length);
+ console.log(title);
+ console.log(line);
+ console.log('');
+ }
+
+ function printUserSection(title, users) {
+ console.log(title);
+ console.log('-'.repeat(title.length));
+ users.forEach(u => console.log(`- ${u.name} (${u.email})`));
+ console.log('');
+ console.log(`${title.split(' ')[0]}: ${users.length}`);
+ console.log('');
+ }
Introducing Type Safety
From Untyped to Typed
# Avant : Pas de types
- function calculateDiscount(user, total, membership, date) {
- if (membership === 'gold' && date.getDay() === 5) {
- return total * 0.25;
- }
- if (membership === 'gold') return total * 0.2;
- return total * 0.1;
- }
# Après : Sécurité complète des types
+ type Membership = 'bronze' | 'silver' | 'gold';
+
+ interface User {
+ id: string;
+ name: string;
+ membership: Membership;
+ }
+
+ interface DiscountResult {
+ original: number;
+ discount: number;
+ final: number;
+ rate: number;
+ }
+
+ function calculateDiscount(
+ user: User,
+ total: number,
+ date: Date = new Date()
+ ): DiscountResult {
+ if (total < 0) throw new Error('Total cannot be negative');
+
+ let rate = 0.1; // Default bronze
+
+ if (user.membership === 'gold' && date.getDay() === 5) {
+ rate = 0.25; // Friday bonus for gold
+ } else if (user.membership === 'gold') {
+ rate = 0.2;
+ } else if (user.membership === 'silver') {
+ rate = 0.15;
+ }
+
+ const discount = total * rate;
+
+ return {
+ original: total,
+ discount,
+ final: total - discount,
+ rate
+ };
+ }
Design Patterns for Refactoring
Strategy Pattern
# Avant : Logique conditionnelle
- function calculateShipping(order, method) {
- if (method === 'standard') {
- return order.total > 50 ? 0 : 5.99;
- } else if (method === 'express') {
- return order.total > 100 ? 9.99 : 14.99;
+ } else if (method === 'overnight') {
+ return 29.99;
+ }
+ }
# Après : Pattern Strategy
+ interface ShippingStrategy {
+ calculate(order: Order): number;
+ }
+
+ class StandardShipping implements ShippingStrategy {
+ calculate(order: Order) {
+ return order.total > 50 ? 0 : 5.99;
+ }
+ }
+
+ class ExpressShipping implements ShippingStrategy {
+ calculate(order: Order) {
+ return order.total > 100 ? 9.99 : 14.99;
+ }
+ }
+
+ class OvernightShipping implements ShippingStrategy {
+ calculate(order: Order) {
+ return 29.99;
+ }
+ }
+
+ function calculateShipping(order: Order, strategy: ShippingStrategy) {
+ return strategy.calculate(order);
+ }
Chain of Responsibility
# Avant : Validation imbriquée
- function validate(user) {
- const errors = [];
- if (!user.email) errors.push('Email required');
+ else if (!isValidEmail(user.email)) errors.push('Invalid email');
+ if (!user.name) errors.push('Name required');
+ if (user.age < 18) errors.push('Must be 18+');
+ if (user.country === 'blocked') errors.push('Country not supported');
+ return errors;
+ }
# Après : Chain of Responsibility
+ abstract class Validator {
+ abstract validate(user: User): string | null;
+ setNext(validator: Validator): Validator {
+ this.next = validator;
+ return validator;
+ }
+ validate(user: User): string | null {
+ const error = this.doValidate(user);
+ if (error) return error;
+ return this.next?.validate(user) ?? null;
+ }
+ }
+
+ class EmailRequiredValidator extends Validator {
+ doValidate(user: User) {
+ return !user.email ? 'Email required' : null;
+ }
+ }
+
+ class EmailFormatValidator extends Validator {
+ doValidate(user: User) {
+ return user.email && !isValidEmail(user.email) ? 'Invalid email' : null;
+ }
+ }
+
+ // Construire la chaîne
+ const validator = new EmailRequiredValidator()
+ .setNext(new EmailFormatValidator())
+ .setNext(new NameRequiredValidator())
+ .setNext(new AgeValidator())
+ .setNext(new CountryValidator());
Refactoring Steps
Safe Refactoring Process
1. PREPARE
- S'assurer que les tests existent (les écrire s'ils manquent)
- Committer l'état actuel
- Créer une branche de fonctionnalité
2. IDENTIFY
- Trouver le code smell à corriger
- Comprendre ce que fait le code
- Planifier la refactorisation
3. REFACTOR (petites étapes)
- Faire un seul petit changement
- Exécuter les tests
- Committer si les tests passent
- Répéter
4. VERIFY
- Tous les tests passent
- Test manuel si nécessaire
- Performance inchangée ou améliorée
5. CLEAN UP
- Mettre à jour les commentaires
- Mettre à jour la documentation
- Commit final
Refactoring Checklist
Code Quality
- [ ] Les fonctions sont petites (< 50 lignes)
- [ ] Les fonctions font une seule chose
- [ ] Pas de code dupliqué
- [ ] Noms explicites (variables, fonctions, classes)
- [ ] Pas de nombres/chaînes magiques
- [ ] Code mort supprimé
Structure
- [ ] Le code connexe est ensemble
- [ ] Frontières de module claires
- [ ] Les dépendances s'écoulent dans une direction
- [ ] Pas de dépendances circulaires
Type Safety
- [ ] Types définis pour toutes les API publiques
- [ ] Pas de types
anysans justification - [ ] Types nullables explicitement marqués
Testing
- [ ] Le code refactorisé est testé
- [ ] Les tests couvrent les cas limites
- [ ] Tous les tests passent
Common Refactoring Operations
| Opération | Description |
|---|---|
| Extract Method | Transformer un fragment en méthode |
| Extract Class | Déplacer le comportement à une nouvelle classe |
| Extract Interface | Créer une interface à partir de l'implémentation |
| Inline Method | Replacer le corps de la méthode chez l'appelant |
| Inline Class | Déplacer le comportement de la classe à l'appelant |
| Pull Up Method | Déplacer la méthode à la superclasse |
| Push Down Method | Déplacer la méthode à une sous-classe |
| Rename Method/Variable | Améliorer la clarté |
| Introduce Parameter Object | Regrouper les paramètres connexes |
| Replace Conditional with Polymorphism | Utiliser le polymorphisme au lieu de switch/if |
| Replace Magic Number with Constant | Constantes nommées |
| Decompose Conditional | Décomposer les conditions complexes |
| Consolidate Conditional | Combiner les conditions dupliquées |
| Replace Nested Conditional with Guard Clauses | Retours précoces |
| Introduce Null Object | Éliminer les vérifications null |
| Replace Type Code with Class/Enum | Typage fort |
| Replace Inheritance with Delegation | Composition plutôt qu'héritage |