Cours Programmation Orientée Objet Complet 🏗️
Les 12 chapitres essentiels — classes, héritage, polymorphisme, SOLID et design patterns
🏠 Hub Programmation
✨ Clean Code
🐍 Python
🔷 TypeScript
☕ Java
Introduction à la POO
La Programmation Orientée Objet organise le code autour d' »objets » — des entités qui regroupent des données (attributs) et des comportements (méthodes). Au lieu d'avoir des fonctions éparpillées et des variables globales, chaque objet est responsable de ses propres données. Une voiture a des attributs (couleur, vitesse) et des méthodes (accélérer, freiner). Un utilisateur a un nom, un email et peut se connecter. La POO rend le code plus modulaire, réutilisable et maintenable — c'est le paradigme dominant dans l'industrie (Java, C#, Python, TypeScript, Swift, Kotlin).
| Pilier | Description | Bénéfice |
|---|---|---|
| Encapsulation | Cacher les détails internes | Protéger les données, simplifier l'API |
| Héritage | Réutiliser le code d'une classe parente | Éviter la duplication |
| Polymorphisme | Même interface, comportements différents | Flexibilité, extensibilité |
| Abstraction | Exposer l'essentiel, masquer le complexe | Simplicité d'utilisation |
Classes et objets
class User:
def __init__(self, name: str, email: str):
self.name = name # Attribut d'instance
self.email = email
self.is_active = True # Valeur par défaut
def greet(self) -> str:
return f »Bonjour, je suis «
def __repr__(self) -> str:
return f »User(name='', email='') »
# Créer des objets (instances)
alice = User(« Alice », « alice@mail.com »)
bob = User(« Bob », « bob@mail.com »)
print(alice.greet()) # « Bonjour, je suis Alice »
print(alice.name) # « Alice »
print(alice) # User(name='Alice', email='alice@mail.com')
const alice = new User(« Alice », « alice@mail.com »);
console.log(alice.greet());
Attributs et méthodes
tva = 0.20 # Attribut de CLASSE (partagé par toutes les instances)
def __init__(self, name: str, price: float):
self.name = name # Attribut d'INSTANCE (propre à chaque objet)
self.price = price
# Méthode d'instance — accède à self
def price_ttc(self) -> float:
return self.price * (1 + self.tva)
# Méthode de classe — accède à cls (la classe)
@classmethod
def set_tva(cls, rate: float):
cls.tva = rate
# Méthode statique — ni self ni cls
@staticmethod
def is_valid_price(price: float) -> bool:
return price > 0
# Property — getter avec syntaxe d'attribut
@property
def display_price(self) -> str:
return f » € »
p = Product(« Laptop », 999)
print(p.price_ttc()) # 1198.80
print(p.display_price) # « 1198.80 € » (pas de parenthèses !)
Product.set_tva(0.055) # Change la TVA pour TOUS les produits
@property transforme une méthode en attribut calculé. L'appelant écrit p.display_price (sans parenthèses) comme si c'était un simple attribut, mais en réalité c'est calculé à la volée. @classmethod sert aux constructeurs alternatifs et à modifier l'état de la classe. @staticmethod est une fonction utilitaire rattachée à la classe.
Encapsulation
class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self.owner = owner
self.__balance = balance # __ = « privé » (name mangling)
@property
def balance(self) -> float:
return self.__balance
def deposit(self, amount: float):
if amount <= 0:
raise ValueError(« Le montant doit être positif »)
self.__balance += amount
def withdraw(self, amount: float):
if amount > self.__balance:
raise ValueError(« Solde insuffisant »)
self.__balance -= amount
account = BankAccount(« Alice », 1000)
account.deposit(500)
print(account.balance) # 1500 (via @property)
# account.__balance = -999 # ❌ AttributeError (protégé)
# account.withdraw(5000) # ❌ ValueError (validation)
L'encapsulation protège l'intégrité des données. Sans elle, n'importe quel code pourrait écrire account.balance = -999. Avec l'encapsulation, toute modification passe par deposit() et withdraw() qui valident les règles métier. En Python, __ est une convention — en TypeScript/Java, on utilise private qui est réellement enforced par le compilateur.
Héritage
class Animal:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def speak(self) -> str:
return « … »
def info(self) -> str:
return f » ( ans) »
# Classes enfants (spécialisées)
class Dog(Animal):
def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age) # Appeler le parent
self.breed = breed
def speak(self) -> str: # Override
return « Woof! »
class Cat(Animal):
def speak(self) -> str:
return « Meow! »
dog = Dog(« Rex », 5, « Berger »)
cat = Cat(« Mimi », 3)
print(dog.speak()) # « Woof! » (méthode override)
print(dog.info()) # « Rex (5 ans) » (méthode héritée)
print(dog.breed) # « Berger » (attribut propre)
# isinstance et issubclass
isinstance(dog, Dog) # True
isinstance(dog, Animal) # True (un Dog EST un Animal)
issubclass(Dog, Animal) # True
Polymorphisme
class Shape:
def area(self) -> float:
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
# Polymorphisme en action : même méthode .area(), résultats différents
shapes: list[Shape] = [Circle(5), Rectangle(4, 6), Circle(3)]
for shape in shapes:
print(f » : »)
# Circle : 78.54
# Rectangle : 24.00
# Circle : 28.27
# Duck typing (Python) — pas besoin d'héritage formel !
# « Si ça marche comme un canard, c'est un canard »
def print_area(shape): # Accepte N'IMPORTE QUEL objet avec .area()
print(shape.area())
Le polymorphisme permet d'ajouter de nouveaux types sans modifier le code existant. La boucle for shape in shapes fonctionne avec Circle, Rectangle, Triangle… sans jamais changer le code de la boucle. C'est le Open/Closed Principle (SOLID) : ouvert à l'extension, fermé à la modification.
Classes abstraites et interfaces
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
pass
@abstractmethod
def refund(self, amount: float) -> bool:
pass
def log(self, message: str): # Méthode concrète (partagée)
print(f »[Payment] « )
class StripeProcessor(PaymentProcessor):
def pay(self, amount: float) -> bool:
self.log(f »Stripe: paiement de € »)
return True
def refund(self, amount: float) -> bool:
self.log(f »Stripe: remboursement de € »)
return True
# processor = PaymentProcessor() # ❌ TypeError: classe abstraite
processor = StripeProcessor() # ✅ Implémente tout
interface PaymentProcessor
class StripeProcessor implements PaymentProcessor {
pay(amount: number): boolean {
console.log(`Stripe: $€`);
return true;
}
refund(amount: number): boolean { return true; }
}
Classe abstraite vs Interface : une classe abstraite peut contenir du code partagé (méthode log()) + des méthodes abstraites. Une interface (TypeScript, Java) est un contrat pur — aucune implémentation. Python n'a pas de mot-clé « interface » — ABC remplit les deux rôles. Utilisez Protocol (Python 3.8+) pour le structural typing.
Composition vs héritage
class FlyingSwimmingDuck(FlyingAnimal, SwimmingAnimal):
pass # Héritage multiple → problème du diamant
# ✅ Composition — assembler des comportements
class Engine:
def __init__(self, horsepower: int):
self.horsepower = horsepower
def start(self):
print(f »Moteur cv démarré »)
class GPS:
def navigate(self, destination: str):
print(f »Navigation vers « )
class Car:
def __init__(self, model: str, hp: int):
self.model = model
self.engine = Engine(hp) # Car HAS-A Engine
self.gps = GPS() # Car HAS-A GPS
def drive(self, destination: str):
self.engine.start()
self.gps.navigate(destination)
car = Car(« Tesla », 300)
car.drive(« Paris »)
# « Moteur 300cv démarré »
# « Navigation vers Paris »
| Critère | Héritage (IS-A) | Composition (HAS-A) |
|---|---|---|
| Relation | Dog est un Animal | Car a un Engine |
| Couplage | Fort (parent → enfant) | Faible (composants interchangeables) |
| Flexibilité | Rigide (hiérarchie fixe) | Flexible (changer à runtime) |
| Réutilisation | Verticale (une seule chaîne) | Horizontale (mixer les composants) |
| Quand | Vraie relation « est un » + polymorphisme | Par défaut (la plupart du temps) |
Principes SOLID
| Lettre | Principe | En une phrase | Violation typique |
|---|---|---|---|
| S | Single Responsibility | Une classe = une seule raison de changer | Classe User qui gère auth + email + BDD |
| O | Open/Closed | Ouvert à l'extension, fermé à la modification | if/elif pour chaque nouveau type |
| L | Liskov Substitution | Un enfant remplace le parent sans casser | Square hérite de Rectangle et casse |
| I | Interface Segregation | Petites interfaces spécifiques, pas une grosse | Interface God avec 20 méthodes |
| D | Dependency Inversion | Dépendre des abstractions, pas du concret | Classe liée à MySQL directement |
# ❌ Une classe fait tout
class User:
def save_to_db(self): …
def send_email(self): …
def generate_report(self): …
# ✅ Chaque classe a une responsabilité
class User:
def __init__(self, name, email): …
class UserRepository:
def save(self, user: User): …
class EmailService:
def send(self, to: str, body: str): …
# D — Dependency Inversion
# ❌ Dépend du concret
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # Couplé à MySQL
# ✅ Dépend de l'abstraction
class OrderService:
def __init__(self, db: Database): # Injection de dépendance
self.db = db
# → Fonctionne avec MySQL, PostgreSQL, SQLite, Mock…
Design patterns essentiels
class Database:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# FACTORY — créer des objets sans exposer la logique
class NotificationFactory:
@staticmethod
def create(channel: str) -> Notification:
if channel == « email »: return EmailNotification()
if channel == « sms »: return SMSNotification()
if channel == « push »: return PushNotification()
raise ValueError(f »Canal inconnu: « )
# OBSERVER — notifier quand l'état change
class EventEmitter:
def __init__(self):
self._listeners =
def on(self, event: str, callback):
self._listeners.setdefault(event, []).append(callback)
def emit(self, event: str, data=None):
for cb in self._listeners.get(event, []):
cb(data)
# STRATEGY — changer l'algorithme à runtime
class Sorter:
def __init__(self, strategy):
self.strategy = strategy # Fonction ou objet
def sort(self, data):
return self.strategy(data)
| Pattern | Quand | Exemples réels |
|---|---|---|
| Singleton | Une seule instance (connexion BDD, config) | Database pool, Logger |
| Factory | Créer des objets selon un paramètre | Notifications, parsers de fichiers |
| Observer | Réagir aux changements d'état | Event listeners, pub/sub, webhooks |
| Strategy | Changer l'algorithme sans modifier le code | Tri, validation, compression |
| Repository | Abstraire l'accès aux données | UserRepository (BDD interchangeable) |
POO en pratique
from datetime import datetime
from typing import Protocol
# Dataclass — classe de données sans boilerplate
@dataclass
class Product:
name: str
price: float
stock: int = 0
@dataclass
class CartItem:
product: Product
quantity: int
@property
def total(self) -> float:
return self.product.price * self.quantity
# Protocol (structural typing — « interface » Python moderne)
class PaymentMethod(Protocol):
def charge(self, amount: float) -> bool: …
class Cart:
def __init__(self):
self.items: list[CartItem] = []
def add(self, product: Product, qty: int = 1):
if qty > product.stock:
raise ValueError(« Stock insuffisant »)
self.items.append(CartItem(product, qty))
@property
def total(self) -> float:
return sum(item.total for item in self.items)
def checkout(self, payment: PaymentMethod) -> bool:
return payment.charge(self.total)
@dataclass réduit le boilerplate — génère automatiquement __init__, __repr__, __eq__. Protocol (Python 3.8+) est le pattern moderne pour les interfaces : n'importe quel objet avec une méthode charge(float) → bool est accepté, sans besoin d'hériter. C'est le duck typing avec du type checking.
Bonnes pratiques
| Aspect | Python | TypeScript | Java |
|---|---|---|---|
| Privé | __ (convention) | private (enforced) | private (enforced) |
| Interface | ABC / Protocol | interface | interface |
| Dataclass | @dataclass | — | record (Java 16+) |
| Héritage multiple | ✅ Oui (MRO) | ❌ Non (implements multi) | ❌ Non |
| Typing | Optionnel (hints) | Obligatoire | Obligatoire |
✅ À FAIRE
• Composition par défaut, héritage si « IS-A » vrai
• SOLID — surtout S (Single Responsibility) et D (DI)
• @dataclass pour les classes de données
• Protocol / Interface pour les contrats
• Injection de dépendance (constructeur)
• Noms de classes = noms (User, Cart, Payment)
• Noms de méthodes = verbes (create, validate, send)
• Type hints systématiques (Python, TypeScript)
• Petites classes focalisées (< 200 lignes)
• Tester avec des mocks (grâce à l'injection)
❌ À ÉVITER
• Héritage profond (> 2 niveaux)
• God Class (une classe qui fait tout)
• Getters/setters inutiles (utiliser @property)
• Héritage pour « réutiliser du code » sans IS-A
• Singleton abusif (état global déguisé)
• Classes sans comportement (juste des données → @dataclass)
• Mixin excessif (héritage multiple complexe)
• Abstraction prématurée (pas de pattern avant d'en avoir besoin)
• Ignorer le duck typing en Python
• Classes avec trop de dépendances (> 5)
🏠 Hub Programmation
✨ Cours Clean Code
🧠 Cours Algorithmes
🐍 Cours Python
🔷 Cours TypeScript
☕ Cours Java
Cours POO Complet — Classes, héritage, polymorphisme, SOLID et design patterns
Référence : Refactoring Guru | Python Classes

