Cours Programmation Orientée Objet Complet 🏗️

Les 12 chapitres essentiels — classes, héritage, polymorphisme, SOLID et design patterns

12
Chapitres
80+
Exemples
Python + TS
Langages
A1→C2
Niveaux

CHAPITRE 01

Introduction à la POO

🏗️ Pourquoi 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

CHAPITRE 02

Classes et objets

🐍 Python
# Classe = plan (blueprint), Objet = instance concrète

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

🔷 TypeScript
class User

const alice = new User(« Alice », « alice@mail.com »);
console.log(alice.greet());

CHAPITRE 03

Attributs et méthodes

📋 Types d'attributs et méthodes
class Product:
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.

CHAPITRE 04

Encapsulation

🔒 Protéger les données internes
# Python : convention _ (protégé) et __ (name mangling)

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.

CHAPITRE 05

Héritage

🧬 Réutiliser et spécialiser
# Classe parente (base)
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

CHAPITRE 06

Polymorphisme

🎭 Même interface, comportements différents
# Polymorphisme = traiter des objets différents de la même façon

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.

CHAPITRE 07

Classes abstraites et interfaces

📐 Contrats à respecter
# Python — ABC (Abstract Base Class)
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

// TypeScript — Interface (contrat pur, sans implémentation)
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.

CHAPITRE 08

Composition vs héritage

🧩 « Favor composition over inheritance »
# ❌ Héritage fragile — hiérarchie rigide
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)

CHAPITRE 09

Principes SOLID

🏛️ Les 5 principes
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
# S — Single Responsibility

# ❌ 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…

CHAPITRE 10

Design patterns essentiels

🔧 Patterns les plus utiles
# SINGLETON — une seule instance
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)

CHAPITRE 11

POO en pratique

🛒 Exemple complet : système e-commerce
from dataclasses import dataclass, field
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.

CHAPITRE 12

Bonnes pratiques

⚖️ POO dans chaque langage
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
✅ Bonnes pratiques

✅ À 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)

🧠 Quiz
Composition vs héritage — la règle simple ?
Héritage = relation « IS-A » (un Chien EST un Animal). Le enfant peut remplacer le parent partout (Liskov). Composition = relation « HAS-A » (une Voiture A un Moteur). Plus flexible, couplage faible. Règle : si vous hésitez → composition. L'héritage est approprié quand la relation IS-A est vraie ET que vous avez besoin du polymorphisme. « Favor composition over inheritance » — Gang of Four.
Qu'est-ce que l'injection de dépendance et pourquoi c'est important ?
Au lieu qu'une classe crée elle-même ses dépendances (self.db = MySQL()), on les passe en paramètre du constructeur (def __init__(self, db: Database)). Avantages : testabilité (on peut injecter un mock), flexibilité (changer MySQL → PostgreSQL sans toucher la classe), découplage (la classe ne connaît que l'abstraction, pas l'implémentation). C'est le principe SOLID « D » (Dependency Inversion).
@dataclass vs classe normale — quand utiliser quoi ?
@dataclass = pour les classes qui portent principalement des données (Product, User, Config, CartItem). Génère automatiquement __init__, __repr__, __eq__. Classe normale = quand la classe a beaucoup de logique métier, des états complexes, ou des invariants à protéger (BankAccount, Cart, OrderService). Règle : si la classe est surtout des attributs → @dataclass. Si elle a beaucoup de méthodes → classe normale.

Cours POO Complet — Classes, héritage, polymorphisme, SOLID et design patterns

Référence : Refactoring Guru | Python Classes