C4 — Robustesse et modélisationDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 2 · Programmation orientée objet

Cours 4Robustesse et modélisation

Signaler et traiter ce qui tourne mal, puis savoir dessiner une conception avant de l'écrire — et la relire dans du code existant.

2 chapitres · 10 h de travail estimé

  1. 1. Exceptions5 h
  2. 2. Modélisation UML5 h

Chapitre 1 · 5 h

Exceptions

Erreurs et exceptions, try, catch, finally, propagation et throws, exceptions personnalisées, et quand lever plutôt que rendre un code d'erreur.

Comment une méthode signale-t-elle qu'elle n'a pas pu faire son travail ?

La réponse du C était le code de retour : -1 en cas d'échec, et un appelant discipliné qui le teste. Le cours de programmation a montré ce que cela coûte — scanf rend le nombre de conversions réussies, et personne ne le regarde jamais.

int retirer(double montant) {    if (montant > solde) return -1;      /* échec */    solde -= montant;    return 0;} compte.retirer(1000);                    /* et voilà. Personne n'a rien vérifié. */

Quatre défauts, indépendants les uns des autres. Le code d'erreur peut être ignoré en toute impunité — c'est le cas ci-dessus. Il occupe la valeur de retour, donc une méthode qui doit rendre un résultat utile n'a plus de place pour signaler l'échec. Il ne porte aucune information : -1 ne dit pas combien il manquait. Et il doit être propagé à la main à travers chaque niveau d'appel, chacun devant le tester et le retransmettre.

Les exceptions règlent les quatre.

Le mécanisme

public void retirer(double montant) {    if (montant <= 0) {        throw new IllegalArgumentException("montant non positif : " + montant);    }    if (montant > solde) {        throw new SoldeInsuffisantException(montant - solde);    }    solde -= montant;}

throw interrompt immédiatement la méthode. La valeur de retour n'est jamais produite : il n'y a plus de code d'erreur à ignorer, et le chemin d'échec est séparé du chemin normal.

Du côté de l'appelant :

try {    compte.retirer(1000);    System.out.println("retrait effectué");} catch (SoldeInsuffisantException e) {    System.out.println("il manque " + e.getManquant() + " euros");} catch (IllegalArgumentException e) {    System.out.println("montant invalide : " + e.getMessage());}

Ce qui se passe alors mérite d'être décrit précisément, car c'est le mécanisme du bloc I d'Algorithmique 2 employé à l'envers. Quand une exception est levée, la machine remonte la pile d'appels — elle dépile les cadres un par un — jusqu'à trouver un catch capable de la traiter. Tous les cadres traversés sont abandonnés. Si aucun catch ne se présente, la remontée atteint main, le programme s'arrête, et la trace affichée est exactement le contenu de la pile au moment du throw.

D'où la propriété centrale : l'exception franchit les niveaux intermédiaires sans qu'ils aient un mot à dire. Une méthode qui ne sait pas traiter un échec n'a rien à écrire du tout — c'est ce qui supprime la propagation manuelle du code d'erreur.

L'ordre des catch compte : ils sont examinés dans l'ordre, et le premier compatible gagne. Un catch (Exception e) placé avant les autres les rend inatteignables, et le compilateur le refuse — c'est la substituabilité du chapitre 6 appliquée aux erreurs, puisque toutes les exceptions héritent d'un ancêtre commun.

finally

FileReader f = null;try {    f = new FileReader("donnees.txt");    /* lecture */} catch (IOException e) {    System.err.println("lecture impossible");} finally {    if (f != null) f.close();        /* exécuté DANS TOUS LES CAS */}

Le bloc finally s'exécute quoi qu'il arrive : sortie normale, exception attrapée, exception non attrapée qui traverse — et même après un return. C'est le seul endroit où l'on peut garantir qu'une ressource sera rendue.

Java offre depuis la version 7 une forme plus sûre, le try avec ressources, qui appelle close() automatiquement :

try (FileReader f = new FileReader("donnees.txt")) {    /* lecture */} catch (IOException e) { ... }

C'est la forme à employer. Elle évite l'oubli du close, le null à tester, et l'exception que close() pourrait lever dans le finally — cas tordu qui masquerait l'exception d'origine.

Un piège à connaître : un return dans le finally écrase celui du try, exception comprise. C'est légal, c'est déroutant, et cela ne s'écrit pas.

Contrôlées ou non contrôlées

Java est le seul grand langage à faire cette distinction, et elle structure tout le chapitre.

Contrôlées (checked)Non contrôlées (unchecked)
Héritent deExceptionRuntimeException
Le compilateurexige de les traiter ou de les déclarerne dit rien
ExemplesIOException, SQLExceptionNullPointerException, IllegalArgumentException
Signifientun échec prévisible et extérieurune erreur de programmation

Une méthode qui peut lever une exception contrôlée sans la traiter doit l'annoncer :

public void charger(String chemin) throws IOException { ... }

Le critère de choix, tel qu'on l'enseigne : une exception contrôlée signale une situation anormale mais attendue, sur laquelle l'appelant peut agir — fichier absent, réseau coupé. Une exception non contrôlée signale un bogue : un argument invalide, un pointeur nul, un indice hors bornes. On ne demande pas à l'appelant de traiter un bogue, on le corrige.

La distinction est débattue : elle produit des signatures encombrées et pousse à écrire des catch vides pour faire taire le compilateur. C'est pourquoi les langages venus après Java — C#, Kotlin — ne l'ont pas reprise. Il faut la connaître parce que Java l'impose, et savoir qu'elle n'est pas une évidence.

Exceptions personnalisées

On en écrit une dès que le type standard ne dit pas assez, ou qu'on veut transporter une donnée.

public class SoldeInsuffisantException extends RuntimeException {    private final double manquant;     public SoldeInsuffisantException(double manquant) {        super("il manque " + manquant + " euros");        this.manquant = manquant;    }     public double getManquant() { return this.manquant; }}

Trois choix se lisent dans ces lignes. Le nom finit par Exception, convention universelle. Le message passe au parent par super(...), et sera affiché dans la trace. Et la classe porte une donnée — le montant manquant — que l'appelant peut exploiter : c'est le troisième défaut du code d'erreur, réglé.

Le choix du parent — Exception ou RuntimeException — est le vrai choix de conception, et il découle du tableau précédent.

Les trois anti-usages

Ils sont fréquents, et chacun annule le bénéfice du chapitre.

Avaler l'exception.

try { ... } catch (Exception e) { }        /* le pire code de ce cours */

L'échec disparaît sans trace. Le programme continue dans un état inconnu, et le diagnostic devient impossible. Si l'on ne sait vraiment pas quoi faire, on laisse remonter — c'est gratuit, il suffit de ne rien écrire.

Employer les exceptions comme structure de contrôle. Sortir d'une boucle par une exception, ou tester l'existence d'une clé en attrapant l'échec, fonctionne et coûte cher : construire une exception implique de capturer la pile d'appels. Une exception doit rester exceptionnelle.

Attraper trop large. catch (Exception e) ramasse tout, y compris ce qu'on n'avait pas prévu — un bogue de programmation qu'il aurait mieux valu voir. On attrape le type le plus précis possible.

Quiz · 1 question

Quels défauts du code de retour les exceptions corrigent-elles ?

  • Un seul : elles rendent l'erreur plus lisible dans les tracesla lisibilité
  • Quatre : le code d'erreur peut être IGNORÉ, il OCCUPE la valeur de retour, il ne porte AUCUNE INFORMATION, et il doit être PROPAGÉ à la main à travers chaque niveau d'appelquatre défauts
  • Aucun : c'est une question de style, et les deux approches sont équivalenteséquivalentes

Réponse : Les quatre défauts sont indépendants. IGNORABLE : rien n'oblige à tester une valeur de retour, et scanf en est la preuve — presque personne ne regarde ce qu'elle rend. OCCUPE LE RETOUR : une méthode qui doit produire un résultat utile n'a plus de place pour signaler l'échec, d'où les conventions bancales (rendre −1, ou null, ou un objet vide). AUCUNE INFORMATION : −1 ne dit pas combien il manquait, alors qu'une exception personnalisée transporte la donnée. PROPAGATION MANUELLE : chaque niveau intermédiaire doit tester et retransmettre, alors qu'une exception traverse la pile sans que les niveaux aient un mot à dire. Le throw interrompt de plus la méthode immédiatement, ce qui sépare nettement le chemin d'échec du chemin normal.

Quiz · 1 question

Quand faut-il faire hériter son exception de RuntimeException plutôt que d'Exception ?

  • Toujours : RuntimeException évite d'encombrer les signatures avec throwstoujours
  • Quand l'exception signale une ERREUR DE PROGRAMMATION — argument invalide, état incohérent — qu'on corrige au lieu de la traiter. Exception (contrôlée) est pour un échec prévisible et extérieur, sur lequel l'appelant peut agirselon la nature de l'échec
  • Quand l'exception est levée dans un constructeur, seul cas où le compilateur n'accepte pas les exceptions contrôléescas du constructeur

Réponse : Le critère est la NATURE de l'échec, pas la commodité. Une exception CONTRÔLÉE (Exception) signale une situation anormale mais ATTENDUE, venue de l'extérieur du programme : fichier absent, réseau coupé, base indisponible. L'appelant peut raisonnablement agir — réessayer, demander un autre fichier — donc le compilateur exige qu'il la traite ou la déclare. Une exception NON CONTRÔLÉE (RuntimeException) signale un BOGUE : argument invalide, pointeur nul, indice hors bornes. On ne demande pas à l'appelant de traiter un bogue, on le corrige — d'où l'absence de contrainte. Choisir RuntimeException pour alléger les signatures est le contre-emploi : cela masque un échec que l'appelant aurait dû prévoir. La distinction est d'ailleurs débattue, et les langages venus après Java ne l'ont pas reprise — mais Java l'impose.

À vous

L'exercice suit le fil de bibliothèque, et compare les deux approches sur le même service.

D'abord la version à code de retour : vous écrivez emprunter qui rend -1, -2, 0, puis un appelant qui oublie de tester — et vous constatez qu'un livre est emprunté deux fois sans que rien ne le dise.

Puis la version à exceptions, avec une exception personnalisée transportant la donnée utile. Vous observerez la remontée de pile sur trois niveaux d'appel : le niveau intermédiaire n'écrit rien, et l'exception le traverse.

Enfin les pièges : l'ordre des catch, le finally qui s'exécute même après un return, et le catch vide — que vous instrumenterez pour voir combien d'échecs il a fait disparaître.

Exercice de code

Opposez code de retour et exceptions sur le même service, puis mesurez ce qu'un catch vide fait disparaître.

Point de départ

// ── 1. Version à code de retour ───────────────────────────────────────────
const OK = 0, DEJA_EMPRUNTE = -1, INCONNU = -2;

class BibliothequeCodes {
  constructor(livres) { this.livres = livres; }
  emprunter(titre, qui) {
    const l = this.livres.find((x) => x.titre === titre);
    if (!l) return INCONNU;
    if (!l.disponible) return DEJA_EMPRUNTE;
    l.disponible = false; l.par = qui;
    return OK;
  }
}

// ── 2. Version à exceptions ───────────────────────────────────────────────
class LivreIndisponibleException extends Error {
  constructor(titre, detenteur) {
    super("« " + titre + " » est déjà emprunté par " + detenteur);
    this.name = "LivreIndisponibleException";
    // ← à écrire : transporter la donnée utile (le détenteur actuel)
  }
}

class BibliothequeExceptions {
  constructor(livres) { this.livres = livres; }
  emprunter(titre, qui) {
    const l = this.livres.find((x) => x.titre === titre);
    if (!l) throw new Error("titre inconnu : " + titre);
    // ← à écrire : lever LivreIndisponibleException si déjà emprunté
    l.disponible = false; l.par = qui;
  }
}

// Trois niveaux d'appel : le niveau intermédiaire n'écrit RIEN.
function servirUnLecteur(biblio, titre, qui) { traiterDemande(biblio, titre, qui); }
function traiterDemande(biblio, titre, qui) { biblio.emprunter(titre, qui); }

// ── 3. Le catch vide, instrumenté ─────────────────────────────────────────
let avales = 0;
function catchVide(action) {
  try { action(); } catch (e) { avales++; }        // le pire code du cours
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Appelez emprunter() deux fois SANS tester le code de retour, et
//    constatez ce qui n'est pas dit.
// 2. Complétez l'exception personnalisée et son levée. Vérifiez que le
//    niveau intermédiaire n'a rien eu à écrire.
// 3. Faites vingt emprunts dont plusieurs impossibles, à travers catchVide :
//    combien d'échecs ont disparu, et qu'en sait le programme ?

const catalogue = () => [
  { titre: "Dune", disponible: true, par: null },
  { titre: "Ubik", disponible: true, par: null },
];
const b = new BibliothequeCodes(catalogue());
b.emprunter("Dune", "Ana");
b.emprunter("Dune", "Bo");
console.log("   état : " + JSON.stringify(b.livres[0]));

Solution

const OK = 0, DEJA_EMPRUNTE = -1, INCONNU = -2;

class BibliothequeCodes {
  constructor(livres) { this.livres = livres; }
  emprunter(titre, qui) {
    const l = this.livres.find((x) => x.titre === titre);
    if (!l) return INCONNU;
    if (!l.disponible) return DEJA_EMPRUNTE;
    l.disponible = false; l.par = qui;
    return OK;
  }
}

class LivreIndisponibleException extends Error {
  constructor(titre, detenteur) {
    super("« " + titre + " » est déjà emprunté par " + detenteur);
    this.name = "LivreIndisponibleException";
    // La donnée utile voyage avec l'exception : c'est le troisième défaut
    // du code de retour, réglé.
    this.detenteur = detenteur;
    this.titre = titre;
  }
}

class BibliothequeExceptions {
  constructor(livres) { this.livres = livres; }
  emprunter(titre, qui) {
    const l = this.livres.find((x) => x.titre === titre);
    if (!l) throw new Error("titre inconnu : " + titre);
    if (!l.disponible) throw new LivreIndisponibleException(titre, l.par);
    l.disponible = false; l.par = qui;
  }
}

// Le niveau intermédiaire : PAS UNE LIGNE consacrée aux erreurs.
function traiterDemande(biblio, titre, qui) { biblio.emprunter(titre, qui); }
function servirUnLecteur(biblio, titre, qui) { traiterDemande(biblio, titre, qui); }

const catalogue = () => [
  { titre: "Dune", disponible: true, par: null },
  { titre: "Ubik", disponible: true, par: null },
];

console.log("— 1. code de retour, non testé —");
const b = new BibliothequeCodes(catalogue());
b.emprunter("Dune", "Ana");
b.emprunter("Dune", "Bo");        // échoue, et personne ne le sait
b.emprunter("Solaris", "Cy");     // titre inconnu, idem
console.log("   " + JSON.stringify(b.livres[0]));
console.log("   Bo croit avoir emprunté Dune, qui est chez Ana. Le programme n'a");
console.log("   rien dit, et rien dans son état ne permet de s'en apercevoir.");

console.log("");
console.log("— 2. exceptions, à travers trois niveaux —");
const c = new BibliothequeExceptions(catalogue());
servirUnLecteur(c, "Dune", "Ana");
console.log("   premier emprunt : ok");
try {
  servirUnLecteur(c, "Dune", "Bo");
} catch (e) {
  if (e instanceof LivreIndisponibleException) {
    console.log("   refusé : " + e.message);
    console.log("   donnée exploitable : détenteur = " + e.detenteur +
                " — on peut proposer une réservation");
  } else {
    console.log("   autre échec : " + e.message);
  }
}
console.log("   traiterDemande n'a écrit AUCUNE ligne sur les erreurs : l'exception");
console.log("   l'a traversé sans lui demander son avis.");

console.log("");
console.log("— l'ordre des catch —");
try {
  try { throw new LivreIndisponibleException("Dune", "Ana"); }
  catch (e) {
    // En Java, un catch (Exception e) placé AVANT le type précis rendrait
    // celui-ci inatteignable, et le compilateur le refuserait. On attrape
    // donc toujours du plus précis au plus général.
    if (e instanceof LivreIndisponibleException) throw new Error("relancée après traitement précis");
  }
} catch (e) { console.log("   " + e.message); }

console.log("");
console.log("— finally s'exécute même après un return —");
function avecFinally() {
  try { return "valeur du try"; }
  finally { console.log("   finally s'exécute — avant que la valeur ne soit rendue"); }
}
console.log("   " + avecFinally());

console.log("");
console.log("— 3. le catch vide, instrumenté —");
let avales = 0;
function catchVide(action) { try { action(); } catch (e) { avales++; } }
const d = new BibliothequeExceptions(catalogue());
const demandes = [["Dune", "Ana"], ["Dune", "Bo"], ["Ubik", "Cy"], ["Ubik", "Dee"],
                  ["Solaris", "Eve"], ["Dune", "Fay"]];
for (const [t, q] of demandes) catchVide(() => d.emprunter(t, q));
console.log("   " + demandes.length + " demandes, " + avales + " échecs AVALÉS sans trace.");
console.log("   Le programme s'est terminé normalement, avec quatre lecteurs qui");
console.log("   croient avoir un livre. C'est exactement l'état du point 1 — le");
console.log("   catch vide a rétabli tous les défauts du code de retour.");
console.log("   Si l'on ne sait pas quoi faire : ne rien écrire, et laisser remonter.");

En travaux pratiques

Travaux pratiques 7 · 3 h

Signaler l'erreur là où on sait la traiter

Choisir entre code de retour et exception, distinguer vérifiées et non vérifiées, et supprimer de son code les deux fautes qui rendent une erreur invisible.

Avant de commencer

  • Les TP 1 à 6

Énoncé

  1. Sans exceptionsÉcrivez le chargement d'un catalogue en signalant les erreurs par des codes de retour. Comptez les lignes de traitement d'erreur par rapport aux lignes utiles.
  2. Avec exceptionsRéécrivez la même chose avec des exceptions. Recomptez, et repérez où le code utile est devenu lisible d'un seul tenant.
  3. Vérifiée ou nonÉcrivez une exception vérifiée et une non vérifiée. Appelez les deux sans les traiter et comparez ce que dit le compilateur.
  4. Le catch videAttrapez une exception et ne faites rien. Provoquez l'erreur et observez ce que voit l'utilisateur. Puis affichez la trace et comparez. Indice : Le pire n'est pas le plantage : c'est le programme qui continue avec des données fausses.
  5. Attraper trop largeAttrapez le type le plus général possible autour d'un bloc, et faites-y survenir une erreur de programmation. Constatez ce qui est masqué.
  6. L'exception qui perd sa causeAttrapez une exception basse et relancez-en une métier SANS transmettre la cause. Comparez la trace obtenue avec celle où la cause est transmise.
  7. Fermer quoi qu'il arriveOuvrez un fichier, provoquez une erreur avant la fermeture, et vérifiez que la ressource fuit. Corrigez avec la fermeture automatique.
  8. ChoisirPour six situations que vous listerez, décidez : code de retour, exception vérifiée, ou non vérifiée. Justifiez chaque choix en une phrase.

C'est réussi quand

  • Votre version à exceptions sépare visiblement le cas nominal du traitement d'erreur
  • Vous montrez un catch qui masque une erreur de programmation
  • Votre trace conserve la cause d'origine sur trois niveaux
  • Votre fichier est fermé même quand une exception survient

Correction

Codes de retour contre exceptions
/* codes de retour : le cas nominal est NOYÉ */
int r = ouvrir(f);
if (r != 0) return r;
r = lireEntete(f);
if (r != 0) { fermer(f); return r; }
r = lireCorps(f);
if (r != 0) { fermer(f); return r; }

/* exceptions : le cas nominal se lit d'un trait */
try (Fichier f = ouvrir(chemin)) {
  lireEntete(f);
  lireCorps(f);
} catch (FormatInvalideException e) {
  journal.erreur("catalogue illisible : " + chemin, e);
}

L'exception sépare le CHEMIN NOMINAL du traitement d'erreur, au lieu de les entrelacer. C'est son vrai apport, avant même la propagation automatique. Elle a aussi un défaut, à connaître : le flot de contrôle devient invisible dans le code, et une exception peut sortir de n'importe quelle ligne.

Vérifiée ou non
class CatalogueIntrouvableException extends Exception { }      /* VÉRIFIÉE */
class CoteInvalideException extends RuntimeException { }       /* non */

void charger() throws CatalogueIntrouvableException { … }
/* l'appelant DOIT attraper ou déclarer : le compilateur l'exige */

void valider() { throw new CoteInvalideException(); }
/* l'appelant peut ignorer : rien ne l'oblige */

Le critère classique : vérifiée quand l'appelant peut RAISONNABLEMENT réagir — fichier absent, réseau indisponible ; non vérifiée quand c'est un bogue de programmation — argument nul, indice hors bornes, invariant rompu. Le débat reste vif, et les langages récents ont tous abandonné les exceptions vérifiées ; l'usage dominant en Java est de les réserver aux erreurs réellement récupérables.

Les deux fautes qui effacent l'erreur
/* 1. le catch vide */
try { catalogue = charger(chemin); }
catch (IOException e) { }        /* catalogue reste null,
                                  le programme continue et
                                  plante 200 lignes plus loin */

/* 2. attraper trop large */
try { traiterTout(); }
catch (Exception e) { journal.avertir("souci mineur"); }
/* attrape aussi NullPointerException, ArrayIndexOutOfBounds…
 c'est-à-dire vos BOGUES, requalifiés en « souci mineur » */

Le catch vide est la faute la plus coûteuse du langage : l'information existait, elle a été jetée. Attraper Exception est la même faute en plus discret — un bogue de programmation y devient un avertissement. La règle : attraper le type le plus PRÉCIS possible, et si l'on ne sait pas quoi faire, ne pas attraper.

Transmettre la cause
/* SANS la cause : la trace commence au niveau métier */
catch (SQLException e) {
  throw new CatalogueException("chargement impossible");
}
→ CatalogueException: chargement impossible
  at Catalogue.charger(Catalogue.java:42)
 /* et POURQUOI ? Personne ne le saura jamais. */

/* AVEC la cause */
catch (SQLException e) {
  throw new CatalogueException("chargement impossible", e);
}
→ CatalogueException: chargement impossible
  at Catalogue.charger(Catalogue.java:42)
Caused by: SQLException: connection refused: port 5432
  at …

Le « Caused by » est ce qui rend une trace exploitable : il conserve toute la chaîne, du symptôme métier jusqu'à la cause technique. Traduire une exception basse en exception métier est une bonne pratique — l'appelant n'a pas à connaître SQL — mais jeter la cause en chemin transforme un diagnostic d'une minute en enquête d'une journée.

Fermer quoi qu'il arrive
/* la ressource FUIT si lireTout() lève */
BufferedReader r = new BufferedReader(new FileReader(f));
String contenu = lireTout(r);
r.close();                       /* jamais atteint */

/* try-with-resources : close() est appelé DANS TOUS LES CAS */
try (BufferedReader r = new BufferedReader(new FileReader(f))) {
  return lireTout(r);
}
/* et si close() lève à son tour, l'exception d'origine est conservée,
 la seconde étant attachée comme « supprimée » */

Un descripteur de fichier non fermé est exactement la fuite du TP 7 de Systèmes : la ressource est finie, et un serveur qui fuit un descripteur par requête tombe en quelques heures. Le bloc try-with-resources garantit la fermeture sans finally verbeux, et conserve la bonne exception — ce qu'un finally écrit à la main manque presque toujours.

La table de décision
fichier de configuration absent   → exception vérifiée (récupérable)
argument null passé à une méthode → non vérifiée (bogue de l'appelant)
recherche sans résultat           → NI l'une NI l'autre : Optional
                                  (l'absence est un cas NORMAL)
saisie utilisateur invalide       → valeur de retour ou Optional
                                  (attendu, fréquent, pas exceptionnel)
invariant interne rompu           → non vérifiée, et ne pas rattraper
disque plein à l'écriture         → vérifiée

La ligne la plus utile est la troisième : une recherche infructueuse n'est PAS une erreur, et lever une exception pour un cas normal coûte cher — construire la trace est une opération lourde — et brouille la lecture. Optional exprime l'absence dans le type de retour, ce qui la rend impossible à oublier.

Ce que la suite en fait

Le chapitre 8 donne la notation pour dessiner ce que les six premiers chapitres ont construit, exceptions comprises : une hiérarchie d'exceptions est une hiérarchie de classes ordinaire, et elle se dessine comme telle.

Le chapitre 9 les fera rencontrer les collections et les fichiers, où elles sont partout : IOException à la lecture, NumberFormatException à la conversion, NoSuchElementException sur une collection vide. C'est là qu'on mesure que les exceptions ne sont pas un ornement mais le mode de signalement de toute la bibliothèque standard.

À retenir

Flashcards · 5 cartes

Quels sont les quatre défauts du code de retour ?
1) IL PEUT ÊTRE IGNORÉ en toute impunité. 2) IL OCCUPE LA VALEUR DE RETOUR, donc une méthode produisant un résultat utile n'a plus de place pour signaler l'échec. 3) IL NE PORTE AUCUNE INFORMATION : −1 ne dit pas combien il manquait. 4) IL DOIT ÊTRE PROPAGÉ À LA MAIN à travers chaque niveau d'appel. Les exceptions règlent les quatre, et throw interrompt de plus la méthode immédiatement.
Que se passe-t-il exactement quand une exception est levée ?
La machine REMONTE LA PILE D'APPELS, dépilant les cadres un par un, jusqu'à trouver un catch capable de la traiter ; tous les cadres traversés sont abandonnés. Sans catch, la remontée atteint main, le programme s'arrête, et la trace affichée EST le contenu de la pile au moment du throw. Propriété centrale : l'exception franchit les niveaux intermédiaires sans qu'ils aient un mot à dire — une méthode qui ne sait pas traiter un échec n'écrit rien du tout.
Que garantit finally, et quelle forme lui préférer ?
Il s'exécute QUOI QU'IL ARRIVE : sortie normale, exception attrapée, exception qui traverse, et même après un return. C'est le seul endroit où garantir qu'une ressource sera rendue. Depuis Java 7 on préfère le TRY AVEC RESSOURCES, qui appelle close() automatiquement : il évite l'oubli, le null à tester, et l'exception que close() pourrait lever en masquant l'originale. Piège : un return dans le finally écrase celui du try, exception comprise.
Contrôlée ou non contrôlée : quel critère, et quelle nuance ?
CONTRÔLÉE (hérite d'Exception) : échec anormal mais ATTENDU, venu de l'extérieur — fichier absent, réseau coupé — sur lequel l'appelant peut agir ; le compilateur exige de la traiter ou de la déclarer par throws. NON CONTRÔLÉE (hérite de RuntimeException) : erreur de PROGRAMMATION — argument invalide, pointeur nul, indice hors bornes — qu'on corrige au lieu de la traiter. Nuance : la distinction est débattue, elle encombre les signatures et pousse aux catch vides ; C# et Kotlin ne l'ont pas reprise.
Quels sont les trois anti-usages des exceptions ?
1) LES AVALER : catch (Exception e) { } fait disparaître l'échec sans trace, et le programme continue dans un état inconnu. Si l'on ne sait pas quoi faire, on LAISSE REMONTER — il suffit de ne rien écrire. 2) LES EMPLOYER COMME STRUCTURE DE CONTRÔLE : construire une exception capture la pile d'appels, c'est coûteux ; une exception doit rester exceptionnelle. 3) ATTRAPER TROP LARGE : catch (Exception e) ramasse aussi les bogues qu'il aurait mieux valu voir.

Chapitre 2 · 5 h

Modélisation UML

Diagramme de classes : attributs, méthodes, visibilité, associations et multiplicités, héritage et implémentation, agrégation et composition ; du diagramme au code, et retour.

Sept chapitres ont construit un vocabulaire : classe, attribut, visibilité, héritage, interface, composition. Il manque un moyen de montrer tout cela sans faire lire le code.

C'est ce que fait un diagramme de classes. Un programme de dix classes se lit en une minute sur un schéma, contre une heure dans les fichiers — et surtout, il se discute : c'est l'artefact qu'on met au tableau quand trois personnes doivent se mettre d'accord avant d'écrire.

UML compte quatorze types de diagrammes. Ce chapitre n'en traite qu'un, celui de classes, parce qu'il est le seul qu'on emploie vraiment en L2 — et parce qu'il est en correspondance directe avec le code.

La classe

┌─────────────────────────────┐│          Livre              │   ← nom (en italique si abstraite)├─────────────────────────────┤│ - titre : String            │   ← attributs│ - disponible : boolean      ││ + NB_MAX : int              │   ← souligné si static├─────────────────────────────┤│ + emprunter(qui : String)   │   ← méthodes│   : boolean                 ││ + estDisponible() : boolean ││ - verifier() : void         │└─────────────────────────────┘

Trois compartiments : le nom, les attributs, les méthodes. Les deux derniers peuvent être omis quand ils n'apportent rien au propos — un diagramme n'est pas une transcription.

Les symboles de visibilité reprennent exactement le chapitre 3 :

SymboleVisibilité
-private
+public
#protected
~paquetage (par défaut)

Deux conventions typographiques valent d'être connues, car elles se lisent d'un coup d'œil : souligné signifie static, italique signifie abstrait — pour une classe comme pour une méthode.

Les relations

C'est le cœur du chapitre, et chaque trait a un sens précis.

Livre ──────────▷ Document          héritage       (triangle creux, trait plein)Livre ┄┄┄┄┄┄┄┄┄▷ Empruntable        réalisation    (triangle creux, pointillés)Commande ◇────── Article            agrégation     (losange creux)Maison  ◆────── Piece               composition    (losange plein)Etudiant ─────── Cours              association    (trait simple)Facture ┄┄┄┄┄┄> Calculatrice        dépendance     (flèche pointillée)

L'héritage — triangle creux, trait plein, pointé vers le parent — traduit extends. C'est le « est-un » du chapitre 5, avec le même critère de substituabilité.

La réalisation — même triangle, mais en pointillés — traduit implements. La distinction visuelle est délibérée : hériter et implémenter ne sont pas la même chose.

L'association est un simple trait : deux classes se connaissent. En pratique, cela devient un attribut d'un côté ou des deux.

L'agrégation et la composition sont deux associations particulières, et leur différence est la seule subtilité du chapitre. Toutes deux expriment « a-un » ; ce qui les sépare est la durée de vie.

Dans une agrégation (losange creux), la partie survit au tout. Une Commande agrège des Article : supprimer la commande ne supprime pas les articles, qui existaient avant et continueront après.

Dans une composition (losange plein), la partie meurt avec le tout, et n'appartient qu'à lui. Une Maison compose ses Piece : détruire la maison détruit les pièces, et une pièce n'appartient pas à deux maisons.

Le test pratique : « si je supprime le tout, la partie a-t-elle encore un sens ? » Oui, agrégation ; non, composition.

Enfin la dépendance — flèche pointillée — signale un usage passager : une classe reçoit l'autre en paramètre ou l'emploie localement, sans la stocker.

Multiplicités

Un chiffre à chaque extrémité dit combien d'objets participent.

Bibliotheque  1 ◆────── 0..*  LivreLivre         *  ─────── 0..1  Lecteur
NotationSens
1exactement un
0..1zéro ou un — donc peut être null
* ou 0..*zéro ou plusieurs
1..*au moins un
2..4entre deux et quatre

Les multiplicités sont la partie la plus utile du diagramme, et la plus souvent oubliée. Elles répondent à des questions que le code ne pose pas explicitement : un livre peut-il être emprunté par deux lecteurs à la fois ? un lecteur sans emprunt est-il légal ? Un 0..1 du côté Lecteur répond « non » et « oui », et impose une décision qu'on aurait sinon prise par accident.

Voici ces relations réunies dans un diagramme interactif. Chaque trait porte son symbole UML — triangle d'héritage, losange de composition ou d'agrégation — et ses multiplicités ; déplacez les classes pour démêler les liens, et retrouvez la composition Bibliotheque ◆ Livre, l'héritage Livre ▷ Document et la réalisation de l'interface Empruntable.

Diagramme · Un diagramme de classes et ses relations

  • Document — attributs : - titre : String
  • Empruntable — méthodes : + emprunter() : boolean
  • Livre — attributs : - disponible : boolean — méthodes : + emprunter() : boolean
  • Bibliotheque — attributs : - livres : List<Livre>
  • Lecteur — attributs : - nom : String
  • Commande
  • Article — attributs : - prix : double

Relations

  • Livre ▷ hérite Document
  • Livre ▷ réalise Empruntable
  • Bibliotheque 1 ◆ compose 0..* Livre
  • Livre emprunté par 0..1 Lecteur
  • Commande ◇ agrège 0..* Article

Du diagramme au code

La traduction est presque mécanique, et c'est ce qui rend le diagramme utile plutôt que décoratif.

Sur le diagrammeDans le code Java
- titre : Stringprivate String titre;
Attribut soulignéstatic
Classe en italiqueabstract class
Triangle creux pleinextends
Triangle creux pointilléimplements
Association 1un attribut du type visé
Association 0..1un attribut, qui peut être null
Association *List<Type> ou Map<Clé, Type>
Compositionle tout crée ses parties dans son constructeur
Agrégationles parties sont reçues en paramètre

Les deux dernières lignes méritent qu'on s'y arrête, car elles transforment une nuance sémantique en différence de code visible :

class Maison {                          /* COMPOSITION */    private final List<Piece> pieces = new ArrayList<>();    public Maison(int nb) {        for (int i = 0; i < nb; i++) pieces.add(new Piece());   /* elle les crée */    }} class Commande {                        /* AGRÉGATION */    private final List<Article> articles;    public Commande(List<Article> articles) {        this.articles = articles;       /* elle les reçoit */    }}

Le chemin inverse compte autant : savoir lire du code existant et en tirer un diagramme est ce qu'on fait le premier jour sur un projet qu'on n'a pas écrit. C'est aussi ce que font les outils de rétro-ingénierie des ateliers de développement.

Ce qu'un diagramme ne dit pas, et ce qu'il ne faut pas en faire

Trois limites, pour ne pas prendre le dessin pour le programme.

Il ne dit rien du comportement. L'ordre des appels, les conditions, les boucles n'y figurent pas — c'est le rôle d'autres diagrammes UML, hors programme, comme celui de séquence.

Il vieillit. Un diagramme qui n'est pas maintenu devient un mensonge, plus nuisible qu'une absence de documentation. D'où deux usages viables : le dessin jetable au tableau avant de coder, et le diagramme régénéré depuis le code.

Il ne doit pas tout montrer. Un diagramme de quarante classes avec tous les getters est illisible et n'aide personne. On dessine ce qui répond à la question du moment : les relations principales, sans les accesseurs, sans les classes utilitaires.

Le modéliser-avant-tout des années 1990 a d'ailleurs largement été abandonné. Ce qui reste, et qui est ce qu'on enseigne ici : un croquis de cinq à dix classes, fait avant d'écrire, pour se mettre d'accord.

Quiz · 1 question

Quelle est la différence entre agrégation et composition, et comment la trancher ?

  • L'agrégation concerne un seul objet, la composition plusieursquestion de nombre
  • La DURÉE DE VIE : dans une composition la partie meurt avec le tout et n'appartient qu'à lui ; dans une agrégation elle lui survit. Test : « si je supprime le tout, la partie a-t-elle encore un sens ? »question de durée de vie
  • L'agrégation se traduit par un attribut, la composition par un héritagequestion de traduction

Réponse : Les deux expriment « a-un » et se traduisent toutes deux par un attribut — c'est bien pourquoi on les confond. Le critère est la DURÉE DE VIE et l'exclusivité. COMPOSITION (losange plein) : une Maison et ses Pièces — détruire la maison détruit les pièces, et une pièce n'appartient pas à deux maisons. AGRÉGATION (losange creux) : une Commande et ses Articles — supprimer la commande ne supprime pas les articles, qui existaient avant et continueront après. La différence se voit d'ailleurs dans le code : en composition, le tout CRÉE ses parties dans son constructeur ; en agrégation, il les REÇOIT en paramètre. Ni l'une ni l'autre n'a de rapport avec l'héritage, qui exprime « est-un ».

Quiz · 1 question

Sur un diagramme, la relation Livre * ── 0..1 Lecteur. Que dit-elle, et comment se traduit-elle ?

  • Un livre a exactement un lecteur, et un lecteur a plusieurs livres : deux attributs obligatoiresun lecteur obligatoire
  • Un livre est emprunté par ZÉRO OU UN lecteur — donc un attribut Lecteur pouvant être null, ce qui interdit deux emprunteurs simultanés — et un lecteur peut avoir plusieurs livres, donc une List du côté Lecteurzéro ou un, et une liste
  • La multiplicité est décorative : elle ne change rien au code produitdécorative

Réponse : Les multiplicités se lisent en croisé : celle placée près de Lecteur dit combien de lecteurs un livre voit, et réciproquement. Ici 0..1 côté Lecteur signifie qu'un livre est emprunté par au plus un lecteur — donc un attribut Lecteur qui peut valoir null, et l'impossibilité structurelle de deux emprunteurs simultanés. Le * côté Livre signifie qu'un lecteur peut détenir plusieurs livres, ce qui donne une List côté Lecteur. Loin d'être décoratives, les multiplicités répondent à des questions que le code ne pose pas explicitement — un livre peut-il avoir deux emprunteurs ? un lecteur sans emprunt est-il légal ? — et forcent une décision qu'on aurait sinon prise par accident. C'est la partie la plus utile du diagramme, et la plus souvent oubliée.

À vous

L'exercice fait les deux trajets, et c'est le second qui est formateur.

Du diagramme au code : on vous donne un diagramme décrit sous forme de données — classes, attributs, relations, multiplicités — et vous écrivez le générateur qui en produit les squelettes Java. Il devra traduire correctement 1, 0..1 et *, et distinguer agrégation et composition par la façon dont les parties arrivent.

Du code au diagramme : on vous donne des classes, et vous en extrayez les relations. Un cas est ambigu — un attribut de type List peut être une agrégation ou une composition — et c'est en regardant qui crée les objets que vous trancherez.

Un dernier contrôle vérifie les multiplicités sur des données : un livre à deux emprunteurs simultanés doit être signalé comme violant le diagramme.

Exercice de code

Générez du Java depuis un diagramme, puis retrouvez les relations à partir du code — agrégation ou composition ?

Point de départ

// ── Un diagramme, décrit sous forme de données ────────────────────────────
const DIAGRAMME = {
  classes: [
    { nom: "Document", abstraite: true,
      attributs: [{ v: "-", nom: "titre", type: "String" }],
      methodes:  [{ v: "+", nom: "decrire", type: "String", abstraite: true }] },
    { nom: "Livre", herite: "Document", implemente: ["Empruntable"],
      attributs: [{ v: "-", nom: "isbn", type: "long" },
                  { v: "+", nom: "NB_MAX", type: "int", statique: true }],
      methodes:  [{ v: "+", nom: "decrire", type: "String" }] },
    { nom: "Bibliotheque",
      attributs: [{ v: "-", nom: "nom", type: "String" }],
      methodes:  [] },
    { nom: "Lecteur", attributs: [{ v: "-", nom: "nom", type: "String" }], methodes: [] },
  ],
  relations: [
    { de: "Bibliotheque", vers: "Livre", type: "composition", multiplicite: "*" },
    { de: "Lecteur", vers: "Livre", type: "agregation", multiplicite: "*" },
    { de: "Livre", vers: "Lecteur", type: "association", multiplicite: "0..1" },
  ],
};

// ── 1. Du diagramme au code ───────────────────────────────────────────────
function versJava(d) {
  const lignes = [];
  for (const c of d.classes) {
    let entete = (c.abstraite ? "abstract " : "") + "class " + c.nom;
    if (c.herite) entete += " extends " + c.herite;
    if (c.implemente) entete += " implements " + c.implemente.join(", ");
    lignes.push("public " + entete + " {");

    const VISIBILITE = { "-": "private", "+": "public", "#": "protected", "~": "" };
    for (const a of c.attributs) {
      lignes.push("    " + [VISIBILITE[a.v], a.statique ? "static final" : "", a.type, a.nom]
        .filter(Boolean).join(" ") + ";");
    }
    // ← à écrire : traduire les relations dont cette classe est l'origine.
    //   « 1 » et « 0..1 » donnent un attribut simple, « * » donne une List.
    lignes.push("}");
    lignes.push("");
  }
  return lignes.join("\n");
}

// ── 2. Du code au diagramme ───────────────────────────────────────────────
const SOURCES = {
  Panier: [
    "class Panier {",
    "  private List<Article> articles;",
    "  Panier(List<Article> articles) { this.articles = articles; }",   // reçus
    "}",
  ],
  Maison: [
    "class Maison {",
    "  private List<Piece> pieces = new ArrayList<>();",
    "  Maison(int n) { for (int i=0;i<n;i++) pieces.add(new Piece()); }", // créées
    "}",
  ],
};

function versDiagramme(nom, source) {
  const texte = source.join("\n");
  const champ = /private List<(\w+)>/.exec(texte);
  if (!champ) return { de: nom, type: "aucune relation détectée" };
  // ← à écrire : agrégation ou composition ? Regardez QUI CRÉE les objets.
  return { de: nom, vers: champ[1], type: "à déterminer", multiplicite: "*" };
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Complétez versJava pour les trois multiplicités.
// 2. Complétez versDiagramme : le critère est « qui crée les objets ».
// 3. Écrivez un contrôle de multiplicité : un livre emprunté par deux
//    lecteurs simultanément viole le 0..1 du diagramme.

console.log(versJava(DIAGRAMME));

Solution

const DIAGRAMME = {
  classes: [
    { nom: "Document", abstraite: true,
      attributs: [{ v: "-", nom: "titre", type: "String" }], methodes: [] },
    { nom: "Livre", herite: "Document", implemente: ["Empruntable"],
      attributs: [{ v: "-", nom: "isbn", type: "long" },
                  { v: "+", nom: "NB_MAX", type: "int", statique: true }], methodes: [] },
    { nom: "Bibliotheque", attributs: [{ v: "-", nom: "nom", type: "String" }], methodes: [] },
    { nom: "Lecteur", attributs: [{ v: "-", nom: "nom", type: "String" }], methodes: [] },
  ],
  relations: [
    { de: "Bibliotheque", vers: "Livre", type: "composition", multiplicite: "*" },
    { de: "Lecteur", vers: "Livre", type: "agregation", multiplicite: "*" },
    { de: "Livre", vers: "Lecteur", type: "association", multiplicite: "0..1" },
  ],
};

const VISIBILITE = { "-": "private", "+": "public", "#": "protected", "~": "" };

function versJava(d) {
  const lignes = [];
  for (const c of d.classes) {
    let entete = (c.abstraite ? "abstract " : "") + "class " + c.nom;
    if (c.herite) entete += " extends " + c.herite;
    if (c.implemente) entete += " implements " + c.implemente.join(", ");
    lignes.push("public " + entete + " {");

    for (const a of c.attributs) {
      lignes.push("    " + [VISIBILITE[a.v], a.statique ? "static final" : "", a.type, a.nom]
        .filter(Boolean).join(" ") + ";");
    }

    const sortantes = d.relations.filter((r) => r.de === c.nom);
    for (const r of sortantes) {
      const pluriel = r.vers.toLowerCase() + "s";
      if (r.multiplicite === "*" || r.multiplicite === "1..*") {
        // Une multiplicité multiple devient une collection. En COMPOSITION,
        // le tout crée ses parties : la liste est initialisée sur place et
        // final. En AGRÉGATION, elle est reçue par le constructeur.
        const init = r.type === "composition" ? " = new ArrayList<>()" : "";
        const finalite = r.type === "composition" ? "final " : "";
        lignes.push("    private " + finalite + "List<" + r.vers + "> " + pluriel + init + ";");
      } else if (r.multiplicite === "0..1") {
        lignes.push("    private " + r.vers + " " + r.vers.toLowerCase() +
                    ";   // peut être null");
      } else {
        lignes.push("    private final " + r.vers + " " + r.vers.toLowerCase() + ";");
      }
    }
    lignes.push("}");
    lignes.push("");
  }
  return lignes.join("\n");
}

console.log("— 1. du diagramme au code —");
console.log(versJava(DIAGRAMME));

console.log("— 2. du code au diagramme —");
const SOURCES = {
  Panier: ["class Panier {", "  private List<Article> articles;",
           "  Panier(List<Article> articles) { this.articles = articles; }", "}"],
  Maison: ["class Maison {", "  private List<Piece> pieces = new ArrayList<>();",
           "  Maison(int n) { for (int i=0;i<n;i++) pieces.add(new Piece()); }", "}"],
};

function versDiagramme(nom, source) {
  const texte = source.join("\n");
  const champ = /private (?:final )?List<(\w+)>/.exec(texte);
  if (!champ) return { de: nom, type: "aucune relation détectée" };
  const partie = champ[1];
  // LE critère : qui crée les objets ? Un « new Partie() » à l'intérieur
  // signale une composition ; une réception en paramètre, une agrégation.
  const cree = new RegExp("new " + partie + "\\s*\\(").test(texte);
  const recu = new RegExp("\\(\\s*List<" + partie + ">").test(texte);
  const type = cree ? "composition" : recu ? "agrégation" : "indéterminé";
  return { de: nom, vers: partie, type, multiplicite: "*",
           indice: cree ? "crée ses parties" : "les reçoit en paramètre" };
}

for (const [nom, src] of Object.entries(SOURCES)) {
  const r = versDiagramme(nom, src);
  console.log("   " + r.de.padEnd(10) + (r.type === "composition" ? "◆" : "◇") +
              "──── " + r.vers.padEnd(9) + r.multiplicite.padEnd(4) +
              r.type.padEnd(13) + "(" + r.indice + ")");
}
console.log("   Le même attribut List<X> donne deux relations différentes : la");
console.log("   nuance n'est pas dans le type, elle est dans QUI CRÉE.");

console.log("");
console.log("— 3. contrôle des multiplicités —");
// Le diagramme dit : Livre 0..1 Lecteur. Deux emprunteurs sont donc interdits.
const emprunts = [
  { livre: "Dune", lecteur: "Ana" },
  { livre: "Ubik", lecteur: "Bo" },
  { livre: "Dune", lecteur: "Cy" },     // viole le 0..1
];
const parLivre = new Map();
for (const e of emprunts) {
  parLivre.set(e.livre, [...(parLivre.get(e.livre) ?? []), e.lecteur]);
}
for (const [livre, lecteurs] of parLivre) {
  const ok = lecteurs.length <= 1;
  console.log("   " + livre.padEnd(6) + lecteurs.length + " lecteur(s) : " +
    lecteurs.join(", ") + (ok ? "   conforme au 0..1" : "   VIOLE le 0..1 du diagramme"));
}
console.log("   Sans la multiplicité, ce cas serait passé inaperçu jusqu'à ce");
console.log("   qu'un utilisateur s'en plaigne.");

En travaux pratiques

Travaux pratiques 8 · 2 h

Le diagramme comme outil de décision

Dessiner AVANT de coder, puis rétro-modéliser le code écrit, et mesurer l'écart entre les deux — c'est l'écart qui est instructif, pas le diagramme.

Avant de commencer

  • Les TP 1 à 7 : la médiathèque en état de marche
  • Un outil de diagramme, ou du papier

Énoncé

  1. Rétro-modéliserDessinez le diagramme de classes de votre médiathèque telle qu'elle est aujourd'hui. Classes, attributs, méthodes publiques, et les relations entre elles.
  2. Nommer les relationsPour chaque trait de votre diagramme, dites s'il s'agit d'un héritage, d'une composition, d'une agrégation ou d'une simple association, et justifiez. Indice : La question qui tranche entre composition et agrégation : si le tout disparaît, la partie disparaît-elle aussi ?
  3. Les cardinalitésAnnotez chaque association de ses cardinalités. Vérifiez ensuite dans le code que chacune est réellement appliquée.
  4. Ce que le diagramme révèleRepérez sur votre dessin : une classe qui connaît trop de monde, une relation dans les deux sens, un cycle de dépendances. Corrigez-en un dans le code.
  5. Concevoir en amontNouvelle exigence : gérer les adhérents et les emprunts avec dates. Dessinez le modèle AVANT d'écrire une ligne. Discutez-en à deux, dessin contre dessin.
  6. Le diagramme de séquenceDessinez le déroulement d'un emprunt : qui appelle qui, dans quel ordre, et où la règle métier est vérifiée. Trouvez si la vérification est au bon endroit.
  7. Coder puis comparerImplémentez ce que vous avez dessiné, puis rétro-modélisez à nouveau. Listez les écarts et dites, pour chacun, qui avait raison — le dessin ou le code.

C'est réussi quand

  • Vous distinguez composition et agrégation sur un exemple de votre propre code
  • Vos cardinalités sont réellement appliquées, ou bien vous savez dire où elles ne le sont pas
  • Vous listez au moins deux écarts entre le dessin initial et le code final

Correction

Le diagramme de la médiathèque
┌──────────────────┐          ┌──────────────────┐
│   Mediatheque    │◆────────▶│    Document      │
├──────────────────┤ 1    0..*├──────────────────┤
│ -documents: List │          │ #titre: String   │
├──────────────────┤          │ #disponible: bool│
│ +ajouter()       │          ├──────────────────┤
│ +chercher()      │          │ +emprunter()     │
│ +penalite()      │          │ +penaliteParJour()│ {abstract}
└──────────────────┘          └────────△─────────┘
                                     │
                      ┌──────────────┼──────────────┐
                   ┌──┴───┐      ┌───┴──┐      ┌────┴───┐
                   │Livre │      │ Dvd  │      │ Revue  │
                   └──────┘      └──────┘      └────────┘

◆── composition   △── héritage   - privé  # protégé  + public

Le diagramme rend visible en un coup d'œil ce qu'il faut lire cinq fichiers pour comprendre : une hiérarchie à trois feuilles et une seule relation de possession. C'est là son utilité — pas remplacer le code, mais montrer sa forme.

Composition ou agrégation
COMPOSITION (losange PLEIN) : le tout POSSÈDE la partie
si le tout disparaît, la partie disparaît
Mediatheque ◆── Document ? discutable : un document peut être
transféré ailleurs → plutôt une AGRÉGATION

AGRÉGATION (losange VIDE) : le tout RÉFÉRENCE la partie
Emprunt ◇── Adherent : l'adhérent survit à l'emprunt

la vraie composition, ici :
Emprunt ◆── DateEmprunt : la date n'existe que par l'emprunt

La question qui tranche est celle du cycle de vie, pas celle du sentiment de possession. Dans le code, la composition se traduit par un objet créé et détruit par son propriétaire, et jamais partagé ; l'agrégation, par une référence reçue de l'extérieur. C'est aussi la question qui décide s'il faut une copie défensive — celle du TP 3.

Le modèle des emprunts
Adherent 1 ──── 0..* Emprunt 0..* ──── 1 Document

class Emprunt {
  private final Adherent adherent;      /* agrégation */
  private final Document document;      /* agrégation */
  private final LocalDate debut;
  private LocalDate retour;             /* null tant que non rendu */
}

/* l'erreur fréquente : mettre l'emprunteur DANS Document
 → un document ne peut alors avoir qu'un emprunt, jamais d'historique */

Une association porteuse d'informations — les dates — devient une CLASSE. C'est l'erreur de conception la plus fréquente à ce stade : loger la relation dans l'un des deux objets, ce qui interdit l'historique et casse dès qu'un second emprunt survient. Le diagramme la fait voir immédiatement, le code ne la révèle qu'à l'usage.

Le diagramme de séquence, et ce qu'il déplace
Interface   Mediatheque   Document   Adherent
  │            │             │          │
  │─emprunter─▶│             │          │
  │            │──peutEmprunter()───────▶│   quota atteint ?
  │            │◀────true────────────────│
  │            │──emprunter()▶│              disponible ?
  │            │◀───true──────│
  │            │─── new Emprunt() ───▶
  │◀───ok──────│

la règle « un adhérent ne dépasse pas 5 emprunts » est chez
l'ADHÉRENT, pas dans Mediatheque : c'est lui qui la connaît

Le diagramme de séquence répond à une question que le diagramme de classes ne pose pas : qui DÉCIDE. Ici, il fait apparaître que la règle des cinq emprunts était écrite dans Mediatheque alors qu'elle dépend de l'état de l'adhérent — le « tell, don't ask » du TP 3, rendu visible par un dessin.

L'écart entre le dessin et le code
écarts typiques après implémentation :
- une classe utilitaire non prévue (validation, formatage)
- une interface extraite pour pouvoir tester
- une relation bidirectionnelle prévue, rendue unidirectionnelle
  (la bidirectionnelle est pénible à maintenir cohérente)
- une méthode prévue, jamais appelée → supprimée

le CODE a souvent raison sur le détail,
le DESSIN a souvent raison sur la structure

Le diagramme n'est pas un contrat à faire respecter : c'est un outil de discussion et de repérage. Un diagramme entretenu à l'identique du code ne sert à rien — la lecture du code y suffirait. Ce qui vaut, c'est le diagramme de conception, fait avant, sur un tableau, à deux, et jeté ensuite ; et la rétro-modélisation, faite après, pour voir la forme de ce qu'on a réellement construit.

Ce que la suite en fait

Le bloc V met tout en œuvre. Le chapitre 9 donne les collections qui matérialisent les multiplicités *List, Map, Set — et l'on verra que le choix entre elles est lui-même une décision de modélisation : un Set interdit les doublons, une Map impose une clé unique.

Le chapitre 10 partira d'un énoncé en français pour arriver à un diagramme, puis au code. La notation de ce chapitre y sera l'outil de travail, et non plus le sujet.

À retenir

Flashcards · 5 cartes

Comment se lisent les symboles d'une classe UML ?
Trois compartiments : nom, attributs, méthodes — les deux derniers pouvant être omis, un diagramme n'étant pas une transcription. Visibilité : − private, + public, # protected, ~ paquetage. Deux conventions typographiques qui se lisent d'un coup d'œil : SOULIGNÉ signifie static, ITALIQUE signifie abstrait, pour une classe comme pour une méthode.
Quels traits pour l'héritage, l'implémentation, l'association et la dépendance ?
HÉRITAGE : triangle creux, trait PLEIN, pointé vers le parent — traduit extends. RÉALISATION : même triangle en POINTILLÉS — traduit implements ; la distinction visuelle est délibérée. ASSOCIATION : trait simple, deux classes se connaissent, ce qui devient un attribut d'un côté ou des deux. DÉPENDANCE : flèche pointillée, usage passager — la classe reçoit l'autre en paramètre ou l'emploie localement, sans la stocker.
Agrégation ou composition : le critère et sa trace dans le code ?
La DURÉE DE VIE et l'exclusivité. COMPOSITION (losange plein) : la partie meurt avec le tout et n'appartient qu'à lui — une Maison et ses Pièces. AGRÉGATION (losange creux) : la partie survit au tout — une Commande et ses Articles. Test : « si je supprime le tout, la partie a-t-elle encore un sens ? ». Dans le code : en composition le tout CRÉE ses parties dans son constructeur, en agrégation il les REÇOIT en paramètre.
Comment traduit-on les multiplicités en code, et pourquoi comptent-elles ?
1 → un attribut du type visé ; 0..1 → un attribut pouvant être null ; * ou 0..* → une List ou une Map ; 1..* → une liste jamais vide, invariant à maintenir. Elles comptent parce qu'elles répondent à des questions que le code ne pose pas explicitement — un livre peut-il avoir deux emprunteurs ? un lecteur sans emprunt est-il légal ? — et forcent une décision qu'on prendrait sinon par accident. C'est la partie la plus utile du diagramme, et la plus souvent oubliée.
Quelles sont les trois limites d'un diagramme de classes ?
1) IL NE DIT RIEN DU COMPORTEMENT : ordre des appels, conditions, boucles n'y figurent pas. 2) IL VIEILLIT : non maintenu, il devient un mensonge plus nuisible qu'une absence de documentation — d'où deux usages viables, le croquis JETABLE avant de coder et le diagramme RÉGÉNÉRÉ depuis le code. 3) IL NE DOIT PAS TOUT MONTRER : quarante classes avec tous les getters n'aident personne. Ce qui reste du modéliser-avant-tout des années 1990 : un croquis de cinq à dix classes, fait avant d'écrire, pour se mettre d'accord.