POO en Java · L2 · Section 2/7
Héritage et composition
Progression
#Héritage, interfaces et composition
Passer par extends pour partager du code est tentant : on écrit moins et l'on hérite de la structure du parent. Pourtant l'héritage d'implémentation installe un contrat rigide ; si la relation « est-un » se fissure, toute la hiérarchie devient fragile. Ce chapitre montre quand l'héritage reste pertinent, et comment la composition offre un couplage plus souple. Le terrain de jeu prolonge le fil conducteur bancaire : une famille de moyens de paiement.
Prérequis : chapitre Modélisation objet (invariants, encapsulation, records) ; notions d'interface en Java.
Objectifs : distinguer héritage de type et héritage d'implémentation ; composer des comportements via interfaces, méthodes par défaut et délégation ; employer les patrons State, Strategy et Template Method avec les constructions modernes du langage.
#1. Le dilemme : extends ou composer ?
L'héritage d'implémentation convient quand la sous-classe est une vraie spécialisation et que le parent est conçu pour être étendu : méthodes protégées documentées comme points de variation, ou squelette explicite. C'est le cas des classes du JDK prévues pour cela (AbstractList), plus rarement de vos propres classes. Sinon, la composition répond mieux :
- Héritage : relation statique « est-un », figée à la compilation, difficile à faire évoluer sans casser les sous-classes.
- Composition : relation « a-un », le comportement devient un champ que l'on remplace, teste séparément ou fait varier à l'exécution.
Règle pratique : héritez de types (interfaces), composez les implémentations.
#2. Classes abstraites : factoriser une spécialisation réelle
Tous nos moyens de paiement partagent un squelette : vérifier le plafond, puis exécuter, puis journaliser. Une classe abstraite porte ce squelette tandis que chaque sous-classe reste un moyen de paiement à part entière :
1import java.math.BigDecimal;2 3public abstract class MoyenPaiement {4 private final String id;5 private final BigDecimal plafond;6 7 protected MoyenPaiement(String id, BigDecimal plafond) {8 this.id = id;9 this.plafond = plafond;10 }11 12 /** Squelette fixe (Template Method) ; seule l'étape d'exécution varie. */13 public final void payer(BigDecimal montant) {14 if (montant.signum() <= 0) {1import java.math.BigDecimal;2 3public final class CarteBancaire extends MoyenPaiement {4 public CarteBancaire(String id, BigDecimal plafond) {5 super(id, plafond);6 }7 8 @Override9 protected void effectuer(BigDecimal montant) {10 System.out.println("Autorisation réseau carte : " + montant);11 }12}payer est final : le squelette ne doit pas être contourné, les sous-classes ne remplissent que effectuer. La journalisation n'est pas protected car elle est un détail interne.
#3. Composition et Strategy : la politique devient un champ
Ce qui varie d'un cas à l'autre ne mérite pas une sous-classe. Les frais d'un virement dépendent du segment client : plutôt que VirementStandard, VirementPremium, VirementEntreprise, injectez la politique de tarification dans un champ.
1import java.math.BigDecimal;2import java.util.function.UnaryOperator;3 4public final class MoteurVirement {5 6 /** Une politique de frais : reçoit le montant, renvoie les frais. */7 private final UnaryOperator<BigDecimal> politiqueFrais;8 9 public MoteurVirement(UnaryOperator<BigDecimal> politiqueFrais) {10 this.politiqueFrais = politiqueFrais;11 }12 13 public BigDecimal fraisTotal(BigDecimal montant) {14 return politiqueFrais.apply(montant);1var tauxFixe = new MoteurVirement(m -> new BigDecimal("0.50"));2var pourcentage = new MoteurVirement(m -> m.multiply(new BigDecimal("0.001")));3 4tauxFixe.fraisTotal(new BigDecimal("80")); // 0.505pourcentage.fraisTotal(new BigDecimal("80")); // 0.080Chaque politique se teste isolément, s'ajoute sans toucher au moteur, et peut venir d'une configuration plutôt que du code. C'est le patron Strategy ramené à sa plus simple expression : une fonction injectée.
#4. Interfaces enrichies : mixins par méthodes par défaut
Depuis Java 8, une interface peut offrir des méthodes par défaut et (depuis Java 9) des méthodes privées. On capture ainsi un comportement transverse utilisable par plusieurs services sans multiplier les super-classes. Exemple : une capacité de tentative répétée.
1import java.util.function.Supplier;2 3public interface Retriable {4 5 int maxAttempts(); // au moins 16 7 default <T> T retry(Supplier<T> action) {8 RuntimeException last = null;9 for (int attempt = 1; attempt <= maxAttempts(); attempt++) {10 try {11 return action.get();12 } catch (RuntimeException e) {13 last = e; // échec transitoire : on retente14 pause();Une classe métier compose plusieurs mixins (Retriable, Auditable, ...) tout en n'étendant qu'au plus une classe. Point d'attention : ne transformez pas les méthodes par défaut en dépôt de code ; au-delà de quelques lignes, une classe déléguée est plus lisible.
#5. Patron State : types scellés plutôt que switch tentaculaire
Une commande traverse des états (CREEE, PAYEE, EXPEDIEE). Modéliser l'état par une chaîne de caractères, c'est accepter qu'un switch géant décide des transitions autorisées, avec un default muet qui avale les états inconnus. Les interfaces scellées rendent l'ensemble de états fermé et le compilateur vérifie l'exhaustivité :
1import java.time.Instant;2import java.util.UUID;3 4public sealed interface EtatCommande permits Creee, Payee, Expediee {}5public record Creee(UUID id) implements EtatCommande {}6public record Payee(UUID id, Instant payeLe) implements EtatCommande {}7public record Expediee(UUID id, Instant expedieeLe) implements EtatCommande {}1import java.time.Instant;2 3public final class Commande {4 private EtatCommande etat;5 6 public Commande(java.util.UUID id) {7 this.etat = new Creee(id);8 }9 10 public EtatCommande etat() {11 return etat;12 }13 14 public void payer(Instant maintenant) {Ajoutez un quatrième état (Remboursee) : le compilateur signale immédiatement les deux switch incomplets, là où une chaîne de caractères n'aurait rien détecté. Les états sont des records immuables, donc comparables et journalisables sans précaution.
#6. Atelier
- Une hiérarchie
Vehicle/Car/Truckest traitée par une méthode bourrée d'instanceof. Refactorez-la : scellez la hiérarchie et remplacez chaqueinstanceofpar unswitchsur motifs au point d'usage ; si le traitement doit rester extérieur aux classes, introduisez unVisitordont le compilateur vérifie l'exhaustivité. - Concevez un
Pipeline<T>qui compose des stratégies de validation : chaque étape renvoie la liste de ses erreurs, la pipeline agrège. Vérification attendue : sur unPipeline<Compte>à deux étapes dont une seule échoue,validerrenvoie exactement une erreur.
Éléments de correction pour l'atelier 2 :
1import java.util.ArrayList;2import java.util.List;3import java.util.function.Function;4 5@FunctionalInterface6interface Validateur<T> {7 List<String> appliquer(T cible);8}9 10final class Pipeline<T> {11 private final List<Validateur<T>> etapes;12 13 private Pipeline(List<Validateur<T>> etapes) {14 this.etapes = List.copyOf(etapes);L'instance est immuable : puis renvoie une nouvelle pipeline, l'originale reste partageable sans risque.