ProjetDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Programmation orientée objet · C5 Mise en pratique · Chapitre 2 · 3 h

Projet

Analyser un énoncé, en tirer un diagramme de classes, implémenter par incréments, tester, et restituer.

L'énoncé tient en trois lignes :

Réaliser une application de gestion de bibliothèque. Les adhérents empruntent des ouvrages pour une durée limitée. Un ouvrage peut exister en plusieurs exemplaires. Les retards donnent lieu à une pénalité.

Le réflexe est d'ouvrir l'éditeur et d'écrire public class Main. C'est la façon la plus sûre de produire, trois semaines plus tard, une classe de huit cents lignes que personne ne peut modifier.

Ce dernier chapitre n'introduit aucune notion. Il dit ce qu'on fait avant d'écrire la première ligne, et dans quel ordre — c'est le seul moment du semestre où l'on conçoit au lieu d'appliquer.

Analyser l'énoncé

La méthode est vieille, un peu naïve, et elle fonctionne : souligner les noms, entourer les verbes.

Les noms communs sont des candidats-classes : adhérent, ouvrage, exemplaire, emprunt, pénalité, bibliothèque. Les verbes sont des candidats-méthodes : emprunter, rendre, calculer une pénalité, rechercher.

Vient ensuite le filtrage, et c'est là que le travail commence — un nom n'est pas toujours une classe.

« Durée » n'est pas une classe : c'est un attribut d'Emprunt, ou une constante de la bibliothèque. Un nom qui n'a ni comportement ni identité propre est une donnée.

« Pénalité » est douteux. Si c'est un simple montant, c'est une valeur calculée. Si elle a une date, un statut payé ou non, un historique — alors elle a une identité, et c'est une classe. L'énoncé ne tranche pas : c'est une question à poser, et savoir qu'il faut la poser vaut mieux que deviner.

« Ouvrage » et « exemplaire » sont deux classes distinctes, et c'est la vraie difficulté de cet énoncé. « Un ouvrage peut exister en plusieurs exemplaires » dit exactement cela : Dune est un ouvrage — un titre, un auteur, un ISBN — dont la bibliothèque possède trois exemplaires physiques, chacun avec sa cote et son état. On n'emprunte pas un ouvrage, on emprunte un exemplaire.

Les fusionner est l'erreur numéro un sur cet énoncé, et elle se paie immédiatement : on ne sait plus dire combien d'exemplaires sont disponibles.

Le filtre final est celui du chapitre 1 : chaque classe retenue doit se décrire en une phrase sans « et ». Si l'on n'y arrive pas, il y en a deux.

Concevoir le diagramme

On dessine ensuite, avec la notation du chapitre 8 — et l'on vise cinq à huit classes. Au-delà sur un projet de L2, c'est généralement qu'on a modélisé des détails.

Adherent  1 ───── 0..*  Emprunt  0..*  ─────  1  Exemplaire  *  ─────  1  Ouvrage

Trois décisions se lisent dans cette seule ligne, et chacune était une question ouverte.

Emprunt est devenu une classe à part entière, et non une simple association. La raison est qu'il porte des données propres — date de début, date de retour prévue, date de retour effective — et un comportement : calculer son retard. Une association qui porte des attributs est une classe.

La multiplicité 0..* du côté Emprunt dit qu'un adhérent peut n'avoir aucun emprunt en cours, et plusieurs à la fois. Un 0..3 dirait « trois au maximum », ce qui est une règle métier qu'il faut alors faire respecter quelque part.

Et Exemplaire → Ouvrage en * ── 1 traduit exactement la phrase de l'énoncé.

Une fois le diagramme posé, on le vérifie en y faisant marcher un scénario : « Ana emprunte Dune, le rend avec cinq jours de retard, paie sa pénalité ». On suit le chemin sur le dessin. Si une information manque — comment retrouver un exemplaire libre de cet ouvrage ? — le diagramme est incomplet, et il vaut mieux s'en apercevoir maintenant.

Implémenter par incréments

C'est ici que la plupart des projets de L2 se perdent, et l'erreur est toujours la même : écrire toutes les classes, puis tous les attributs, puis toutes les méthodes, et essayer de faire tourner l'ensemble la veille du rendu. Rien ne compile jamais avant la fin, et le premier test a lieu quand il est trop tard.

La méthode qui marche est la tranche verticale : choisir un scénario complet, le faire fonctionner de bout en bout, puis passer au suivant.

Incrément 1   créer un ouvrage, un exemplaire, les afficherIncrément 2   un adhérent emprunte un exemplaire disponibleIncrément 3   le refus si l'exemplaire est déjà emprunté   (exceptions)Incrément 4   le retour, et le calcul du retardIncrément 5   la pénalité, et l'historiqueIncrément 6   la recherche par titre et par auteur          (collections)

Trois bénéfices, et le troisième est le plus important.

Le programme compile et s'exécute en permanence. Une régression se détecte le jour même, pas trois semaines plus tard.

On a toujours quelque chose à montrer. Un projet à moitié fait mais qui tourne vaut mieux qu'un projet complet qui ne compile pas — c'est vrai pour la note comme pour la suite.

La conception se corrige en marchant. L'incrément 3 révélera peut-être qu'il faut une classe Reservation à laquelle on n'avait pas pensé. La découvrir en écrivant coûte une heure ; la découvrir à la fin coûte une refonte.

Tester

Un jeu de tests, même minimal, n'est pas un supplément : c'est ce qui permet de modifier sans casser.

Quatre choses méritent d'être testées dans un programme objet, et elles ne sont pas les mêmes qu'en programmation impérative.

Les invariants du chapitre 3 : un constructeur refuse-t-il bien un ISBN vide, un solde négatif, une date de retour antérieure à la date d'emprunt ?

Les cas limites, comme au chapitre 10 du cours de programmation : emprunter le dernier exemplaire disponible, rendre un jour pile après l'échéance, une liste vide.

Le comportement polymorphe : une méthode redéfinie fait-elle bien ce qu'elle doit dans chaque sous-classe ? C'est le seul endroit où le chapitre 6 peut se vérifier mécaniquement.

Les exceptions : la bonne exception est-elle levée dans le bon cas ? Un test qui vérifie qu'un emprunt impossible échoue vaut autant qu'un test de succès.

Un dernier bénéfice, moins évident : écrire un test, c'est être le premier client de sa propre API. Si le test est pénible à écrire — s'il faut créer sept objets pour en tester un — la conception est trop couplée. Le test révèle le défaut avant l'utilisateur.

Restituer

Le rendu compte, et il tient en peu de chose.

Un fichier de description — ce que fait le programme, comment le compiler et le lancer, ce qui est fait et ce qui ne l'est pas. La dernière partie est celle qu'on omet, et c'est la plus appréciée : un projet honnête sur ses limites est jugé plus favorablement qu'un projet qui les cache.

Le diagramme de classes, tel qu'il est à la fin — pas celui du début. S'ils diffèrent, c'est normal, et l'écart vaut d'être expliqué en deux phrases : c'est la preuve qu'on a conçu en marchant.

Un jeu de données de démonstration, pour que le correcteur n'ait pas à en inventer.

Les cinq pièges classiques

Ils reviennent chaque année, et chacun contredit un chapitre précis.

La classe divine : un Main de six cents lignes qui fait tout, entouré de classes anémiques réduites à des getters. C'est le chapitre 1 et le chapitre 3 ignorés — et la marque distinctive est que les autres classes n'ont aucune méthode intéressante.

L'encapsulation absente : tout en public, y compris les attributs, parce que « c'est plus simple pour y accéder depuis le Main ». Cela signale généralement le piège précédent.

L'héritage partout : une hiérarchie de six niveaux, parce qu'on a trouvé des attributs communs. Le test « est-un » du chapitre 5 en élimine la moitié, et la composition règle le reste.

Le grand assemblage final : voir la section précédente.

L'oubli du diagramme : le code est écrit d'abord, le diagramme dessiné la veille pour satisfaire à la consigne. Il ne sert alors à rien — et cela se voit.

Quiz · 1 question

Sur l'énoncé « un ouvrage peut exister en plusieurs exemplaires », faut-il une ou deux classes ?

  • Une seule : un exemplaire est un ouvrage, avec un attribut nombre indiquant combien il en resteune seule
  • Deux classes distinctes : l'OUVRAGE porte le titre, l'auteur et l'ISBN ; l'EXEMPLAIRE porte la cote, l'état et la disponibilité. On n'emprunte pas un ouvrage, on emprunte un exemplairedeux, en association
  • Deux classes liées par héritage : Exemplaire extends Ouvrage, pour hériter du titre et de l'auteurdeux, en héritage

Réponse : C'est la difficulté centrale de cet énoncé, et la fusionner est l'erreur numéro un. Un ouvrage et un exemplaire n'ont ni les mêmes attributs ni la même identité : Dune est UN ouvrage — un titre, un auteur, un ISBN — dont la bibliothèque possède TROIS exemplaires physiques, chacun avec sa cote, son état d'usure et sa disponibilité propre. Un simple compteur ne suffit pas : il ne dit pas quel exemplaire est chez qui, ni lequel est abîmé. La relation est une association « * ── 1 » : plusieurs exemplaires pour un ouvrage. L'héritage serait la faute du chapitre 5 — un exemplaire n'EST pas un ouvrage, il est un exemplaire DE cet ouvrage, ce qui est une association et non une nature. Le test le confirme : partout où un Ouvrage est attendu, pourrait-on donner un Exemplaire ? Non.

Quiz · 1 question

Pourquoi implémenter par tranches verticales plutôt que classe par classe ?

  • Parce que c'est plus rapide : on écrit moins de code au totalplus rapide
  • Parce que le programme COMPILE ET TOURNE en permanence : les régressions se voient le jour même, on a toujours quelque chose à montrer, et la conception se corrige en marchant plutôt qu'à la fintoujours exécutable
  • Parce que les tranches verticales évitent d'avoir à écrire un diagramme de classesévite le diagramme

Réponse : Le volume de code est le même — ce n'est pas une question de rapidité d'écriture mais de RISQUE. Écrire toutes les classes, puis tous les attributs, puis toutes les méthodes, laisse un programme qui ne compile jamais avant la fin : le premier test a lieu quand il est trop tard pour changer quoi que ce soit. En prenant un scénario complet et en le faisant fonctionner de bout en bout, on obtient trois choses. Une régression se détecte le jour même. On a toujours un livrable, et un projet à moitié fait qui tourne vaut mieux qu'un projet complet qui ne compile pas. Et surtout, la conception se corrige en marchant : découvrir à l'incrément 3 qu'il manque une classe coûte une heure, la découvrir à la fin coûte une refonte. Le diagramme reste nécessaire — il est même ce qui permet de découper en tranches.

À vous

L'exercice conduit l'analyse de l'énoncé d'ouverture, du texte au modèle.

D'abord l'extraction : le programme repère les noms et les verbes, et vous décidez, pour chacun, s'il devient une classe, un attribut, une méthode, ou rien. Le corrigé justifie chaque verdict — y compris les deux cas douteux, « pénalité » et « durée ».

Ensuite la vérification du modèle par un scénario : « Ana emprunte Dune, le rend avec cinq jours de retard ». Vous faites marcher le scénario sur le diagramme décrit en données, et le programme signale ce qui manque pour le mener à bout.

Enfin un contrôleur de conception, appliqué à deux versions du même projet : il compte les méthodes par classe, repère la classe divine, les attributs publics et les hiérarchies trop profondes. Vous verrez qu'un modèle correct et un modèle raté se distinguent à des chiffres simples.

Exercice de code

Tirez un modèle d'un énoncé, éprouvez-le par un scénario, puis auditez deux conceptions.

Point de départ

const ENONCE = [
  "Réaliser une application de gestion de bibliothèque.",
  "Les adhérents empruntent des ouvrages pour une durée limitée.",
  "Un ouvrage peut exister en plusieurs exemplaires.",
  "Les retards donnent lieu à une pénalité.",
].join(" ");

// ── 1. Du texte aux candidats ─────────────────────────────────────────────
const NOMS = ["bibliothèque", "adhérent", "ouvrage", "durée", "exemplaire", "retard", "pénalité"];
const VERBES = ["emprunter", "rendre", "calculer", "rechercher"];

// Pour chaque nom : "classe", "attribut", ou "à decider" (question à poser).
const VERDICTS = {
  // ← à écrire, avec une justification en une ligne
};

// ── 2. Le modèle, décrit en données ───────────────────────────────────────
const MODELE = {
  classes: {
    Adherent:   { attributs: ["nom"], methodes: ["emprunter", "rendre"] },
    Ouvrage:    { attributs: ["titre", "auteur", "isbn"], methodes: [] },
    Exemplaire: { attributs: ["cote", "etat", "disponible"], methodes: ["estDisponible"] },
    Emprunt:    { attributs: ["debut", "prevu", "effectif"], methodes: ["retardEnJours", "penalite"] },
  },
  relations: [
    { de: "Adherent", vers: "Emprunt", mult: "0..*" },
    { de: "Emprunt", vers: "Exemplaire", mult: "1" },
    { de: "Exemplaire", vers: "Ouvrage", mult: "1" },
  ],
};

// Fait marcher un scénario sur le modèle et signale ce qui manque.
function verifierScenario(modele, etapes) {
  const manques = [];
  for (const e of etapes) {
    const c = modele.classes[e.classe];
    if (!c) { manques.push("classe absente : " + e.classe); continue; }
    // ← à écrire : vérifier que la méthode et les attributs requis existent
  }
  return manques;
}

const SCENARIO = [
  { classe: "Adherent",   methode: "emprunter",     attributs: [] },
  { classe: "Exemplaire", methode: "estDisponible", attributs: ["disponible"] },
  { classe: "Emprunt",    methode: "retardEnJours", attributs: ["prevu", "effectif"] },
  { classe: "Emprunt",    methode: "penalite",      attributs: [] },
  { classe: "Ouvrage",    methode: "exemplairesLibres", attributs: [] },   // manque ?
];

// ── 3. Mesurer une conception ─────────────────────────────────────────────
const PROJET_RATE = {
  Main:       { methodes: 34, attributsPublics: 12, profondeur: 0 },
  Livre:      { methodes: 0,  attributsPublics: 5,  profondeur: 0 },
  Adherent:   { methodes: 0,  attributsPublics: 4,  profondeur: 0 },
  LivrePoche: { methodes: 1,  attributsPublics: 0,  profondeur: 4 },
};
const PROJET_CORRECT = {
  Bibliotheque: { methodes: 6, attributsPublics: 0, profondeur: 0 },
  Ouvrage:      { methodes: 4, attributsPublics: 0, profondeur: 0 },
  Exemplaire:   { methodes: 5, attributsPublics: 0, profondeur: 0 },
  Emprunt:      { methodes: 5, attributsPublics: 0, profondeur: 0 },
  Adherent:     { methodes: 5, attributsPublics: 0, profondeur: 0 },
};

function auditer(nom, projet) {
  // ← à écrire : signaler la classe divine, les classes anémiques, les
  //   attributs publics et les hiérarchies de plus de trois niveaux.
  console.log("   " + nom + " : à auditer");
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Remplissez VERDICTS pour les sept noms.
// 2. Complétez verifierScenario : que manque-t-il pour mener le scénario ?
// 3. Écrivez auditer() et comparez les deux projets.

console.log(ENONCE);

Solution

const ENONCE = [
  "Réaliser une application de gestion de bibliothèque.",
  "Les adhérents empruntent des ouvrages pour une durée limitée.",
  "Un ouvrage peut exister en plusieurs exemplaires.",
  "Les retards donnent lieu à une pénalité.",
].join(" ");

console.log("— 1. du texte aux candidats —");
const VERDICTS = {
  bibliothèque: ["classe", "porte le catalogue et les règles de prêt : identité et comportement"],
  adhérent:     ["classe", "identité propre, emprunts en cours, comportement"],
  ouvrage:      ["classe", "titre, auteur, ISBN — l'oeuvre, indépendamment des copies"],
  exemplaire:   ["classe", "copie PHYSIQUE : cote, état, disponibilité. On emprunte celui-ci"],
  durée:        ["attribut", "aucun comportement, aucune identité : un champ d'Emprunt ou une constante"],
  retard:       ["attribut", "valeur CALCULÉE à partir de deux dates : une méthode d'Emprunt"],
  pénalité:     ["à décider", "montant calculé ? ou objet avec date, statut payé, historique ? QUESTION À POSER"],
};
for (const [nom, [verdict, pourquoi]] of Object.entries(VERDICTS)) {
  console.log("   " + nom.padEnd(14) + verdict.padEnd(11) + pourquoi);
}
console.log("   Et un candidat que l'énoncé ne NOMME PAS : Emprunt. Il apparaît");
console.log("   parce que « emprunter pour une durée » porte des dates — une");
console.log("   association qui porte des attributs est une classe.");

const MODELE = {
  classes: {
    Adherent:   { attributs: ["nom"], methodes: ["emprunter", "rendre"] },
    Ouvrage:    { attributs: ["titre", "auteur", "isbn"], methodes: [] },
    Exemplaire: { attributs: ["cote", "etat", "disponible"], methodes: ["estDisponible"] },
    Emprunt:    { attributs: ["debut", "prevu", "effectif"], methodes: ["retardEnJours", "penalite"] },
  },
  relations: [
    { de: "Adherent", vers: "Emprunt", mult: "0..*" },
    { de: "Emprunt", vers: "Exemplaire", mult: "1" },
    { de: "Exemplaire", vers: "Ouvrage", mult: "1" },
  ],
};

function verifierScenario(modele, etapes) {
  const manques = [];
  for (const e of etapes) {
    const c = modele.classes[e.classe];
    if (!c) { manques.push("classe absente : " + e.classe); continue; }
    if (!c.methodes.includes(e.methode)) {
      manques.push(e.classe + "." + e.methode + "() n'existe pas dans le modèle");
    }
    for (const a of e.attributs) {
      if (!c.attributs.includes(a)) manques.push(e.classe + "." + a + " manque");
    }
  }
  return manques;
}

const SCENARIO = [
  { classe: "Adherent",   methode: "emprunter",         attributs: [] },
  { classe: "Exemplaire", methode: "estDisponible",     attributs: ["disponible"] },
  { classe: "Emprunt",    methode: "retardEnJours",     attributs: ["prevu", "effectif"] },
  { classe: "Emprunt",    methode: "penalite",          attributs: [] },
  { classe: "Ouvrage",    methode: "exemplairesLibres", attributs: [] },
];

console.log("");
console.log("— 2. le scénario sur le modèle —");
const manques = verifierScenario(MODELE, SCENARIO);
for (const m of manques) console.log("   MANQUE : " + m);
console.log("   « Ana veut emprunter Dune » suppose de retrouver un exemplaire");
console.log("   LIBRE de cet ouvrage. Le modèle ne le permet pas : la relation va");
console.log("   d'Exemplaire vers Ouvrage, pas l'inverse. Il faut soit une");
console.log("   navigabilité dans les deux sens, soit une méthode de Bibliotheque.");
console.log("   Découvrir cela ICI coûte une minute ; le découvrir en codant, une refonte.");

console.log("");
console.log("— 3. mesurer une conception —");
const PROJET_RATE = {
  Main:       { methodes: 34, attributsPublics: 12, profondeur: 0 },
  Livre:      { methodes: 0,  attributsPublics: 5,  profondeur: 0 },
  Adherent:   { methodes: 0,  attributsPublics: 4,  profondeur: 0 },
  LivrePoche: { methodes: 1,  attributsPublics: 0,  profondeur: 4 },
};
const PROJET_CORRECT = {
  Bibliotheque: { methodes: 6, attributsPublics: 0, profondeur: 0 },
  Ouvrage:      { methodes: 4, attributsPublics: 0, profondeur: 0 },
  Exemplaire:   { methodes: 5, attributsPublics: 0, profondeur: 0 },
  Emprunt:      { methodes: 5, attributsPublics: 0, profondeur: 0 },
  Adherent:     { methodes: 5, attributsPublics: 0, profondeur: 0 },
};

function auditer(nom, projet) {
  const entrees = Object.entries(projet);
  const total = entrees.reduce((s, [, c]) => s + c.methodes, 0);
  const alertes = [];
  for (const [classe, c] of entrees) {
    // Classe divine : elle concentre l'essentiel des méthodes du projet.
    if (total > 0 && c.methodes / total > 0.5) {
      alertes.push(classe + " concentre " + Math.round((c.methodes / total) * 100) +
                   " % des méthodes : CLASSE DIVINE");
    }
    // Classe anémique : des données, aucun comportement.
    if (c.methodes === 0 && c.attributsPublics > 0) {
      alertes.push(classe + " n'a aucune méthode : CLASSE ANÉMIQUE");
    }
    if (c.attributsPublics > 0) {
      alertes.push(classe + " expose " + c.attributsPublics + " attributs publics");
    }
    if (c.profondeur > 3) {
      alertes.push(classe + " est à " + c.profondeur + " niveaux d'héritage : trop profond");
    }
  }
  console.log("   " + nom + " — " + entrees.length + " classes, " + total + " méthodes");
  if (alertes.length === 0) console.log("      aucune alerte");
  else for (const a of alertes) console.log("      " + a);
}

auditer("projet raté", PROJET_RATE);
auditer("projet correct", PROJET_CORRECT);
console.log("   Un modèle raté et un modèle correct se distinguent à des chiffres");
console.log("   simples : répartition des méthodes, attributs publics, profondeur.");
console.log("   Aucun ne mesure la qualité — mais tous signalent où regarder.");

En travaux pratiques

Travaux pratiques 10 · 3 h

Rendre le projet, et savoir le défendre

Assembler la médiathèque des neuf séances en un projet livrable, testé, et dont chaque décision de conception peut être justifiée à l'oral.

Avant de commencer

  • Les TP 1 à 9
  • Un dépôt Git, et JUnit

Énoncé

  1. L'inventaireListez ce que votre médiathèque fait aujourd'hui, et ce que l'énoncé demandait. Signalez explicitement les manques plutôt que de les laisser découvrir.
  2. Séparer les couchesRéorganisez en trois paquetages : domaine, persistance, interface. Vérifiez qu'aucune classe du domaine n'importe rien de l'interface. Indice : Une dépendance dans le mauvais sens se voit à un import.
  3. Tester le domaineÉcrivez des tests sur les règles métier : emprunt d'un document indisponible, quota d'adhérent, calcul de pénalité, retour. Un test par règle.
  4. Tester les cas limitesAjoutez les cas limites : catalogue vide, adhérent sans emprunt, retard de zéro jour, emprunt le jour même du retour, document rendu deux fois.
  5. PersisterSauvegardez et rechargez le catalogue dans un fichier. Vérifiez qu'un aller-retour complet redonne exactement l'état initial.
  6. Le remplacementÉcrivez une seconde implémentation de la persistance, dans un autre format. Branchez-la SANS modifier une seule ligne du domaine.
  7. Relire son propre codeCherchez dans votre projet : un accesseur inutile, une cascade de tests de type, une classe qui fait deux choses, un catch vide. Corrigez ce que vous trouvez.
  8. Préparer la soutenancePour trois décisions de conception, préparez la justification et l'alternative que vous avez écartée. C'est ce qui distingue un projet compris d'un projet recopié.

C'est réussi quand

  • Aucune classe du domaine n'importe quoi que ce soit de l'affichage ou du stockage
  • Vos tests échouent si vous cassez volontairement une règle métier
  • La seconde persistance se branche sans toucher au domaine
  • Vous savez défendre trois choix et nommer ce que vous avez écarté

Correction

Le sens des dépendances
mediatheque/
domaine/       Document, Livre, Dvd, Adherent, Emprunt, Mediatheque
persistance/   DepotDocuments (interface), DepotFichier, DepotMemoire
interface/     ConsoleUI

RÈGLE : les flèches vont vers le DOMAINE, jamais l'inverse
interface   ──▶ domaine
persistance ──▶ domaine
domaine     ──▶ RIEN

vérification mécanique :
grep -r "import.*interface\." src/domaine/   → doit être VIDE

Le domaine porte les règles métier, et elles ne dépendent ni de l'endroit où l'on stocke ni de la façon dont on affiche. C'est ce qui permet de tester le métier sans fichier ni écran, et de remplacer l'un ou l'autre sans y toucher. La commande grep transforme ce principe en vérification, ce qui vaut mieux qu'une bonne intention.

L'inversion de dépendance
/* dans le DOMAINE : l'interface, exprimée dans ses termes */
public interface DepotDocuments {
  void enregistrer(Document d);
  Optional<Document> parCote(String cote);
  List<Document> tous();
}

/* dans la PERSISTANCE : les implémentations */
public class DepotFichier  implements DepotDocuments { … }
public class DepotMemoire  implements DepotDocuments { … }   /* pour les tests */

/* le domaine reçoit son dépôt, il ne le CHOISIT pas */
public Mediatheque(DepotDocuments depot) { this.depot = depot; }

L'interface appartient au domaine — c'est LUI qui dit ce dont il a besoin — et l'implémentation est ailleurs. C'est ce renversement qui permet à l'étape 6 de brancher un second format sans rien modifier, et à l'étape 3 de tester le métier avec un dépôt en mémoire, sans fichier ni disque.

Les tests des règles métier
@Test void unDocumentIndisponibleNeSEmpruntePas() {
  Document d = new Livre("Dune", 1965);
  assertTrue(d.emprunter());
  assertFalse(d.emprunter());      /* le second échoue */
}

@Test void leQuotaEstApplique() {
  Adherent a = new Adherent("Ada");
  for (int i = 0; i < 5; i++) a.emprunter(unLivre());
  assertThrows(QuotaDepasseException.class, () -> a.emprunter(unLivre()));
}

@Test void unRetardDeZeroJourNeCoutePasRien() {
  assertEquals(0.0, new Emprunt(livre, ada, hier).penalite(aujourdHui));
}

Un test par règle, nommé par la règle : quand il échoue, son NOM dit déjà ce qui est cassé. Le test qui prouve le plus est le second — il vérifie qu'une règle REFUSE, ce que l'on oublie presque toujours en ne testant que les cas qui marchent.

L'aller-retour de persistance
@Test void sauvegarderPuisRechargerRedonneLeMemeEtat() {
  Mediatheque avant = uneMediathequeDeTest();
  Path f = Files.createTempFile("cat", ".txt");

  new DepotFichier(f).enregistrerTout(avant.tous());
  List<Document> apres = new DepotFichier(f).tous();

  assertEquals(avant.tous(), apres);    /* d'où l'equals du TP 6 */
}

Ce test unique couvre l'écriture ET la lecture, y compris l'encodage, les séparateurs et les champs vides. Il repose sur l'equals redéfini au TP 6 — sans lui, la comparaison porterait sur les références et échouerait toujours. C'est le premier test à écrire sur toute persistance, et souvent le seul qui trouve quelque chose.

La relecture de son propre code
à chercher, dans l'ordre de rentabilité :

catch (Exception e) { }          → une erreur effacée      (TP 7)
if (x instanceof …) else if …    → une méthode manquante   (TP 6)
getListe() qui renvoie le champ  → encapsulation qui fuit  (TP 3)
une classe de 400 lignes         → elle fait deux choses
un accesseur jamais appelé        → à supprimer
un commentaire qui explique COMMENT → renommer plutôt que commenter

Ces six motifs se cherchent mécaniquement, et chacun renvoie à un TP précis du semestre. C'est le meilleur usage de la dernière séance : relire son propre code avec une liste, plutôt qu'ajouter une fonctionnalité de plus. Un projet plus petit et cohérent se défend toujours mieux qu'un projet plus large et bancal.

Défendre un choix
« Pourquoi une classe abstraite Document plutôt qu'une interface ? »
→ parce qu'il y a un ÉTAT commun (titre, disponibilité) et un
constructeur à factoriser. J'aurais pris une interface si les
documents n'avaient partagé que des comportements.

« Pourquoi Emprunt est-il une classe et pas un champ de Document ? »
→ parce que la relation porte des DATES, et qu'un document a un
historique de plusieurs emprunts. Le champ interdisait l'historique.

« Pourquoi ArrayList et pas LinkedList ? »
→ je l'ai MESURÉ au TP 9 : accès par indice 12 000 fois plus rapide,
parcours 8 fois plus rapide, et je n'insère jamais en tête.

Ce qui est évalué à la soutenance n'est pas le nombre de fonctionnalités, c'est la capacité à dire pourquoi. Une réponse qui nomme l'alternative écartée et le critère de décision vaut mieux qu'un projet plus riche défendu par « c'est ce qu'on fait d'habitude » — et la troisième réponse, qui s'appuie sur une mesure, est la plus solide des trois.

Ce que ce semestre laisse

Dix chapitres plus tôt, la question était de passer du programme qui calcule au programme qui modélise.

Le bloc I a montré ce qui casse sans objet, et donné la classe. Le bloc II l'a rendue responsable de sa propre cohérence. Le bloc III — le cœur — a permis de factoriser sans abuser du « est-un », puis d'écrire du code qui traite uniformément des objets de types différents. Le bloc IV a rendu tout cela robuste et dessinable, et le bloc V l'a fait vivre.

Reste une idée qui traverse les cinq blocs, et c'est peut-être ce qu'il faut en garder : la programmation objet consiste à décider où mettre chaque chose. Quelle classe porte quelle donnée, quelle classe porte quelle règle, ce qui est visible et ce qui ne l'est pas, ce qui est commun et ce qui varie. Le langage ne prend aucune de ces décisions à votre place — il se contente de rendre les bonnes faciles à écrire et les mauvaises faciles à regretter.

Et le critère de réussite est resté le même depuis le chapitre 1 : quand une exigence change, combien de fichiers faut-il ouvrir ? Un seul, si la chose était au bon endroit.

À retenir

Flashcards · 6 cartes

Comment tire-t-on des classes d'un énoncé, et quel filtre applique-t-on ?
On souligne les NOMS (candidats-classes) et l'on entoure les VERBES (candidats-méthodes), puis on FILTRE : un nom sans comportement ni identité propre est un attribut, pas une classe — « durée » en est un. Certains cas sont douteux et doivent être POSÉS EN QUESTION plutôt que devinés : « pénalité » est une valeur calculée si c'est un montant, une classe si elle a une date et un statut. Filtre final : chaque classe se décrit en une phrase sans « et ».
Quand une association devient-elle une classe à part entière ?
Quand elle porte des DONNÉES PROPRES ou un COMPORTEMENT. Emprunt n'est pas un simple lien entre Adherent et Exemplaire : il a une date de début, une date prévue, une date effective, et il sait calculer son retard. Règle : une association qui porte des attributs est une classe.
Comment vérifie-t-on un diagramme avant d'écrire du code ?
En y faisant MARCHER UN SCÉNARIO complet : « Ana emprunte Dune, le rend avec cinq jours de retard, paie sa pénalité ». On suit le chemin sur le dessin, et si une information manque — comment retrouver un exemplaire libre de cet ouvrage ? — le diagramme est incomplet. S'en apercevoir à ce moment coûte une minute ; s'en apercevoir en codant coûte une refonte.
Qu'est-ce qu'une tranche verticale, et quels sont ses trois bénéfices ?
Choisir UN scénario complet et le faire fonctionner de bout en bout, plutôt que d'écrire toutes les classes puis tous les attributs puis toutes les méthodes. Bénéfices : 1) le programme COMPILE ET TOURNE en permanence, donc une régression se voit le jour même ; 2) on a TOUJOURS quelque chose à montrer, et un projet à moitié fait qui tourne vaut mieux qu'un projet complet qui ne compile pas ; 3) la CONCEPTION SE CORRIGE EN MARCHANT — découvrir une classe manquante tôt coûte une heure, tard une refonte.
Que teste-t-on dans un projet objet, et que révèle l'écriture d'un test ?
Les INVARIANTS (le constructeur refuse-t-il bien un ISBN vide ?), les CAS LIMITES (dernier exemplaire, retour le jour de l'échéance, liste vide), le COMPORTEMENT POLYMORPHE (chaque sous-classe fait-elle ce qu'elle doit ?), et les EXCEPTIONS (la bonne, dans le bon cas). Bénéfice moins évident : écrire un test, c'est être le premier CLIENT de sa propre API — s'il faut créer sept objets pour en tester un, la conception est trop couplée.
Nommez les cinq pièges classiques d'un projet de L2.
1) LA CLASSE DIVINE : un Main de six cents lignes entouré de classes anémiques réduites à des getters. 2) L'ENCAPSULATION ABSENTE : tout en public « pour y accéder depuis le Main » — signale généralement le premier piège. 3) L'HÉRITAGE PARTOUT : une hiérarchie de six niveaux fondée sur des attributs communs, que le test « est-un » réduit de moitié. 4) LE GRAND ASSEMBLAGE FINAL au lieu des tranches verticales. 5) L'OUBLI DU DIAGRAMME, dessiné la veille pour satisfaire à la consigne — et cela se voit.