Les 4 piliers de la POO
Notions théoriques
La Programmation Orientée Objet repose sur quatre piliers fondamentaux. Maîtrisez-les : ils reviennent dans tous les entretiens techniques et dans chaque framework professionnel.
Tableau de synthèse
| Pilier | Définition | Mots-clés C# | Objectif |
|---|---|---|---|
| Encapsulation | Cacher les détails internes, n'exposer que ce qui est nécessaire | private, public, protected, propriétés get/set | Protéger l'intégrité des données |
| Héritage | Une classe enfant réutilise et étend une classe parent | :, base, virtual, override, sealed | Réutiliser le code, spécialiser |
| Abstraction | Définir un contrat sans fournir l'implémentation | abstract, interface, méthodes abstraites | Masquer la complexité, normaliser |
| Polymorphisme | Un même appel produit des comportements différents selon le type réel | virtual/override, is, as, interfaces | Extensibilité, interchangeabilité |
Encapsulation — rappel
L'encapsulation consiste à protéger l'état interne d'un objet en déclarant ses champs private et en exposant uniquement des propriétés ou méthodes publiques contrôlées.
class CompteBancaire
{
private decimal _solde; // jamais modifiable directement de l'extérieur
public decimal Solde => _solde; // lecture seule
public void Deposer(decimal montant)
{
if (montant <= 0) throw new ArgumentException("Montant positif requis");
_solde += montant;
}
}
Héritage — rappel
L'héritage permet à une classe d'hériter des membres publics et protégés d'une classe parent et de les spécialiser via override.
class Notification
{
public virtual void Envoyer(string message)
=> Console.WriteLine($"[Notification] {message}");
}
class NotificationEmail : Notification
{
public string Destinataire { get; }
public NotificationEmail(string destinataire) => Destinataire = destinataire;
public override void Envoyer(string message)
=> Console.WriteLine($"[Email → {Destinataire}] {message}");
}
Abstraction — rappel
L'abstraction définit ce qu'un objet doit faire sans dire comment. En C#, cela passe par les classes abstraites et les interfaces.
abstract class Forme
{
public abstract double CalculerSurface(); // obligatoire pour les sous-classes
public abstract double CalculerPerimetre();
public void Decrire() // méthode concrète partagée
=> Console.WriteLine($"Surface : {CalculerSurface():F2} Périmètre : {CalculerPerimetre():F2}");
}
Polymorphisme — rappel
Le polymorphisme permet de manipuler des objets de types différents via un type commun (parent ou interface), chaque objet répondant à sa façon.
List<Forme> formes = new() { new Cercle(5), new Rectangle(4, 6), new Triangle(3, 4, 5) };
foreach (Forme f in formes)
{
f.Decrire(); // appelle le bon CalculerSurface() selon le type réel
}
Exemple combinant les 4 piliers
Voici un système de notifications qui utilise simultanément les 4 piliers :
using System;
using System.Collections.Generic;
// ABSTRACTION : interface commune
interface INotification
{
void Envoyer(string message);
string Canal { get; }
}
// ENCAPSULATION : champs privés, propriétés publiques
class NotificationEmail : INotification
{
private readonly string _destinataire; // privé
public string Canal => "Email";
public NotificationEmail(string destinataire)
{
if (string.IsNullOrWhiteSpace(destinataire))
throw new ArgumentException("Destinataire requis");
_destinataire = destinataire;
}
public void Envoyer(string message)
=> Console.WriteLine($"[Email → {_destinataire}] {message}");
}
// HÉRITAGE : NotificationSmsUrgent spécialise NotificationSms
class NotificationSms : INotification
{
protected readonly string _numero;
public string Canal => "SMS";
public NotificationSms(string numero) => _numero = numero;
public virtual void Envoyer(string message)
=> Console.WriteLine($"[SMS → {_numero}] {message}");
}
class NotificationSmsUrgent : NotificationSms // HÉRITAGE
{
public NotificationSmsUrgent(string numero) : base(numero) { }
public override void Envoyer(string message) // POLYMORPHISME : override
=> Console.WriteLine($"[SMS URGENT → {_numero}] ⚠ {message.ToUpper()} ⚠");
}
// Service d'orchestration
class ServiceNotification
{
private readonly List<INotification> _canaux = new();
public void AjouterCanal(INotification canal) => _canaux.Add(canal);
public void DiffuserATous(string message)
{
foreach (INotification n in _canaux)
{
n.Envoyer(message); // POLYMORPHISME : chaque canal répond à sa façon
}
}
}
// Programme principal
ServiceNotification service = new();
service.AjouterCanal(new NotificationEmail("alice@example.com"));
service.AjouterCanal(new NotificationSms("+33600000001"));
service.AjouterCanal(new NotificationSmsUrgent("+33600000002"));
service.DiffuserATous("Maintenance prévue demain à 22 h");
Introduction aux principes SOLID
Les 4 piliers POO permettent d'appliquer les principes SOLID, qui guident la conception de logiciels maintenables :
| Lettre | Principe | Résumé |
|---|---|---|
| S | Single Responsibility | Une classe = une seule responsabilité |
| O | Open/Closed | Ouvert à l'extension, fermé à la modification |
| L | Liskov Substitution | Une sous-classe peut remplacer sa classe parent sans casser le programme |
| I | Interface Segregation | Préférer plusieurs petites interfaces à une grosse interface générale |
| D | Dependency Inversion | Dépendre d'abstractions (interfaces), pas d'implémentations concrètes |
Vous mettez déjà en œuvre O (chaque nouveau taux TVA = nouvelle classe sans modifier l'existant) et D (injecter ICalculateurTaxe plutôt que TvaNormale directement) depuis la séance précédente.
Exemple pratique
using System;
using System.Collections.Generic;
// --- Abstraction ---
abstract class Vehicule
{
public string Immatriculation { get; }
public string Proprietaire { get; }
protected Vehicule(string immatriculation, string proprietaire)
{
Immatriculation = immatriculation;
Proprietaire = proprietaire;
}
// Méthode abstraite : chaque véhicule calcule son coût différemment
public abstract decimal CalculerCoutJournalier();
// Méthode concrète partagée
public virtual string Decrire() =>
$"{GetType().Name} [{Immatriculation}] — propriétaire : {Proprietaire}";
}
// --- Héritage + Encapsulation ---
class Voiture : Vehicule
{
private readonly int _nombrePlaces;
public Voiture(string immat, string proprietaire, int nombrePlaces)
: base(immat, proprietaire)
{
_nombrePlaces = nombrePlaces;
}
public override decimal CalculerCoutJournalier() => 45m + _nombrePlaces * 2m;
}
class Camion : Vehicule
{
private readonly decimal _chargeTonnes;
public Camion(string immat, string proprietaire, decimal chargeTonnes)
: base(immat, proprietaire)
{
_chargeTonnes = chargeTonnes;
}
public override decimal CalculerCoutJournalier() => 120m + _chargeTonnes * 10m;
public override string Decrire() => base.Decrire() + $" | Charge : {_chargeTonnes} t";
}
class Moto : Vehicule
{
public Moto(string immat, string proprietaire) : base(immat, proprietaire) { }
public override decimal CalculerCoutJournalier() => 25m;
}
// --- Polymorphisme ---
List<Vehicule> flotte = new()
{
new Voiture("AB-123-CD", "Dupont", 5),
new Camion("ZZ-999-ZZ", "Transport SA", 12m),
new Moto("EF-456-GH", "Martin"),
};
Console.WriteLine("=== Flotte de véhicules ===\n");
decimal totalJournee = 0m;
foreach (Vehicule v in flotte)
{
decimal cout = v.CalculerCoutJournalier();
Console.WriteLine($"{v.Decrire()}");
Console.WriteLine($" → Coût journalier : {cout:C}\n");
totalJournee += cout;
}
Console.WriteLine($"Coût total journée : {totalJournee:C}");
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Vous allez créer une flotte de véhicules mettant en œuvre les 4 piliers. Partez de cette structure :
abstract class Vehicule
{
public string Immatriculation { get; }
public string Proprietaire { get; }
protected Vehicule(string immatriculation, string proprietaire)
{
Immatriculation = immatriculation;
Proprietaire = proprietaire;
}
public abstract decimal CalculerCoutJournalier();
public virtual string Decrire() =>
$"{GetType().Name} [{Immatriculation}] propriétaire : {Proprietaire}";
}
Étape 1 — Créer la classe Voiture
Voiture hérite de Vehicule. Elle a un champ privé _nombrePlaces. Le coût journalier est 45 + nombrePlaces * 2.
readonly pour les champs initialisés une seule foisUn champ private readonly ne peut être assigné que dans le constructeur. Il protège contre les modifications accidentelles et exprime clairement l'intention : cette valeur est fixée à la création et ne change plus.
Étape 2 — Créer la classe Camion
Camion a un champ _chargeTonnes (decimal). Coût : 120 + chargeTonnes * 10. Son Decrire() ajoute la charge au texte parent.
base.Methode() pour enrichir sans dupliquerAppeler base.Decrire() permet d'enrichir le comportement parent plutôt que de le réécrire entièrement. Si la classe parent change son texte, la sous-classe bénéficie automatiquement de la mise à jour.
Étape 3 — Polymorphisme sur la flotte
Créez une List<Vehicule> avec au moins une Voiture, un Camion et une Moto, puis affichez le coût journalier de chaque véhicule et le total.
Déclarer List<Vehicule> (et non List<Voiture>) est la clé du polymorphisme. Le code de la boucle n'a pas besoin de savoir si c'est une Voiture ou un Camion : il appelle simplement Decrire() et CalculerCoutJournalier(), et C# invoque automatiquement la bonne implémentation.