Aller au contenu principal

Chapitre 2: Exercices de Refactoring de Code

Objectif

Ces exercices simulent des situations réelles d'entrevue où on vous montre du mauvais code et on vous demande de le refactorer. Chaque exercice inclut le code problématique, la solution, et une explication claire de ce qui a été amélioré.


Exercice 1 — Éliminer les If imbriqués (Guard Clauses)​

"Ce code est difficile à lire. Refactorisez-le."

Le mauvais code​

public void processOrder(Order order) {
if (order != null) {
if (order.getStatus().equals("ACTIVE")) {
if (order.getItems() != null && !order.getItems().isEmpty()) {
if (order.getCustomer() != null) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
order.setTotal(total);
emailService.sendConfirmation(order.getCustomer().getEmail(), total);
}
}
}
}
}

Problème : "Pyramid of Doom" — les if imbriqués rendent le code illisible et difficile à maintenir.

Le code refactoré​

public void processOrder(Order order) {
if (order == null) return;
if (!"ACTIVE".equals(order.getStatus())) return;
if (order.getItems() == null || order.getItems().isEmpty()) return;
if (order.getCustomer() == null) return;

double total = calculateTotal(order.getItems());
order.setTotal(total);
emailService.sendConfirmation(order.getCustomer().getEmail(), total);
}

private double calculateTotal(List<Item> items) {
return items.stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum();
}

Ce qui a été fait​

TechniqueExplication
Guard clausesLes return en début de méthode éliminent les cas invalides tôt
Extract methodcalculateTotal() est extrait dans sa propre méthode
Stream APIRemplace la boucle for par un pipeline fonctionnel
"ACTIVE".equals()Inverse l'appel pour éviter un NullPointerException
Phrase d'entrevue

"I use guard clauses to handle edge cases early and keep the happy path unindented. I also extract helper methods to improve readability and testability."


Exercice 2 — Remplacer If/Else par le Strategy Pattern​

"Ce code doit supporter de nouveaux types de paiement facilement. Refactorisez-le."

Le mauvais code​

public double calculateFee(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
return amount * 0.03;
} else if (paymentType.equals("PAYPAL")) {
return amount * 0.025;
} else if (paymentType.equals("BANK_TRANSFER")) {
return amount * 0.01;
} else if (paymentType.equals("CRYPTO")) {
return amount * 0.005;
} else {
throw new IllegalArgumentException("Unknown: " + paymentType);
}
}

Problème : Chaque nouveau type de paiement = modifier cette méthode. Viole le principe Open/Closed (ouvert à l'extension, fermé à la modification).

Le code refactoré​

interface FeeCalculator {
double calculate(double amount);
}

class CreditCardFee implements FeeCalculator {
public double calculate(double amount) { return amount * 0.03; }
}

class PaypalFee implements FeeCalculator {
public double calculate(double amount) { return amount * 0.025; }
}

// Registry avec lambdas pour les cas simples
Map<String, FeeCalculator> calculators = Map.of(
"CREDIT_CARD", new CreditCardFee(),
"PAYPAL", new PaypalFee(),
"BANK_TRANSFER", amount -> amount * 0.01,
"CRYPTO", amount -> amount * 0.005
);

public double calculateFee(String type, double amount) {
FeeCalculator calc = calculators.get(type);
if (calc == null) throw new IllegalArgumentException("Unknown: " + type);
return calc.calculate(amount);
}

Ce qui a été fait​

TechniqueExplication
Strategy PatternChaque type de paiement est encapsulé dans sa propre classe/lambda
Open/Closed PrincipleAjouter un nouveau type = ajouter 1 ligne dans le Map
LambdasPour les cas simples, on utilise des lambdas au lieu de classes
RegistryLa Map sert de registre pour retrouver le bon calculateur
Phrase d'entrevue

"I replaced the if/else chain with a Strategy Pattern. Adding a new payment type is now a single line in the registry, without modifying existing code — that's the Open/Closed Principle."


Exercice 3 — Découper une God Class (SRP)​

"Cette classe fait trop de choses. Refactorisez-la."

Le mauvais code​

public class UserService {
public User createUser(String name, String email) {
// Validation
if (name == null || email == null) throw new IllegalArgumentException();
if (!email.contains("@")) throw new IllegalArgumentException("Bad email");

// Création
User user = new User(name, email);

// Persistance directe
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
jdbcTemplate.update(sql, name, email);

// Email
String subject = "Welcome!";
String body = "Hello " + name;
emailSender.send(email, subject, body);

// Logging
log.info("User created: " + email);

return user;
}
}

Problème : Cette classe fait 4 choses : validation, persistance, envoi d'email et logging. Viole le Single Responsibility Principle.

Le code refactoré​

// 1. Validation
class UserValidator {
void validate(String name, String email) {
Objects.requireNonNull(name, "name required");
Objects.requireNonNull(email, "email required");
if (!email.contains("@")) throw new IllegalArgumentException("Bad email");
}
}

// 2. Persistance
@Repository
class UserRepository {
void save(User user) {
jdbcTemplate.update(
"INSERT INTO users (name, email) VALUES (?, ?)",
user.getName(), user.getEmail()
);
}
}

// 3. Notification
@Service
class WelcomeEmailService {
void sendWelcome(User user) {
emailSender.send(user.getEmail(), "Welcome!", "Hello " + user.getName());
}
}

// 4. Orchestration (la seule responsabilité du Service)
@Service
public class UserService {
private final UserValidator validator;
private final UserRepository repo;
private final WelcomeEmailService welcomeEmail;

@Transactional
public User createUser(String name, String email) {
validator.validate(name, email);
User user = new User(name, email);
repo.save(user);
welcomeEmail.sendWelcome(user);
return user;
}
}

Ce qui a été fait​

TechniqueExplication
Single ResponsibilityChaque classe a une seule raison de changer
DécoupageValidator, Repository, EmailService, Service (orchestrateur)
TestabilitéChaque composant se teste indépendamment
MaintenabilitéChanger la logique d'email ne touche pas la persistance
Phrase d'entrevue

"I applied the Single Responsibility Principle. The UserService now only orchestrates — validation, persistence, and notification are each in their own class. This makes each piece independently testable and maintainable."


Exercice 4 — Remplacer les Constantes Magiques​

"Ce code utilise des nombres magiques partout. Refactorisez-le."

Le mauvais code​

public double calculateShipping(double weight, int zone) {
if (zone == 1) {
if (weight <= 5) return 4.99;
else if (weight <= 20) return 9.99;
else return 14.99;
} else if (zone == 2) {
if (weight <= 5) return 7.99;
else if (weight <= 20) return 14.99;
else return 24.99;
} else {
return 29.99;
}
}

Le code refactoré​

public enum ShippingZone {
LOCAL(1, Map.of(
WeightCategory.LIGHT, 4.99,
WeightCategory.MEDIUM, 9.99,
WeightCategory.HEAVY, 14.99
)),
NATIONAL(2, Map.of(
WeightCategory.LIGHT, 7.99,
WeightCategory.MEDIUM, 14.99,
WeightCategory.HEAVY, 24.99
));

private final int code;
private final Map<WeightCategory, Double> rates;

public double getRate(double weight) {
return rates.get(WeightCategory.fromWeight(weight));
}
}

enum WeightCategory {
LIGHT(0, 5), MEDIUM(5, 20), HEAVY(20, Double.MAX_VALUE);

static WeightCategory fromWeight(double weight) {
return Arrays.stream(values())
.filter(c -> weight > c.min && weight <= c.max)
.findFirst()
.orElse(HEAVY);
}
}

// Utilisation
public double calculateShipping(double weight, ShippingZone zone) {
return zone.getRate(weight);
}
Phrase d'entrevue

"I replaced magic numbers with enums and lookup tables. The code is now self-documenting and adding a new zone is just adding an enum value."


Exercice 5 — Remplacer l'héritage par la composition​

"Cette hiérarchie de classes est rigide. Refactorisez-la."

Le mauvais code​

class Animal {
void eat() { System.out.println("Eating"); }
}
class FlyingAnimal extends Animal {
void fly() { System.out.println("Flying"); }
}
class SwimmingAnimal extends Animal {
void swim() { System.out.println("Swimming"); }
}
// Problème : un canard vole ET nage. Héritage multiple impossible en Java.

Le code refactoré (Composition)​

interface Behavior {
void perform();
}

class FlyBehavior implements Behavior {
public void perform() { System.out.println("Flying"); }
}
class SwimBehavior implements Behavior {
public void perform() { System.out.println("Swimming"); }
}

class Animal {
private final String name;
private final List<Behavior> behaviors;

Animal(String name, Behavior... behaviors) {
this.name = name;
this.behaviors = List.of(behaviors);
}

void performAll() {
behaviors.forEach(Behavior::perform);
}
}

// Un canard peut voler ET nager
Animal duck = new Animal("Duck", new FlyBehavior(), new SwimBehavior());
duck.performAll(); // Flying + Swimming
Phrase d'entrevue

"I favor composition over inheritance. Instead of a rigid class hierarchy, I compose behaviors. This follows the 'favor composition over inheritance' principle from the Gang of Four."


Résumé des principes SOLID appliqués​

PrincipeExerciceTechnique
S — Single ResponsibilityEx. 3 (God Class)Découper en classes spécialisées
O — Open/ClosedEx. 2 (Strategy)Ajouter sans modifier
L — Liskov SubstitutionEx. 5 (Composition)Composition plutôt qu'héritage
I — Interface SegregationEx. 2 (FeeCalculator)Interfaces petites et ciblées
D — Dependency InversionEx. 3 (UserService)Dépendre d'abstractions, pas d'implémentations

⬅️ Chapitre 1: JPA & BDD | Chapitre 3: Algorithmes →