POO : Préparation à l'examen
Dans ce TP, nous allons balayer l'ensemble des notions vues en POO à travers un cas complet : la gestion d'une médiathèque. C'est le type de sujet que vous pouvez rencontrer à l'examen : un diagramme UML à analyser, du code à corriger, du code à compléter et du code à écrire entièrement.
Sommaire
Ressources disponibles :
Objectifs
À la fin de ce TP vous saurez :
- Lire et analyser un diagramme de classes (même présenté au format texte).
- Repérer et corriger des erreurs classiques dans du code objet.
- Compléter une classe à partir d'une modélisation.
- Implémenter une interface et une classe abstraite.
- Manipuler une collection d'objets grâce au polymorphisme.
La modélisation
Le jour de l'examen, le diagramme UML peut être fourni sous forme d'image… ou décrit textuellement. Voici la modélisation de notre médiathèque au format texte :
+--------------------------------------+
| « abstract » |
| Document |
+--------------------------------------+
| # titre : string |
| # annee : int |
| - disponible : bool = true |
+--------------------------------------+
| + __construct(titre, annee) |
| + getTitre() : string |
| + estDisponible() : bool |
| + abstract resume() : string |
+--------------------------------------+
^ ^
| |
+-------------------+ +-------------------+
| Livre | | Dvd |
+-------------------+ +-------------------+
| - auteur : string | | - duree : int |
| - nbPages : int | +-------------------+
+-------------------+ | + resume():string |
| + resume():string | +-------------------+
+-------------------+
+--------------------------------------+
| « interface » |
| Empruntable |
+--------------------------------------+
| + emprunter(Adherent $a) : bool |
| + rendre() : void |
+--------------------------------------+
Livre et Dvd implémentent l'interface Empruntable.
+--------------------------------------+
| Adherent |
+--------------------------------------+
| - nom : string |
| - prenom : string |
+--------------------------------------+
| + __construct(nom, prenom) |
| + identite() : string |
+--------------------------------------+
+--------------------------------------+
| Mediatheque |
+--------------------------------------+
| - nom : string |
| - lesDocuments : Document[] |
+--------------------------------------+
| + __construct(nom) |
| + ajouter(Document $d) : void |
| + afficherCatalogue() : void |
| + nbDisponibles() : int |
+--------------------------------------+
Mediatheque "1" ---- "0..*" Document (lesDocuments)Rappel des symboles : + public, - privé (private), # protégé (protected).
Exercice 1 : Analyser l'UML
Avant d'écrire la moindre ligne de code, prenez le temps de lire la modélisation. C'est à vous de jouer !
Questionnement
- Combien de classes sont instanciables ? Lesquelles ?
- Pourquoi la classe
Documentest-elle abstraite ? Qu'est-ce que cela implique ? - Quelle est la différence entre
EmpruntableetDocument? Pourquoi ne pas avoir misemprunter()dansDocument? - Que signifie la multiplicité
"1" ---- "0..*"entreMediathequeetDocument? - La propriété
titreest notée#(protected) alors quedisponibleest notée-(private). Quelle différence concrète cela fait-il pour les classesLivreetDvd? - Si nous devions ajouter un document
Magazine, quelles classes seraient impactées ?
Voir l'une des solutions possibles
- Quatre classes sont instanciables :
Livre,Dvd,AdherentetMediatheque.Documentest abstraite (pas denew) etEmpruntableest une interface (pas denewnon plus). Documentregroupe la logique commune (titre, année, disponibilité) mais un « document » générique n'a pas de sens seul : on ne sait pas rédiger sonresume(). La rendre abstraite force les classes filles à implémenterresume()et interdit lenew Document(…).Documentfactorise du code (héritage, « est un »),Empruntabledéfinit un contrat (comportement, « est capable de »). Un futur objet non-document (une salle de réunion par exemple) pourrait êtreEmpruntablesans être unDocument.- Une médiathèque possède de zéro à plusieurs documents ; un document appartient à une seule médiathèque.
titre(protected) est accessible directement dansLivreetDvd($this->titre).disponible(private) ne l'est pas : les classes filles doivent passer par les méthodes publiques commeestDisponible().- Une seule classe est à créer :
Magazine extends Document(avec sonresume()). Les autres classes ne changent pas, c'est tout l'intérêt de la modélisation :Mediathequemanipule desDocumentsans connaître leur type précis.
Exercice 2 : Corriger le code
Un étudiant a commencé l'implémentation en PHP, mais son code contient 7 erreurs (syntaxe, visibilité, respect de l'UML). À vous de les retrouver et de les corriger. Pas de panique, relisez l'UML ligne par ligne.
abstract class Document {
protected titre;
protected $annee;
public $disponible = true;
function __construct($titre, $annee){
$this->titre = $titre;
$this->annee = $annee;
}
public function getTitre(){
return $titre;
}
public function estDisponible(){
return $this->disponible;
}
public function resume(){
}
}
class Livre extends Document {
private $auteur;
private $nbPages;
function __construct($titre, $annee, $auteur, $nbPages){
$this->auteur = $auteur;
$this->nbPages = $nbPages;
}
public function resume($details){
return "{$this->titre} ({$this->annee}), {$this->auteur}, {$this->nbPages} pages";
}
}
$unDocument = new Document("Titre", 2020);Point de contrôle
- Vous avez trouvé les 7 erreurs ?
- Pour chacune, savez-vous dire s'il s'agit d'une erreur de syntaxe, d'une erreur d'exécution ou d'un non-respect de l'UML ?
Voir l'une des solutions possibles
protected titre;: il manque le$(protected $titre;).public $disponible: l'UML indique-, la propriété doit êtreprivate.return $titre;dansgetTitre(): il faut écrirereturn $this->titre;.resume()est déclarée avec du code (vide) : l'UML la définit abstraite, il faut écrireabstract public function resume();(et supprimer le corps).- Le constructeur de
Livren'appelle pas celui du parent : il faut ajouterparent::__construct($titre, $annee);(sinontitreetanneene sont jamais initialisés). resume($details)dansLivre: la signature ne respecte pas celle de la classe mère (redéfinition = signature identique), il fautresume()sans paramètre.new Document(…): impossible d'instancier une classe abstraite, cette ligne provoque une erreur fatale (Cannot instantiate abstract class Document).
Code corrigé :
abstract class Document {
protected $titre;
protected $annee;
private $disponible = true;
function __construct($titre, $annee){
$this->titre = $titre;
$this->annee = $annee;
}
public function getTitre(){
return $this->titre;
}
public function estDisponible(){
return $this->disponible;
}
abstract public function resume();
}
class Livre extends Document {
private $auteur;
private $nbPages;
function __construct($titre, $annee, $auteur, $nbPages){
parent::__construct($titre, $annee);
$this->auteur = $auteur;
$this->nbPages = $nbPages;
}
public function resume(){
return "{$this->titre} ({$this->annee}), {$this->auteur}, {$this->nbPages} pages";
}
}Exercice 3 : Compléter la classe Dvd
Voici le squelette de la classe Dvd. Je vous laisse le compléter en respectant l'UML (les zones à compléter sont indiquées par des commentaires // À compléter) :
class Dvd extends Document {
// À compléter : la ou les propriétés
function __construct($titre, $annee, $duree){
// À compléter : initialisation
}
public function resume(){
// À compléter : retourne « <titre> (<annee>), durée <duree> min »
}
}Point de contrôle
- Votre constructeur appelle-t-il celui du parent ?
dureeest-elle bienprivate?resume()a-t-elle exactement la même signature que dansDocument?
Voir l'une des solutions possibles
class Dvd extends Document {
private $duree;
function __construct($titre, $annee, $duree){
parent::__construct($titre, $annee);
$this->duree = $duree;
}
public function resume(){
return "{$this->titre} ({$this->annee}), durée {$this->duree} min";
}
}Exercice 4 : Implémenter l'interface Empruntable
Il est maintenant temps d'écrire le contrat et de le faire respecter. C'est à vous de jouer !
- Écrire l'interface
Empruntableen PHP (voir l'UML). - Écrire la classe
Adherent(constructeur +identite()qui retourne « Prenom NOM »). - Modifier
LivreetDvdpour qu'ils implémententEmpruntable:emprunter(Adherent $a)retournefalsesi le document n'est pas disponible, sinon il passe le document en indisponible et retournetrue.rendre()repasse le document en disponible.
Un instant !
La propriété disponible est private dans Document… Vos méthodes emprunter() et rendre() ne peuvent donc pas y accéder directement depuis Livre ou Dvd. Deux pistes : ajouter un mutateur protected dans Document, ou implémenter emprunter() / rendre() directement dans Document. Laquelle choisissez-vous, et pourquoi ?
Voir l'une des solutions possibles
interface Empruntable {
public function emprunter(Adherent $a): bool;
public function rendre(): void;
}
class Adherent {
private $nom;
private $prenom;
function __construct($nom, $prenom){
$this->nom = $nom;
$this->prenom = $prenom;
}
public function identite(){
return $this->prenom . " " . strtoupper($this->nom);
}
}
abstract class Document implements Empruntable {
protected $titre;
protected $annee;
private $disponible = true;
function __construct($titre, $annee){
$this->titre = $titre;
$this->annee = $annee;
}
public function getTitre(){
return $this->titre;
}
public function estDisponible(){
return $this->disponible;
}
public function emprunter(Adherent $a): bool {
if(!$this->disponible){
return false;
}
$this->disponible = false;
return true;
}
public function rendre(): void {
$this->disponible = true;
}
abstract public function resume();
}Ici, la solution retenue est d'implémenter emprunter() et rendre() une seule fois dans Document : le code est factorisé et disponible reste private. Livre et Dvd n'ont rien à ajouter, ils héritent du comportement.
Exercice 5 : La médiathèque et le polymorphisme
Dernière étape, assembler le tout. Écrire la classe Mediatheque puis le programme principal :
ajouter(Document $d)ajoute un document dans la collectionlesDocuments.afficherCatalogue()affiche leresume()de chaque document (une ligne par document).nbDisponibles()retourne le nombre de documents disponibles.- Programme principal :
- Créer une médiathèque, y ajouter 2 livres et 2 DVD.
- Créer un adhérent et lui faire emprunter un document.
- Afficher le catalogue puis le nombre de documents disponibles.
Questionnement
- Dans
afficherCatalogue(), vous appelezresume()sans connaître le type réel de l'objet (LivreouDvd). Comment s'appelle ce mécanisme ? - Pourquoi la collection est-elle typée
Document[]et pasLivre[]? - Que se passerait-il si une classe fille de
Document« oubliait » d'écrireresume()?
Voir l'une des solutions possibles
class Mediatheque {
private $nom;
private $lesDocuments = [];
function __construct($nom){
$this->nom = $nom;
}
public function ajouter(Document $d): void {
$this->lesDocuments[] = $d;
}
public function afficherCatalogue(): void {
foreach ($this->lesDocuments as $leDocument) {
// Polymorphisme : le resume() appelé est celui du type réel (Livre ou Dvd)
echo $leDocument->resume() . "\n";
}
}
public function nbDisponibles(): int {
$compteur = 0;
foreach ($this->lesDocuments as $leDocument) {
if($leDocument->estDisponible()){
$compteur++;
}
}
return $compteur;
}
}
// Programme principal
$mediatheque = new Mediatheque("Médiathèque du lycée");
$mediatheque->ajouter(new Livre("1984", 1949, "George Orwell", 328));
$mediatheque->ajouter(new Livre("Dune", 1965, "Frank Herbert", 412));
$dvd = new Dvd("Interstellar", 2014, 169);
$mediatheque->ajouter($dvd);
$mediatheque->ajouter(new Dvd("Le Voyage de Chihiro", 2001, 125));
$adherent = new Adherent("Brosseau", "Valentin");
$dvd->emprunter($adherent);
$mediatheque->afficherCatalogue();
echo "Documents disponibles : " . $mediatheque->nbDisponibles() . "\n"; // 3Réponses aux questions :
- C'est le polymorphisme : la méthode exécutée est choisie dynamiquement selon le type réel de l'objet.
- Typer la collection
Document[]permet d'y ranger tous les types de documents (actuels et futurs). AvecLivre[], impossible d'y mettre unDvd. - La classe fille serait obligée d'être déclarée
abstractà son tour, sinon PHP lève une erreur : une classe concrète doit implémenter toutes les méthodes abstraites héritées.
Conclusion
Vous venez de dérouler un sujet type examen :
- Analyse d'un diagramme de classes (visibilités, multiplicités, abstraction, interface).
- Correction d'un code qui ne respecte pas l'UML.
- Complétion et implémentation de classes (héritage, interface, contrat).
- Manipulation d'une collection grâce au polymorphisme.
Si l'un des cinq exercices vous a résisté, retournez voir la mise en pratique correspondante (classes abstraites, interfaces, redéfinition, polymorphisme).
👋 Si vous avez des questions, n'hésitez pas.