Aller au contenu principal

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) :

LettreConceptDéfinitionExemple RPG
HHéritageUne classe hérite des attributs et méthodes d'une autreGuerrier extends Personnage
EEncapsulationLes données internes sont protégées, accessibles via des méthodesprivate int pointsDeVie + getPointsDeVie()
AAbstractionOn définit un contrat sans imposer l'implémentationabstract class Personnage + abstract void attaquer()
PPolymorphismeUn même appel de méthode produit des résultats différents selon le type réelList<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 à Guerrier d'hériter de Personnage et d'implémenter ce contrat
  • L'encapsulation protège les pointsDeVie d'une modification directe non contrôlée
  • Le polymorphisme permet de traiter Guerrier et Mage de la même façon via List<Personnage>
info

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 !
}
}
}
}
info

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


Quel acronyme résume les 4 concepts fondamentaux de la POO dans ce cours ?


Quel concept de la POO protège les données internes d'une classe en les déclarant private ?


Pourquoi ne doit-on jamais écrire 'new Personnage()' dans notre jeu ?


Quelle méthode doit-on redéfinir avec @Override dans chaque sous-classe de Personnage ?


La méthode toString() est définie dans la classe Object dont toutes les classes héritent. Qu'exprime @Override lorsqu'on la redéfinit dans Personnage ?


Dans simulerTour(List<Personnage> attaquants, ...), quel concept permet d'appeler attaquer() sans savoir si c'est un Guerrier ou un Mage ?


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.


Bonne pratique - Contrôler les mutations d'état

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.


Bonne pratique - Toujours redéfinir toString()

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.


Bonne pratique - Méthodes génériques via le type parent

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.

📌 Une solution