4 concepts fondamentaux
Les 4 concepts fondamentaux de la POO
Notions théoriques
Synthèse : HEAP
Pour mémoriser les 4 piliers de la POO, retenez l'acronyme HEAP (qui signifie aussi "tas" en anglais, là où Java stocke les objets en mémoire) :
| Lettre | Concept | Définition | Exemple RPG |
|---|---|---|---|
| H | Héritage | Une classe hérite des attributs et méthodes d'une autre | Guerrier extends Personnage |
| E | Encapsulation | Les données internes sont protégées, accessibles via des méthodes | private int pointsDeVie + getPointsDeVie() |
| A | Abstraction | On définit un contrat sans imposer l'implémentation | abstract class Personnage + abstract void attaquer() |
| P | Polymorphisme | Un même appel de méthode produit des résultats différents selon le type réel | List<Personnage> contenant Guerriers et Mages |
Héritage (H)
L'héritage crée une hiérarchie de classes. La sous-classe réutilise le code de la classe parente et peut le spécialiser.
// Personnage = classe parente (abstraite)
// Guerrier, Mage, Archer = sous-classes (concrètes)
public class Guerrier extends Personnage {
public Guerrier(String nom, int pointsDeVie, int force, String typeArme) {
super(nom, pointsDeVie, force); // réutilise le constructeur de Personnage
this.typeArme = typeArme;
}
}
Encapsulation (E)
L'encapsulation protège les données internes en les déclarant private et en fournissant des getters/setters pour y accéder de façon contrôlée.
public abstract class Personnage {
private String nom;
private int pointsDeVie;
private int force;
public String getNom() { return nom; }
public int getPointsDeVie() { return pointsDeVie; }
public void subirDegats(int degats) {
this.pointsDeVie = Math.max(0, this.pointsDeVie - degats); // jamais négatif
}
}
Abstraction (A)
L'abstraction définit un modèle général sans l'implémenter complètement. La classe abstraite impose un contrat que chaque sous-classe doit remplir.
public abstract class Personnage {
// Méthode abstraite : chaque personnage doit implémenter sa propre façon d'attaquer
public abstract void attaquer(Personnage cible);
// Méthode concrète partagée par tous
public boolean estVivant() {
return pointsDeVie > 0;
}
}
Polymorphisme (P)
Le polymorphisme permet de traiter des objets de types différents de façon uniforme, en appelant la bonne méthode selon le type réel.
List<Personnage> equipe = List.of(
new Guerrier("Thor", 120, 18, "épée"),
new Mage("Elara", 80, 12, 60),
new Archer("Lyra", 90, 14, 20)
);
for (Personnage p : equipe) {
p.attaquer(boss); // chaque personnage attaque à sa manière
}
Les 4 concepts en interaction
Ces 4 concepts ne sont pas indépendants : ils se renforcent mutuellement.
- L'abstraction définit le contrat (
abstract void attaquer()) - L'héritage permet à
Guerrierd'hériter dePersonnageet d'implémenter ce contrat - L'encapsulation protège les
pointsDeVied'une modification directe non contrôlée - Le polymorphisme permet de traiter
GuerrieretMagede la même façon viaList<Personnage>
Un programme Java bien conçu utilise les 4 piliers ensemble. Si un concept manque, la conception est incomplète : sans encapsulation, les données sont vulnérables ; sans abstraction, le code est rigide ; sans héritage, il y a de la duplication ; sans polymorphisme, le code n'est pas extensible.
Exemple pratique
import java.util.ArrayList;
import java.util.List;
// Personnage.java — ABSTRACTION + ENCAPSULATION
public abstract class Personnage {
private String nom; // encapsulation : private
private int pointsDeVie;
private int force;
public Personnage(String nom, int pointsDeVie, int force) {
this.nom = nom;
this.pointsDeVie = pointsDeVie;
this.force = force;
}
// Getters
public String getNom() { return nom; }
public int getPointsDeVie() { return pointsDeVie; }
public int getForce() { return force; }
// Méthode contrôlée : jamais de PV négatifs
public void subirDegats(int degats) {
this.pointsDeVie = Math.max(0, this.pointsDeVie - degats);
}
public boolean estVivant() { return pointsDeVie > 0; }
// toString() redéfini pour un affichage lisible
@Override
public String toString() {
return getNom() + " [PV: " + getPointsDeVie() + " | Force: " + getForce() + "]";
}
// ABSTRACTION : contrat que chaque sous-classe doit remplir
public abstract void attaquer(Personnage cible);
}
// Guerrier.java — HÉRITAGE
public class Guerrier extends Personnage {
private String typeArme;
public Guerrier(String nom, int pointsDeVie, int force, String typeArme) {
super(nom, pointsDeVie, force); // héritage : appel constructeur parent
this.typeArme = typeArme;
}
public String getTypeArme() { return typeArme; }
@Override
public void attaquer(Personnage cible) {
int degats = getForce() * 2;
System.out.println(getNom() + " frappe " + cible.getNom() + " (" + typeArme + "). Dégâts : " + degats);
cible.subirDegats(degats); // encapsulation : via la méthode, pas directement
}
@Override
public String toString() {
return super.toString() + " | Arme: " + typeArme;
}
}
// Mage.java — HÉRITAGE
public class Mage extends Personnage {
private int mana;
public Mage(String nom, int pointsDeVie, int force, int mana) {
super(nom, pointsDeVie, force);
this.mana = mana;
}
public int getMana() { return mana; }
@Override
public void attaquer(Personnage cible) {
if (mana >= 15) {
int degats = getForce() * 3;
System.out.println(getNom() + " lance un sort sur " + cible.getNom() + ". Dégâts magiques : " + degats);
cible.subirDegats(degats);
mana -= 15;
} else {
int degats = getForce();
System.out.println(getNom() + " frappe faiblement " + cible.getNom() + ". Dégâts : " + degats);
cible.subirDegats(degats);
}
}
@Override
public String toString() {
return super.toString() + " | Mana: " + mana;
}
}
// Main.java — POLYMORPHISME en action
public class Main {
public static void main(String[] args) {
// Équipe héros (liste hétérogène : polymorphisme)
List<Personnage> heroes = new ArrayList<>();
heroes.add(new Guerrier("Thor", 120, 18, "épée runique"));
heroes.add(new Mage("Elara", 80, 12, 60));
// Équipe ennemis
List<Personnage> ennemis = new ArrayList<>();
ennemis.add(new Guerrier("Golem", 200, 10, "poing de pierre"));
// Un tour de combat complet
simulerTour(heroes, ennemis);
simulerTour(ennemis, heroes);
// Affichage final
System.out.println("\n=== État final ===");
heroes.forEach(System.out::println); // toString() polymorphe
ennemis.forEach(System.out::println);
}
// Méthode générique : ne connaît que Personnage
static void simulerTour(List<Personnage> attaquants, List<Personnage> defenseurs) {
System.out.println("\n--- Nouveau tour ---");
for (Personnage attaquant : attaquants) {
if (!attaquant.estVivant()) continue;
Personnage cible = defenseurs.stream()
.filter(Personnage::estVivant)
.findFirst()
.orElse(null);
if (cible != null) {
attaquant.attaquer(cible); // dispatch dynamique : polymorphisme !
}
}
}
}
La méthode simulerTour() illustre parfaitement le polymorphisme : elle ne sait pas si elle traite des Guerriers ou des Mages. Elle appelle simplement attaquer() et Java choisit la bonne implémentation.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Vous allez assembler les 4 concepts dans un mini-projet : un tour de combat complet entre deux équipes, sans jamais instancier Personnage directement.
Étape 1 — Encapsuler les attributs de Personnage
Rendez nom, pointsDeVie et force privés dans Personnage et ajoutez une méthode subirDegats(int) qui empêche les PV de descendre en dessous de 0.
Passer par une méthode subirDegats() plutôt que modifier pointsDeVie directement garantit que la règle "jamais de PV négatifs" est respectée partout. C'est l'essence de l'encapsulation : centraliser la logique métier dans la classe.
Étape 2 — Redéfinir toString() dans chaque classe
Redéfinissez toString() dans Personnage, puis dans Guerrier pour ajouter l'arme, et dans Mage pour ajouter le mana.
Redéfinir toString() dans chaque classe facilite énormément le débogage. Au lieu d'afficher Guerrier@7852e922 (adresse mémoire), System.out.println(guerrier) affichera "Thor [PV: 120 | Force: 18] | Arme: épée runique".
Étape 3 — Simuler un tour de combat complet
Créez la méthode simulerTour() et appelez-la depuis main pour faire combattre deux équipes pendant 3 tours.
simulerTour() reçoit List<Personnage> et ne connaît jamais la différence entre un Guerrier et un Mage. Ce design est robuste : ajouter un type Archer ne nécessite aucune modification de simulerTour(). C'est le bénéfice combiné de l'abstraction et du polymorphisme.