POO en Java · L2 · Section 1/7
Modélisation objet
Progression
#Modéliser avec classes, objets et invariants
La programmation orientée objet ne se réduit pas à générer des getters et des setters. Une classe utile raconte une histoire : elle transporte un état cohérent et propose les gestes métier qui le font évoluer sans casser les règles du domaine. Ce chapitre part d'un exemple familier, la gestion d'un compte bancaire, et détaille chaque décision de conception.
Prérequis : savoir déclarer une classe, un constructeur et des méthodes en Java ; comprendre la visibilité
private/public.Objectifs : formuler des invariants explicites, encapsuler les mutations derrière des méthodes intentionnelles, distinguer données mutables et valeurs immuables, choisir
BigDecimalpour les montants.
#1. Définir le périmètre et les invariants
Avant d'écrire la première ligne, listez les contraintes incontournables : un compte possède un IBAN, le solde initial est positif ou nul, le découvert est interdit. Ces règles deviennent les garde-fous du constructeur et des méthodes publiques.
1import java.math.BigDecimal;2import java.util.Objects;3 4public final class CompteBancaire {5 private final String iban;6 private BigDecimal solde;7 8 public CompteBancaire(String iban, BigDecimal soldeInitial) {9 this.iban = Objects.requireNonNull(iban, "IBAN obligatoire");10 if (soldeInitial.signum() < 0) {11 throw new IllegalArgumentException("Solde initial négatif : " + soldeInitial);12 }13 this.solde = soldeInitial;14 }Trois choix se lisent directement dans ce code :
- Les champs restent privés ; on n'expose pas de setter générique mais des méthodes intentionnelles (
crediter,debiter). Les consommateurs passent par une API métier au lieu d'assigner arbitrairement un champ. finalsur la classe interdit une spécialisation non prévue : cette classe n'a pas été conçue pour l'héritage.- Le solde est un
BigDecimal, pas undouble: l'argent ne supporte pas les erreurs d'arrondi binaire (0.1 + 0.2ne vaut pas exactement0.3en virgule flottante).
#2. Encapsuler les mutations
Chaque opération traduit une règle : créditer refuse les montants nuls ou négatifs, débiter vérifie en plus le découvert. La validation est factorisée dans une petite méthode privée statique, ce qui garantit la même règle partout.
1public synchronized void crediter(BigDecimal montant) {2 verifierMontantStrictementPositif(montant);3 solde = solde.add(montant);4}5 6public synchronized void debiter(BigDecimal montant) {7 verifierMontantStrictementPositif(montant);8 if (solde.compareTo(montant) < 0) {9 throw new IllegalStateException(10 "Découvert refusé : solde=" + solde + ", tentative=" + montant);11 }12 solde = solde.subtract(montant);13}14 Les exceptions non vérifiées suffisent ici, car « débiter plus que le solde » signale un usage fautif de l'API. Le chapitre Qualité montrera quand promouvoir ce cas en exception vérifiée du domaine (DecouvertInterditException). Le corps de la classe reste court, mais chaque instruction porte un sens : les règles métier vivent dans le code, et les messages d'exception décrivent l'anomalie au lieu de rester génériques.
#3. Représenter les valeurs avec des records
Certaines données ne doivent jamais changer : un virement effectué est un fait, pas une variable. Les records (standardisés en Java 16) portent exactement ce cas : champs finaux, equals, hashCode et toString générés, et un constructeur compact pour valider les invariants.
1import java.math.BigDecimal;2import java.time.Instant;3import java.util.Objects;4 5public record Virement(String source, String cible, BigDecimal montant, Instant a) {6 7 public Virement {8 Objects.requireNonNull(source, "Compte source manquant");9 Objects.requireNonNull(cible, "Compte cible manquant");10 Objects.requireNonNull(a, "Horodatage manquant");11 if (montant.signum() <= 0) {12 throw new IllegalArgumentException("Montant invalide : " + montant);13 }14 }Utiliser un record en guise de simple sac de données mutables serait une erreur d'intention : un record affirme « je suis une valeur ». Deux virements identiques au même instant sont alors equals, ce qui simplifie les tests et les collections.
#4. Vérifier ses invariants : le test comme spécification
Un invariant non testé n'est qu'une intention. Un test expressif documente le comportement et échouera si quelqu'un supprime la garde :
1import org.junit.jupiter.api.Test;2import java.math.BigDecimal;3import static org.assertj.core.api.Assertions.*;4 5class CompteBancaireTest {6 7 @Test8 void refuse_le_decouvert() {9 var compte = new CompteBancaire("FR76...", new BigDecimal("100"));10 assertThatThrownBy(() -> compte.debiter(new BigDecimal("200")))11 .isInstanceOf(IllegalStateException.class)12 .hasMessageContaining("Découvert refusé");13 }14 Vérification : mvn test (ou gradle test) doit afficher deux tests passés. Si le premier échoue, la garde du débit a disparu.
#5. Atelier
- Modélisez un intervalle de dates immuable
DateRange(LocalDate debut, LocalDate fin). Le constructeur compact doit rejeterfinavantdebut, et une méthodeoverlaps(DateRange other)renvoietruequand les deux intervalles se chevauchent. Vérification : deux intervalles qui se touchent renvoienttrue, un intervalle mal orienté lève une exception. - Concevez une machine à états pour une commande (
CREATED,PAID,SHIPPED) : chaque transition est une méthode explicite (payer(),expedier()) et une transition interdite lèveIllegalStateException. Vérification : créer, payer puis tenterpayer()à nouveau doit lever l'exception.
Éléments de correction pour l'atelier 1 :
1import java.time.LocalDate;2import java.util.Objects;3 4public record DateRange(LocalDate debut, LocalDate fin) {5 public DateRange {6 Objects.requireNonNull(debut);7 Objects.requireNonNull(fin);8 if (fin.isBefore(debut)) {9 throw new IllegalArgumentException("fin avant debut : " + fin);10 }11 }12 13 public boolean overlaps(DateRange other) {14 return !debut.isAfter(other.fin) && !other.debut.isAfter(fin);En maîtrisant ces fondations, vous construisez des objets qui protègent leurs invariants et rendent explicites les opérations métier, ce qui facilite la maintenance comme les tests automatiques.