Aller au contenu principal

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

PilierDéfinitionMots-clés C#Objectif
EncapsulationCacher les détails internes, n'exposer que ce qui est nécessaireprivate, public, protected, propriétés get/setProtéger l'intégrité des données
HéritageUne classe enfant réutilise et étend une classe parent:, base, virtual, override, sealedRéutiliser le code, spécialiser
AbstractionDéfinir un contrat sans fournir l'implémentationabstract, interface, méthodes abstraitesMasquer la complexité, normaliser
PolymorphismeUn même appel produit des comportements différents selon le type réelvirtual/override, is, as, interfacesExtensibilité, 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 :

LettrePrincipeRésumé
SSingle ResponsibilityUne classe = une seule responsabilité
OOpen/ClosedOuvert à l'extension, fermé à la modification
LLiskov SubstitutionUne sous-classe peut remplacer sa classe parent sans casser le programme
IInterface SegregationPréférer plusieurs petites interfaces à une grosse interface générale
DDependency InversionDépendre d'abstractions (interfaces), pas d'implémentations concrètes
info

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


Quel pilier permet de protéger l'état interne d'un objet via des champs `private` ?


Quel pilier permet à une liste de `Vehicule` d'appeler `CalculerCoutJournalier()` sans connaître le type réel ?


Quelle lettre de SOLID dit qu'une classe doit être ouverte à l'extension mais fermée à la modification ?


Que signifie le principe D (Dependency Inversion) ?


Quel est le rôle d'une méthode abstraite dans une classe abstraite ?


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.


Bonne pratique - readonly pour les champs initialisés une seule fois

Un 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.


Bonne pratique - base.Methode() pour enrichir sans dupliquer

Appeler 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.


Bonne pratique - Liste du type parent, pas du type concret

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.

📌 Une solution