Clean code et refactoring - 14 - Techniques de refactoring

Dix techniques de refactoring essentielles avec du code avant/apres en TypeScript

  1. 01 Clean code et refactoring - 00 - Pourquoi le clean code est un investissement, pas un luxe
  2. 02 Clean code et refactoring - 01 - Nommage : la competence la plus sous-estimee
  3. 03 Clean code et refactoring - 02 - Fonctions : courtes, claires, responsables
  4. 04 Clean code et refactoring - 03 - Conditions et lisibilité : sortir de la pyramide
  5. 05 Clean code et refactoring - 04 - Commentaires et documentation : quand le code ne suffit pas
  6. 06 Clean code et refactoring - 05 - Immutabilite et effets de bord : moins de surprises, moins de bugs
  7. 07 Clean code et refactoring - 06 - Gestion des erreurs propre : fail fast, fail loud
  8. 08 Clean code et refactoring - 07 - Programmation defensive vs offensive : valider aux frontieres, faire confiance a l'intérieur
  9. 09 Clean code et refactoring - 08 - SOLID en pratique avec TypeScript
  10. 10 Clean code et refactoring - 09 - DRY, KISS, YAGNI
  11. 11 Clean code et refactoring - 10 - Couplage et cohesion
  12. 12 Clean code et refactoring - 11 - Complexite cyclomatique
  13. 13 Clean code et refactoring - 12 - Abstractions prematurees vs tardives
  14. 14 Clean code et refactoring - 13 - Code smells
  15. 15 Clean code et refactoring - 14 - Techniques de refactoring
  16. 16 Clean code et refactoring - 15 - Refactoring legacy sans tout casser
  17. 17 Clean code et refactoring - 16 - Tests comme filet de sécurité pour le refactoring
  18. 18 Clean code et refactoring - 17 - Structurer un projet — feature-based vs layer-based
  19. 19 Clean code et refactoring - 18 - Constantes, configuration et magic numbers
  20. 20 Clean code et refactoring - 19 - Linting et formatting — ESLint, Biome, automatiser la qualité
  21. 21 Clean code et refactoring - 20 - Conventions d'équipe et ADR
  22. 22 Clean code et refactoring - 21 - Dette technique — quand elle est acceptable, quand elle tue le projet
  23. 23 Clean code et refactoring - 22 - Code review — donner et recevoir du feedback
  24. 24 Clean code et refactoring - 23 - Glossaire — tous les termes de la serie

14 - Techniques de refactoring

Ce que tu vas apprendre

  • Dix techniques de refactoring avec des exemples avant/apres
  • Quand utiliser chaque technique et dans quel ordre
  • Comment enchaîner les refactorings pour transformer du code legacy
  • Les pièges a éviter pendant un refactoring

Prerequisites

Cet article fait suite a 13 - Code smells. Chaque smell de l'article précédent a un ou plusieurs refactorings associes. On passe a la pratique.


Martin Fowler a catalogue plus de 60 techniques de refactoring. La bonne nouvelle : tu n'en utilises qu'une dizaine au quotidien. La mauvaise : ces dix techniques representent 90% du travail, et les mal appliquer fait plus de degats que de ne rien faire. Voici celles que j'utilise le plus, avec du vrai code.

1. Extract Function

Le refactoring le plus frequent. Tu prends un bloc de code, tu le mets dans une fonction avec un nom qui dit ce qu'il fait.

typescript// Avant
function printInvoice(invoice: Invoice): void {
  console.log("=== FACTURE ===");
  console.log(`Client: ${invoice.customer.name}`);

  let total = 0;
  for (const line of invoice.lines) {
    const lineTotal = line.quantity * line.unitPrice;
    const tax = lineTotal * line.taxRate;
    total += lineTotal + tax;
    console.log(`${line.description}: ${lineTotal + tax}`);
  }

  console.log(`Total: ${total}`);
}

// Apres
function printInvoice(invoice: Invoice): void {
  printHeader(invoice.customer);
  const total = calculateTotal(invoice.lines);
  printLines(invoice.lines);
  printFooter(total);
}

function calculateTotal(lines: InvoiceLine[]): number {
  return lines.reduce((sum, line) => {
    const lineTotal = line.quantity * line.unitPrice;
    return sum + lineTotal + lineTotal * line.taxRate;
  }, 0);
}

La regle : si tu dois ajouter un commentaire pour expliquer un bloc, ce bloc merite d'etre une fonction. Le nom de la fonction remplace le commentaire.

2. Inline Function

L'inverse de Extract. Quand une fonction n'ajoute rien par rapport a son corps.

typescript// Avant : indirection inutile
function isAdult(age: number): boolean {
  return moreThanEighteen(age);
}

function moreThanEighteen(age: number): boolean {
  return age >= 18;
}

// Apres : inline
function isAdult(age: number): boolean {
  return age >= 18;
}

Inline est utile quand une couche d'abstraction ne fait que passer l'appel. Ca arrive souvent apres un refactoring précédent qui a redistribue les responsabilités.

3. Rename

Le refactoring le plus sous-estime. Un bon nom elimine des commentaires et des questions.

typescript// Avant
function calc(d: number[]): number {
  let r = 0;
  for (const v of d) {
    r += v;
  }
  return r / d.length;
}

// Apres
function calculateAverage(measurements: number[]): number {
  let sum = 0;
  for (const value of measurements) {
    sum += value;
  }
  return sum / measurements.length;
}

Rename s'applique aux fonctions, variables, paramètres, classes, fichiers. Chaque rename rend le code un peu plus lisible. Les IDE modernes font ca en une touche (F2 dans VS Code).

4. Introduce Parameter Object

Quand plusieurs paramètres voyagent toujours ensemble, ils forment un concept qui merite un nom. C'est la solution au smell "Data Clumps" qu'on a vu dans l'article précédent.

typescript// Avant : cinq parametres a chaque appel
function searchProducts(
  minPrice: number,
  maxPrice: number,
  category: string,
  sortBy: string,
  page: number
): Product[] { /* ... */ }

searchProducts(10, 100, "electronics", "price", 1);

// Apres : un objet qui a du sens
interface ProductSearchCriteria {
  priceRange: { min: number; max: number };
  category: string;
  sortBy: string;
  page: number;
}

function searchProducts(criteria: ProductSearchCriteria): Product[] { /* ... */ }

searchProducts({
  priceRange: { min: 10, max: 100 },
  category: "electronics",
  sortBy: "price",
  page: 1,
});

Le code appelant est plus lisible. Les paramètres ont un contexte. Et tu peux ajouter des champs sans casser la signature.

5. Replace Conditional with Polymorphism

Quand un switch ou une cascade de if/else fait des choses différentes selon un type, le polymorphisme est souvent plus propre. On l'a vu dans l'article sur la complexité cyclomatique.

typescript// Avant
function calculatePay(employee: Employee): number {
  switch (employee.type) {
    case "fulltime":
      return employee.salary;
    case "parttime":
      return employee.hourlyRate * employee.hoursWorked;
    case "contractor":
      return employee.dailyRate * employee.daysWorked * 1.1;
    default:
      throw new Error(`Unknown type: ${employee.type}`);
  }
}

// Apres : chaque type sait se calculer
interface Payable {
  calculatePay(): number;
}

class FullTimeEmployee implements Payable {
  constructor(private salary: number) {}
  calculatePay(): number { return this.salary; }
}

class PartTimeEmployee implements Payable {
  constructor(private hourlyRate: number, private hoursWorked: number) {}
  calculatePay(): number { return this.hourlyRate * this.hoursWorked; }
}

class Contractor implements Payable {
  constructor(private dailyRate: number, private daysWorked: number) {}
  calculatePay(): number { return this.dailyRate * this.daysWorked * 1.1; }
}

Ajouter un nouveau type d'employe ne touche pas le code existant. C'est le principe Open/Closed en action (voir article SOLID).

6. Extract Variable

Donner un nom a une expression complexe pour la rendre lisible.

typescript// Avant
if (user.subscription.plan === "premium" &&
    user.subscription.expiresAt > new Date() &&
    user.account.balance >= 0 &&
    !user.account.isSuspended) {
  grantAccess(user);
}

// Apres
const hasPremiumPlan = user.subscription.plan === "premium";
const isSubscriptionActive = user.subscription.expiresAt > new Date();
const hasPositiveBalance = user.account.balance >= 0;
const isNotSuspended = !user.account.isSuspended;

const canAccessPremiumContent =
  hasPremiumPlan && isSubscriptionActive && hasPositiveBalance && isNotSuspended;

if (canAccessPremiumContent) {
  grantAccess(user);
}

Chaque variable intermediaire est un mini-commentaire. Le if final se lit comme une phrase.

7. Replace Magic Number with Named Constant

Les nombres magiques rendent le code opaque. Une constante nommee donne le contexte.

typescript// Avant
if (password.length < 8) {
  throw new Error("Mot de passe trop court");
}

if (retryCount > 3) {
  throw new Error("Trop de tentatives");
}

setTimeout(cleanup, 86400000);

// Apres
const MIN_PASSWORD_LENGTH = 8;
const MAX_RETRY_ATTEMPTS = 3;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;

if (password.length < MIN_PASSWORD_LENGTH) {
  throw new Error("Mot de passe trop court");
}

if (retryCount > MAX_RETRY_ATTEMPTS) {
  throw new Error("Trop de tentatives");
}

setTimeout(cleanup, ONE_DAY_MS);

8. Encapsulate Collection

Ne jamais exposer une collection directement. L'appelant peut la modifier par accident.

typescript// Avant : la collection est exposee
class Team {
  members: User[] = [];
}

// L'appelant peut faire n'importe quoi
team.members.push(user); // pas de validation
team.members = [];        // supprime tout le monde

// Apres : la collection est protegee
class Team {
  private _members: User[] = [];

  get members(): readonly User[] {
    return this._members;
  }

  addMember(user: User): void {
    if (this._members.length >= 10) {
      throw new Error("Equipe pleine");
    }
    if (this._members.some((m) => m.id === user.id)) {
      throw new Error("Deja membre");
    }
    this._members.push(user);
  }

  removeMember(userId: string): void {
    this._members = this._members.filter((m) => m.id !== userId);
  }
}

L'équipe contrôle ses regles d'ajout et de suppression. Les invariants sont proteges.

9. Compose Method

Transformer une longue méthode en une sequence d'appels de meme niveau d'abstraction.

typescript// Avant : melange de niveaux d'abstraction
async function deployApplication(config: DeployConfig): Promise<void> {
  // Bas niveau : lecture de fichiers
  const packageJson = JSON.parse(fs.readFileSync("package.json", "utf-8"));
  const version = packageJson.version;

  // Haut niveau : decision metier
  if (config.environment === "production" && !config.approved) {
    throw new Error("Production deploy requires approval");
  }

  // Bas niveau : build
  execSync("npm run build");
  execSync(`docker build -t app:${version} .`);

  // Haut niveau : notification
  await notify(`Deploy ${version} started`);
}

// Apres : chaque etape est au meme niveau
async function deployApplication(config: DeployConfig): Promise<void> {
  const version = readAppVersion();
  requireApprovalForProduction(config);
  const image = buildDockerImage(version);
  await pushImage(image, config.registry);
  await notifyTeam(`Deploy ${version} started`);
}

Chaque ligne de la fonction refactoree est au meme niveau d'abstraction. Tu lis le "quoi" sans etre noye dans le "comment".

10. Move Function

Deplacer une fonction là où elle a le plus de sens. C'est la réponse au smell "Feature Envy".

typescript// Avant : le calcul de distance est dans OrderService
class OrderService {
  calculateDeliveryDistance(
    warehouse: Warehouse,
    customer: Customer
  ): number {
    const dx = warehouse.lat - customer.lat;
    const dy = warehouse.lon - customer.lon;
    return Math.sqrt(dx * dx + dy * dy) * 111;
  }
}

// Apres : le calcul de distance est dans un module geographique
class GeoCalculator {
  distanceKm(from: Coordinates, to: Coordinates): number {
    const dx = from.lat - to.lat;
    const dy = from.lon - to.lon;
    return Math.sqrt(dx * dx + dy * dy) * 111;
  }
}

Le OrderService n'a pas a connaître la geometrie terrestre. Le GeoCalculator est réutilisable ailleurs.

Pour aller plus loin sur l'organisation de ces techniques dans un flux de travail réel, retrouve des retours d'experience sur paltemps.fr.

Résumé

  • Extract Function est le refactoring numero un -- si tu en retiens un seul, c'est celui-la
  • Rename est sous-estime mais a un impact énorme sur la lisibilité
  • Introduce Parameter Object elimine les longues listes de paramètres
  • Replace Conditional with Polymorphism supprime les switch repetes
  • Extract Variable rend les conditions complexes lisibles
  • Compose Method aligne les niveaux d'abstraction
  • Chaque technique est petite et reversible -- le risque est faible

Article précédent : 13 - Code smells

Article suivant : 15 - Refactoring legacy

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.