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