On analyse le code :
Est-ce que le code peut avoir plusieurs raisons de changer ?
Single responsability principle
Open/closed
Liskov substitution
Interface segregation
Dependency injection
A class should have only one reason to change
— Robert C. Martin, Clean Code
Modularité
Lisibilité
Evolutivité
Testabilité
Pour quelles raisons ce code pourrait changer ?
Allons voir un exemple : ArticleService
le chemin vers le fichier de configuration peut changer
le type de fichier de configuration peut changer (ie: yaml)
la source de configuration peut évoluer (ie: environnement)
la requête SQL peut changer
le type de stockage peut changer (ie: API externe)
l’algorithme de récupération peut changer (ie: status)
isoler la responsabilité de la configuration
isoler la responsabilité du stockage
garder la responsabilité du code métier
Trop de fragmentation avec de très petites classes
Augmentation de la complexité
Découplage trop tôt
On analyse le code :
Est-ce que le code peut avoir plusieurs raisons de changer ?
On arbitre :
Est-ce que c’est grave ?
Est-ce que c’est coûteux aujourd’hui ? demain ?
Mécanisme d’inversion de contrôle
Réduire le couplage
Facilite la réutilisation
Simplifie la mise en place de tests unitaires
Par le constructeur
Par un paramètre de méthode
Si on ne peut pas faire autrement :
Par un setter
Par manipulation du code dynamiquement (introspection)
On reprend l’exemple refactoré : ArticleService
on passe en paramètre les variables d’instance
Perte de lisibilité/traçabilité
Couplage caché
Surtout lorsqu’on a une injection magique fournie par le framework
On analyse le code :
Quelles sont mes dépendances ?
On arbitre :
Qu’est-ce que je gagne à les injecter ?
Les entités logicielles doivent être ouvertes à l’extension, mais fermées à la modification.
— Bertrand Meyer
On peut ajouter un nouveau comportement
Mais le code déjà présent ne devrait pas changer
Respect du contrat d’interface
Pas de surprise à l’exécution
public class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if ("REGULAR".equals(customerType)) {
return amount * 0.05;
} else if ("PREMIUM".equals(customerType)) {
return amount * 0.10;
} else if ("VIP".equals(customerType)) {
return amount * 0.20;
}
return 0.0;
}
}
public class DiscountCalculator {
private final List<DiscountPolicy> policies;
public double calculateDiscount(String customerType, double amount) {
return findByCustomerType(customerType)
.map(policy -> policy.applyDiscount(amount))
.orElse(0.0);
}
private Optional<DiscountPolicy> findByCustomerType(String customerType) {
return policies.stream()
.filter(policy -> policy.appliesTo(customerType))
.findFirst();
}
}
public class RegularDiscount implements DiscountPolicy {
@Override
public boolean appliesTo(String customerType) {
return "REGULAR".equals(customerType);
}
@Override
public double applyDiscount(double amount) {
return amount * 0.05;
}
}
Héritage ou composition pour déléguer
Abstractions (interfaces, classes abstraites, design patterns)
Overengineering
Fragmentation du code métier
⇒ Cibler le code qui change souvent
On analyse le code :
Est-ce que c’est normal de modifier cette classe pour ma fonctionnalité ?
On arbitre :
Est-ce que c’est grave ?
Est-ce que c’est coûteux aujourd’hui ? demain ?
Les objets d’une classe dérivée doivent pouvoir être remplacés par des objets de la classe de base sans altérer la correction du programme.
Exemple : une méthode qui utilise List doit pouvoir fonctionner avec ArrayList, LinkedList ou toute autre implémentation de List
Eviter les surprises
Respect des contrats d’interface
public class Character {
public void move() { System.out.println("Character moves"); }
public void attack() { System.out.println("Character attacks!"); }
}
public class Merchant extends Character {
@Override
public void attack() {
throw new UnsupportedOperationException("Merchants do not attack!");
}
}
public void fight(Character ...characters) { /* appel de attack() */ }
fight(new Character(), new Merchant());
// ❌ exception inattendue lors de l'exécution
⇒ On extrait la partie problématique dans une interface
public interface Combatant {
void attack();
}
public abstract class Character {
public void move() { System.out.println("Character moves"); }
}
⇒ On utilise l’interface uniquement là où c’est pertinent
public class Player extends Character implements Combatant {
@Override
public void attack() { System.out.println("Warrior swings sword!"); }
}
public class Merchant extends Character {
public void trade() { System.out.println("Merchant opens shop..."); }
}
public void fight(Combatatant ...combatants) { /* appel de attack() */ }
fight(new Character(), new Merchant());
// 🚨 ne compile plus, Merchant != Combatant
On analyse le code :
Est-ce que ça pose problème si je remplace une classe/interface par son implémentation ?
J’ai une "UnsupportedOperation", est-ce que c’est légitime ?
On arbitre :
Est-ce que c’est grave ?
Est-ce que c’est coûteux aujourd’hui ? demain ?
Les clients ne doivent pas être forcés de dépendre d’interfaces qu’ils n’utilisent pas.
Eviter de la complexité inutile
Meilleure modularité
Implémentation vide ou qui retourne un "not supported"
public interface Quest {
void start();
void complete();
void trackProgress();
void giveReward();
}
public class CinematicQuest implements Quest {
public void start() { System.out.println("Début d'une cinématique."); }
public void complete() { System.out.println("Fin de la cinématique."); }
// La progression ne s'affiche jamais à l'utilisateur, mais est tout de même "fictive"
public void trackProgress() { System.out.println("Progression interne (non visible)."); }
// Pas de récompense, mais méthode appelée pour rien
public void giveReward() { System.out.println("Pas de récompense prévue."); }
}
⇒ Séparation des interfaces
public interface Quest {
void start();
void complete();
}
public interface ProgressTrackable {
void trackProgress();
}
public interface Rewardable {
void giveReward();
}
⇒ Utilisation à la demande
public class SimpleFetchQuest implements Quest, ProgressTrackable, Rewardable {
public void start() { System.out.println("Va chercher 10 pommes."); }
public void complete() { System.out.println("Tu as apporté 10 pommes."); }
public void trackProgress() { System.out.println("Pommes récupérées : 7/10"); }
public void giveReward() { System.out.println("Tu reçois 100 pièces."); }
}
public class CinematicQuest implements Quest {
public void start() { System.out.println("Début de la cinématique."); }
public void complete() { System.out.println("Fin de la cinématique."); }
}
On analyse le code :
Qu’est-ce qui peut changer ?
On arbitre :
Est-ce que c’est grave ?
Est-ce que c’est coûteux aujourd’hui ? demain ?