Chapitre 2: Exercices de Refactoring de Code
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
| Technique | Explication |
|---|---|
| Guard clauses | Les return en début de méthode éliminent les cas invalides tôt |
| Extract method | calculateTotal() est extrait dans sa propre méthode |
| Stream API | Remplace la boucle for par un pipeline fonctionnel |
| "ACTIVE".equals() | Inverse l'appel pour éviter un NullPointerException |
"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
| Technique | Explication |
|---|---|
| Strategy Pattern | Chaque type de paiement est encapsulé dans sa propre classe/lambda |
| Open/Closed Principle | Ajouter un nouveau type = ajouter 1 ligne dans le Map |
| Lambdas | Pour les cas simples, on utilise des lambdas au lieu de classes |
| Registry | La Map sert de registre pour retrouver le bon calculateur |
"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
| Technique | Explication |
|---|---|
| Single Responsibility | Chaque classe a une seule raison de changer |
| Découpage | Validator, Repository, EmailService, Service (orchestrateur) |
| Testabilité | Chaque composant se teste indépendamment |
| Maintenabilité | Changer la logique d'email ne touche pas la persistance |
"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);
}
"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
"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
| Principe | Exercice | Technique |
|---|---|---|
| S — Single Responsibility | Ex. 3 (God Class) | Découper en classes spécialisées |
| O — Open/Closed | Ex. 2 (Strategy) | Ajouter sans modifier |
| L — Liskov Substitution | Ex. 5 (Composition) | Composition plutôt qu'héritage |
| I — Interface Segregation | Ex. 2 (FeeCalculator) | Interfaces petites et ciblées |
| D — Dependency Inversion | Ex. 3 (UserService) | Dépendre d'abstractions, pas d'implémentations |