Les Quatre Piliers : Encapsulation, Héritage, Polymorphisme, Abstraction
Introduction : Les Quatre Piliers de la POO
La Programmation Orientée Objet repose sur quatre concepts fondamentaux qui transforment votre façon de concevoir les programmes. Ces piliers permettent de créer du code plus organisé, réutilisable et maintenable.
🏛️ Les Quatre Piliers
Encapsulation - Protéger les données
Héritage - Réutiliser le code
Polymorphisme - Flexibilité et extensibilité
Abstraction - Simplifier la complexité
1. Révision : Classes, Objets et Constructeurs
Une classe est un plan, un objet est une instance concrète, et le constructeur est la "chaîne de montage" qui assemble l'objet en initialisant ses attributs.
La surcharge de constructeurs est une technique puissante qui offre plus de flexibilité lors de la création d'objets.
Exemple 1 : Surcharge de Constructeurs
Cet exemple montre comment créer des objets Student de différentes manières :
class Student {
String name;
int age;
String major;
// Constructeur n°1 : complet
public Student(String name, int age, String major) {
this.name = name;
this.age = age;
this.major = major;
}
// Constructeur n°2 : surchargé, avec des valeurs par défaut
public Student(String name) {
this(name, 18, "Undeclared");
}
public void displayInfo() {
System.out.println("Nom: " + name + ", Âge: " + age + ", Spécialité: " + major);
}
}
public class Main {
public static void main(String[] args) {
Student student1 = new Student("John", 20, "Informatique");
student1.displayInfo();
Student student2 = new Student("Jane");
student2.displayInfo();
}
}
Astuce : this() pour Éviter la Duplication
L'instruction this(name, 18, "Undeclared"); appelle un autre constructeur de la même classe pour éviter de dupliquer le code d'initialisation. C'est une excellente pratique !
Pilier n°1 : L'Encapsulation
Définition
C'est l'idée de regrouper les données (attributs) et les méthodes qui les manipulent au sein d'un même "objet", tout en cachant les détails internes au monde extérieur.
Objectif Principal
Protéger l'intégrité des données d'un objet. Un objet doit être le seul maître de son état interne pour éviter qu'il ne soit corrompu par des modifications invalides ou accidentelles.
Analogie : Le Tableau de Bord d'une Voiture
En tant que conducteur, vous interagissez avec une interface simple : un volant, des pédales, un levier de vitesse. Vous n'avez pas besoin de manipuler directement les injecteurs de carburant ou la transmission. La complexité du moteur est cachée (private). Vous utilisez des méthodes publiques (public) comme accelerer() ou freiner(), et la voiture gère elle-même sa mécanique interne de manière sûre.
Implémentation en Java
On déclare les attributs avec le mot-clé private
On fournit un accès contrôlé via des getters (pour lire) et setters (pour modifier)
Exemple 2 : Encapsulation avec BankAccount
Cet exemple montre comment protéger le solde d'un compte :
class BankAccount {
private String accountNumber;
private double balance; // Privé ! Personne ne peut le modifier directement.
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// Getter : seule façon de LIRE le solde
public double getBalance() {
return this.balance;
}
// Méthode publique pour déposer de l'argent
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
System.out.println(amount + " € déposés. Nouveau solde : " + this.balance + " €.");
} else {
System.out.println("Le montant du dépôt doit être positif.");
}
}
// Méthode publique pour retirer de l'argent
public void withdraw(double amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
System.out.println(amount + " € retirés. Nouveau solde : " + this.balance + " €.");
} else {
System.out.println("Fonds insuffisants ou montant invalide.");
}
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("123456789", 1000);
System.out.println("Solde initial : " + account.getBalance() + " €.");
account.deposit(500);
account.withdraw(2000); // Échoue car fonds insuffisants
account.withdraw(300);
}
}
Résultat :
Solde initial : 1000.0 €.
500.0 € déposés. Nouveau solde : 1500.0 €.
Fonds insuffisants ou montant invalide.
300.0 € retirés. Nouveau solde : 1200.0 €.
Bénéfices de l'Encapsulation
Sécurité - Les données sont protégées
Contrôle - Vous pouvez ajouter de la logique de validation
C'est un mécanisme qui permet à une nouvelle classe (classe "fille") de recevoir les attributs et les méthodes d'une classe existante (classe "mère").
Objectif Principal
Réutiliser le code et créer une hiérarchie logique entre les classes. L'héritage modélise une relation de type "est un". Par exemple, un Chienest unAnimal.
Analogie : Les Véhicules
Tous les véhicules ont des caractéristiques communes (une vitesse, une capacité de passagers). On peut créer une classe Vehicule avec ces éléments. Ensuite, une classe Voiture peut hériter de Vehicule et ajouter des attributs spécifiques comme nombreDePortes.
Implémentation en Java
On utilise le mot-clé extends pour définir la relation d'héritage
On utilise le mot-clé super pour appeler le constructeur ou les méthodes de la classe mère
Exemple 3 : Héritage avec Animal
// Classe Mère (générale)
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " mange.");
}
}
// Classe Fille (spécifique), hérite de Animal
class Dog extends Animal {
public Dog(String name) {
super(name); // Appelle le constructeur de la classe mère
}
// Méthode spécifique à Dog
public void bark() {
System.out.println(name + " aboie : Woof !");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("Rex");
myDog.eat(); // Méthode héritée de Animal
myDog.bark(); // Méthode propre à Dog
}
}
Rôle de super()
super(name) est obligatoire ici pour appeler le constructeur de Animal et initialiser l'attribut name qui est défini dans la classe mère.
Pour aller plus loin :
Source: EvoluNoob
Pilier n°3 : Le Polymorphisme
Définition
"Qui peut prendre plusieurs formes". C'est la capacité d'un objet d'être traité comme une instance de sa propre classe, mais aussi comme une instance de sa classe mère. Une seule action peut avoir des comportements différents selon le type réel de l'objet.
Objectif Principal
Écrire du code générique et flexible qui peut fonctionner avec une multitude d'objets différents sans avoir à connaître leur type exact.
Analogie : La Prise Électrique
Vous avez une prise murale (l'interface). Vous pouvez y brancher une lampe, un chargeur de téléphone, ou un aspirateur. L'action est toujours la même : "fournir du courant". Cependant, le résultat est très différent pour chaque appareil. Le polymorphisme vous permet d'écrire un code qui dit "fournir du courant" sans se soucier de l'appareil qui est branché.
Implémentation en Java
Principalement via la redéfinition de méthode (@Override) dans une hiérarchie d'héritage. Une variable de type Animal peut contenir un objet Chien ou Chat, et l'appel à la méthode makeSound() exécutera la version spécifique à l'objet réel.
Exemple 4 : Polymorphisme en Action
class Animal {
public void makeSound() {
System.out.println("L'animal fait un bruit.");
}
}
class Dog extends Animal {
@Override // Annotation qui indique qu'on redéfinit une méthode mère
public void makeSound() {
System.out.println("Le chien aboie.");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Le chat miaule.");
}
}
public class Main {
public static void main(String[] args) {
// Le polymorphisme permet cette affectation
Animal myAnimal1 = new Dog();
Animal myAnimal2 = new Cat();
// La JVM sait quelle version appeler au moment de l'exécution
myAnimal1.makeSound(); // Affiche: Le chien aboie.
myAnimal2.makeSound(); // Affiche: Le chat miaule.
}
}
L'Annotation @Override
Cette annotation n'est pas obligatoire mais c'est une excellente pratique. Elle demande au compilateur de vérifier que vous êtes bien en train de redéfinir une méthode qui existe dans la classe mère.
Pour aller plus loin :
Source: EvoluNoob
Pilier n°4 : L'Abstraction
Définition
Cacher les détails d'implémentation complexes et n'exposer que les fonctionnalités essentielles et de haut niveau d'un objet. L'abstraction se concentre sur CE QUE un objet fait, et non sur COMMENT il le fait.
Objectif Principal
Réduire la complexité et permettre de travailler sur des systèmes à grande échelle sans avoir à en comprendre chaque détail.
Analogie : La Télécommande de la Télévision
Votre télécommande est une interface abstraite. Elle a des boutons simples : "Allumer", "Volume +", "Chaîne -". Vous savez ce que ces boutons font. Mais vous n'avez aucune idée de la complexité des circuits électroniques et des logiciels internes qui s'exécutent. Cette complexité est abstraite, cachée.
Implémentation en Java
Classes Abstraites (abstract class) : Un mélange entre un plan et une implémentation partielle
Interfaces (interface) : Un contrat 100% abstrait
Exemple 5 : Polymorphisme avec une Interface
// L'interface définit un contrat
interface Shape {
double calculateArea(); // Pas de corps, juste la signature
}
// Cette classe respecte le contrat
class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// Cette classe respecte aussi le contrat
class Rectangle implements Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
public class Main {
public static void main(String[] args) {
Shape myShape1 = new Circle(5);
Shape myShape2 = new Rectangle(4, 7);
System.out.println("Aire de la forme 1 : " + myShape1.calculateArea());
System.out.println("Aire de la forme 2 : " + myShape2.calculateArea());
}
}
Quand Utiliser Quoi ?
Classe Abstraite : Quand vous avez du code commun à partager
Interface : Quand vous voulez juste définir un contrat
Résumé des Quatre Piliers
Pilier
Objectif
Bénéfice
Encapsulation
Protéger les données
Sécurité et contrôle
Héritage
Réutiliser le code
DRY et organisation
Polymorphisme
Flexibilité du code
Extensibilité
Abstraction
Simplifier la complexité
Maintenabilité
Exercices Pratiques : Mettre les Piliers en Action
Ces exercices vous permettront de mettre en pratique chaque pilier de la POO pour construire des classes bien structurées et robustes.
Exercice 1 : L'Encapsulation (Classe `Produit`)
Créez une classe `Produit` pour représenter un article dans un magasin. Elle doit être correctement encapsulée :
Attributs privés : `nom` (String) et `prix` (double).
Un constructeur pour initialiser le nom et le prix.
Un getter pour le `nom` et le `prix`.
Un setter pour le `prix` qui empêche de définir un prix négatif. Si le prix est négatif, affichez un message d'erreur et ne changez pas le prix.
Testez votre classe en créant un produit, en affichant ses informations, puis en essayant de mettre à jour son prix avec une valeur valide et une valeur invalide.
Indice : Votre setter `setPrix(double nouveauPrix)` doit contenir une condition `if (nouveauPrix >= 0)`.
Voir la solution
// Fichier: Produit.java
public class Produit {
private String nom;
private double prix;
public Produit(String nom, double prix) {
this.nom = nom;
// On utilise le setter même dans le constructeur pour appliquer la validation
this.setPrix(prix);
}
public String getNom() {
return this.nom;
}
public double getPrix() {
return this.prix;
}
public void setPrix(double nouveauPrix) {
if (nouveauPrix >= 0) {
this.prix = nouveauPrix;
} else {
System.out.println("Erreur : Le prix ne peut pas être négatif.");
}
}
}
// Fichier: Magasin.java
public class Magasin {
public static void main(String[] args) {
Produit telephone = new Produit("Smartphone X", 599.99);
System.out.println("Produit : " + telephone.getNom() + ", Prix : " + telephone.getPrix() + "€");
System.out.println("\nMise à jour du prix à -50€...");
telephone.setPrix(-50.0); // Devrait afficher une erreur
System.out.println("Prix actuel : " + telephone.getPrix() + "€");
System.out.println("\nMise à jour du prix à 649.99€...");
telephone.setPrix(649.99);
System.out.println("Prix actuel : " + telephone.getPrix() + "€");
}
}
Exercice 2 : L'Héritage (Classes `Employe` et `Manager`)
Créez une classe de base `Employe` avec les attributs `nom` et `salaire`. Créez ensuite une classe `Manager` qui hérite de `Employe`. La classe `Manager` doit avoir un attribut supplémentaire : `departement` (String).
Le constructeur de `Manager` doit accepter un nom, un salaire et un département, et appeler le constructeur de la classe `Employe` pour initialiser le nom et le salaire.
Ajoutez une méthode `afficherDetails()` dans les deux classes pour afficher les informations spécifiques à chacune.
Indice : Utilisez `super(nom, salaire);` dans le constructeur de `Manager` et l'annotation `@Override` pour la méthode `afficherDetails()` de `Manager`.
Voir la solution
// Fichier: Employe.java
public class Employe {
protected String nom;
protected double salaire;
public Employe(String nom, double salaire) {
this.nom = nom;
this.salaire = salaire;
}
public void afficherDetails() {
System.out.println("Nom : " + nom + ", Salaire : " + salaire + "€");
}
}
// Fichier: Manager.java
public class Manager extends Employe {
private String departement;
public Manager(String nom, double salaire, String departement) {
// Appel au constructeur de la classe mère (Employe)
super(nom, salaire);
this.departement = departement;
}
@Override
public void afficherDetails() {
// Appel à la méthode de la classe mère pour ne pas dupliquer le code
super.afficherDetails();
System.out.println("Département : " + this.departement);
}
}
// Fichier: Entreprise.java
public class Entreprise {
public static void main(String[] args) {
Employe emp = new Employe("Alice", 35000);
Manager mgr = new Manager("Bob", 60000, "Technologie");
System.out.println("Détails de l'employé :");
emp.afficherDetails();
System.out.println("\nDétails du manager :");
mgr.afficherDetails();
}
}
Exercice 3 : Polymorphisme et Abstraction (Interface `Dessinable`)
Mettez en pratique le polymorphisme et l'abstraction :
Créez une interface `Dessinable` avec une seule méthode abstraite : `void dessiner()`.
Créez deux classes, `Cercle` et `Rectangle`, qui implémentent l'interface `Dessinable`. Chaque classe doit fournir sa propre version de la méthode `dessiner()` (par exemple, en affichant "Je dessine un cercle." ou "Je dessine un rectangle.").
Dans votre `main`, créez un tableau de type `Dessinable[]` contenant un objet `Cercle` et un objet `Rectangle`.
Parcourez ce tableau avec une boucle et appelez la méthode `dessiner()` sur chaque élément.
Indice : Le polymorphisme vous permet de stocker des `Cercle` et des `Rectangle` dans un tableau de `Dessinable`. La JVM saura quelle méthode `dessiner()` appeler pour chaque objet.
Voir la solution
// Fichier: Dessinable.java (Interface)
public interface Dessinable {
void dessiner(); // Méthode abstraite par défaut
}
// Fichier: Cercle.java
public class Cercle implements Dessinable {
@Override
public void dessiner() {
System.out.println("Je dessine un cercle. (O)");
}
}
// Fichier: Rectangle.java
public class Rectangle implements Dessinable {
@Override
public void dessiner() {
System.out.println("Je dessine un rectangle. [ ]");
}
}
// Fichier: Main.java
public class Main {
public static void main(String[] args) {
// Le tableau est de type interface, mais contient des objets de classes concrètes
Dessinable[] formes = new Dessinable[2];
formes[0] = new Cercle();
formes[1] = new Rectangle();
// Grâce au polymorphisme, on peut parcourir le tableau de manière uniforme
for (Dessinable forme : formes) {
forme.dessiner(); // La JVM appelle la bonne méthode
}
}
}