01 — Les piliers de la POO : encapsulation, héritage, polymorphisme
Ce que tu vas apprendre
- Les 4 piliers de la POO et ce qu'ils résolvent vraiment
- Les pièges de l'héritage (Fragile Base Class, God Object)
- Comment le polymorphisme remplace les
switch/ifimbriqués - Exemples complets TypeScript et Python
Prérequis
Pilier 1 : L'encapsulation
L'encapsulation consiste à regrouper données et comportements dans un objet, et à contrôler ce qui est accessible de l'extérieur.
L'objectif : protéger l'intégrité de l'état interne. Un objet ne devrait jamais se retrouver dans un état incohérent depuis l'extérieur.
typescript// Sans encapsulation — l'état peut être corrompé depuis n'importe où
class Order {
status: string;
items: any[];
total: number;
}
const order = new Order();
order.status = "shipped"; // Passe de "pending" à "shipped" sans passer par "confirmed" — bug
order.total = -500; // Total négatif — incohérent
typescript// Avec encapsulation — les règles sont appliquées par l'objet
class Order {
private _status: "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";
private _items: OrderItem[];
private _total: number;
constructor(items: OrderItem[]) {
if (items.length === 0) throw new Error("Commande vide");
this._status = "pending";
this._items = [...items];
this._total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
confirm(): void {
if (this._status !== "pending") {
throw new Error(`Impossible de confirmer une commande "${this._status}"`);
}
this._status = "confirmed";
}
get status(): string { return this._status; }
get total(): number { return this._total; }
}
En Python :
pythonclass Order:
TRANSITIONS = {
"pending": {"confirmed", "cancelled"},
"confirmed": {"shipped", "cancelled"},
"shipped": {"delivered"},
"delivered": set(),
"cancelled": set(),
}
def __init__(self, items: list) -> None:
if not items:
raise ValueError("Commande vide")
self._status = "pending"
self._items = list(items)
self._total = sum(i["price"] * i["quantity"] for i in items)
def _transition(self, next_status: str) -> None:
if next_status not in self.TRANSITIONS[self._status]:
raise ValueError(f"Transition invalide : {self._status} → {next_status}")
self._status = next_status
def confirm(self) -> None:
self._transition("confirmed")
@property
def status(self) -> str:
return self._status
@property
def total(self) -> float:
return self._total
L'encapsulation ne cache pas de l'information — elle protège des invariants.
Un objet dont l'état peut être modifié librement depuis l'extérieur n'est qu'un dictionnaire avec des méthodes.
Pilier 2 : L'héritage
L'héritage permet à une classe d'hériter des propriétés et méthodes d'une classe parente. C'est le mécanisme de réutilisation de code historique de la POO.
typescriptclass Animal {
constructor(protected name: string) {}
describe(): string {
return `Je suis ${this.name}`;
}
}
class Dog extends Animal {
speak(): string {
return `${this.name} dit : Woof !`;
}
}
class Cat extends Animal {
speak(): string {
return `${this.name} dit : Miaou !`;
}
}
const dog = new Dog("Rex");
console.log(dog.describe()); // "Je suis Rex"
console.log(dog.speak()); // "Rex dit : Woof !"
Les pièges de l'héritage
L'héritage est l'une des fonctionnalités les plus mal utilisées de la POO. Deux problèmes récurrents :
Fragile Base Class : modifier la classe parente casse silencieusement les classes filles.
typescriptclass Vehicle {
startEngine(): void {
console.log("Moteur démarré");
this.onEngineStart(); // Méthode que les sous-classes peuvent surcharger
}
protected onEngineStart(): void {}
}
class ElectricCar extends Vehicle {
protected onEngineStart(): void {
this.chargeCheck(); // Vérifie la batterie au démarrage
}
private chargeCheck(): void {
console.log("Batterie : 80%");
}
}
// Si Vehicle.startEngine() est modifié pour ne plus appeler onEngineStart()
// ElectricCar.chargeCheck() n'est plus jamais appelé — bug silencieux
God Object via héritage profond : des hiérarchies de 4-5 niveaux rendent le code illisible.
typescript// Éviter — hiérarchie trop profonde
class BaseEntity extends AbstractModel extends DataObject extends Serializable extends Cacheable {
// Qui sait ce qu'on hérite vraiment ?
}
La règle reconnue dans l'industrie, popularisée par le GoF et par Eric Gamma lui-même après coup :
"Favor object composition over class inheritance." — Gamma, Helm, Johnson, Vlissides, Design Patterns (1994)
Préférer la composition :
typescript// Composition — explicite et découplé
class ElectricCar {
private engine: Engine;
private battery: Battery;
constructor(engine: Engine, battery: Battery) {
this.engine = engine;
this.battery = battery;
}
start(): void {
this.battery.check();
this.engine.start();
}
}
python# Python — composition
class ElectricCar:
def __init__(self, engine: 'Engine', battery: 'Battery') -> None:
self._engine = engine
self._battery = battery
def start(self) -> None:
self._battery.check()
self._engine.start()
Pilier 3 : Le polymorphisme
Le polymorphisme permet à différents objets de répondre à la même interface de manière appropriée à leur type.
C'est le pilier qui remplace les switch/if imbriqués sur des types.
Sans polymorphisme
typescriptfunction processPayment(payment: { type: string; amount: number }) {
if (payment.type === "card") {
console.log(`Paiement par carte : ${payment.amount}€`);
// appel Stripe
} else if (payment.type === "paypal") {
console.log(`Paiement PayPal : ${payment.amount}€`);
// appel PayPal API
} else if (payment.type === "virement") {
console.log(`Virement bancaire : ${payment.amount}€`);
// logique SEPA
}
// Ajouter un nouveau type = modifier cette fonction
}
Avec polymorphisme
typescriptinterface PaymentMethod {
process(amount: number): Promise<PaymentResult>;
describe(): string;
}
class CardPayment implements PaymentMethod {
constructor(private readonly cardToken: string) {}
async process(amount: number): Promise<PaymentResult> {
// Appel Stripe
return { success: true, transactionId: "ch_xxx" };
}
describe(): string {
return "Carte bancaire";
}
}
class PayPalPayment implements PaymentMethod {
constructor(private readonly email: string) {}
async process(amount: number): Promise<PaymentResult> {
// Appel PayPal
return { success: true, transactionId: "PAY-xxx" };
}
describe(): string {
return `PayPal (${this.email})`;
}
}
// La fonction ne connaît pas le type concret — elle appelle l'interface
async function processPayment(method: PaymentMethod, amount: number): Promise<void> {
const result = await method.process(amount);
if (result.success) {
console.log(`Paiement réussi via ${method.describe()}`);
}
}
// Ajouter un nouveau type de paiement = ajouter une classe, pas modifier processPayment
En Python, le polymorphisme fonctionne par duck typing — pas besoin d'interface explicite :
pythonfrom abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class PaymentResult:
success: bool
transaction_id: str
class PaymentMethod(ABC):
@abstractmethod
def process(self, amount: int) -> PaymentResult:
...
@abstractmethod
def describe(self) -> str:
...
class CardPayment(PaymentMethod):
def __init__(self, card_token: str) -> None:
self._token = card_token
def process(self, amount: int) -> PaymentResult:
# Appel Stripe
return PaymentResult(success=True, transaction_id="ch_xxx")
def describe(self) -> str:
return "Carte bancaire"
class PayPalPayment(PaymentMethod):
def __init__(self, email: str) -> None:
self._email = email
def process(self, amount: int) -> PaymentResult:
return PaymentResult(success=True, transaction_id="PAY-xxx")
def describe(self) -> str:
return f"PayPal ({self._email})"
def process_payment(method: PaymentMethod, amount: int) -> None:
result = method.process(amount)
if result.success:
print(f"Paiement réussi via {method.describe()}")
Le polymorphisme applique le principe Open/Closed des SOLID : le code est ouvert à l'extension (ajouter une classe) mais fermé à la modification (ne pas toucher processPayment).
Pilier 4 : L'abstraction
L'abstraction consiste à exposer une interface simple qui cache la complexité d'implémentation.
typescript// L'utilisateur de Repository ne sait pas si c'est PostgreSQL, Redis, ou un fichier JSON
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
// Requête SQL, mapping, gestion des erreurs — caché
const row = await this.db.query("SELECT * FROM users WHERE id = $1", [id]);
return row ? mapRowToUser(row) : null;
}
async save(user: User): Promise<void> {
await this.db.query(
"INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET ...",
[user.id, user.email, user.name]
);
}
async delete(id: string): Promise<void> {
await this.db.query("DELETE FROM users WHERE id = $1", [id]);
}
}
En tests, on remplace PostgresUserRepository par un InMemoryUserRepository — le code métier ne le sait pas.
Ce que la POO résout bien (et ce qu'elle ne résout pas)
Forces
- Modélisation de domaines avec des entités à cycle de vie (User, Order, Session)
- Encapsulation d'invariants complexes (transitions de statut, règles métier)
- Polymorphisme pour l'extensibilité sans modification
- Familiarité — la plupart des développeurs l'ont appris en premier
Limites
- L'état mutable partagé est une source de bugs difficiles à reproduire
- L'héritage crée des couplages forts difficiles à défaire
- Les hiérarchies profondes rendent le code imprévisible
- Plus difficile à paralléliser (état mutable non thread-safe par défaut)
Résumé
| Pilier | Ce qu'il apporte | Piège courant |
|---|---|---|
| Encapsulation | Protège les invariants | Getters/setters sur tout = pas d'encapsulation |
| Héritage | Réutilisation de code | Hiérarchies profondes, Fragile Base Class |
| Polymorphisme | Extensibilité sans modification | Sur-ingénierie pour des cas simples |
| Abstraction | Cache la complexité | Abstractions prématurées |
La POO n'est pas défaillante — elle est efficace sur les bons problèmes. L'enjeu est de savoir lesquels.
Étape suivante : 02 — Les piliers du fonctionnel
Sources
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2000). Design Principles and Design Patterns. Object Mentor.
- Dahl, O.-J., & Nygaard, K. (1966). SIMULA: An ALGOL-Based Simulation Language. Communications of the ACM, 9(9), 671-678.
- Liskov, B. (1987). Data Abstraction and Hierarchy. SIGPLAN Notices, 23(5).
- Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall.