Classes abstraites
Notions théoriques
Qu'est-ce qu'une classe abstraite ?
Une classe abstraite est une classe incomplète — elle définit un contrat que les sous-classes doivent compléter. On ne peut pas instancier directement une classe abstraite.
abstract class Forme // ne peut pas être instanciée
{
public string Nom { get; init; } = "";
public abstract double Aire(); // DOIT être implémentée par les sous-classes
public void Afficher() // méthode concrète partagée
=> Console.WriteLine($"{Nom} — Aire : {Aire():F2}");
}
// new Forme(); // ERREUR : classe abstraite, pas instanciable
class Cercle : Forme
{
public double Rayon { get; }
public Cercle(double rayon) { Nom = "Cercle"; Rayon = rayon; }
public override double Aire() => Math.PI * Rayon * Rayon; // obligatoire !
}
Méthodes abstraites vs méthodes virtuelles
abstract | virtual | |
|---|---|---|
| Corps de méthode | Interdit | Obligatoire |
| Implémentation dans la sous-classe | Obligatoire (override) | Optionnelle (override) |
| Classe doit être | abstract | N'importe quelle |
abstract class Base
{
public abstract void FaireQuelqueChose(); // pas de corps, DOIT être override
public virtual string Description() => "Base"; // a un corps, PEUT être override
}
Constructeur dans une classe abstraite
Une classe abstraite peut avoir un constructeur, appelé par les sous-classes avec base(...) :
abstract class Employe
{
public string Prenom { get; }
public string Nom { get; }
public decimal Salaire { get; protected set; }
protected Employe(string prenom, string nom, decimal salaire)
{
Prenom = prenom;
Nom = nom;
Salaire = salaire;
}
public abstract decimal CalculerPrime(); // chaque type calcule sa prime
public override string ToString()
=> $"{Prenom} {Nom} | Salaire : {Salaire:F0} € | Prime : {CalculerPrime():F0} €";
}
Template Method Pattern
Un pattern classique avec les classes abstraites : la classe parente définit l'algorithme général, les sous-classes fournissent les détails :
abstract class Rapport
{
public void Generer() // template method — séquence fixe
{
AfficherEntete();
AfficherContenu(); // variable selon le type de rapport
AfficherPiedDePage();
}
private void AfficherEntete() => Console.WriteLine("=== RAPPORT ===");
protected abstract void AfficherContenu(); // à implémenter
private void AfficherPiedDePage() => Console.WriteLine($"Généré le {DateTime.Now:dd/MM/yyyy}");
}
class RapportNotes : Rapport
{
private double[] _notes;
public RapportNotes(double[] notes) { _notes = notes; }
protected override void AfficherContenu()
{
Console.WriteLine($"Nombre de notes : {_notes.Length}");
Console.WriteLine($"Moyenne : {_notes.Average():F2}/20");
}
}
new RapportNotes([14, 16, 12, 18]).Generer();
Exemple pratique
abstract class Paiement
{
public decimal Montant { get; }
public string Reference { get; }
public DateTime Date { get; }
protected Paiement(decimal montant, string reference)
{
if (montant <= 0)
throw new ArgumentOutOfRangeException(nameof(montant), "Le montant doit être positif.");
Montant = montant;
Reference = reference;
Date = DateTime.Now;
}
public abstract bool Valider();
public abstract string TypePaiement { get; }
public string GenererRecu()
=> Valider()
? $"[REÇU] {TypePaiement} | {Montant:F2} € | Réf. {Reference} | {Date:dd/MM/yyyy HH:mm}"
: $"[REFUSÉ] {TypePaiement} | {Montant:F2} € | Réf. {Reference}";
}
class PaiementCarte : Paiement
{
private string _numeroCarte;
public PaiementCarte(decimal montant, string ref_, string numeroCarte)
: base(montant, ref_) { _numeroCarte = numeroCarte; }
public override string TypePaiement => "Carte bancaire";
public override bool Valider() => _numeroCarte.Length == 16 && Montant <= 5000m;
}
class PaiementVirement : Paiement
{
private string _iban;
public PaiementVirement(decimal montant, string ref_, string iban)
: base(montant, ref_) { _iban = iban; }
public override string TypePaiement => "Virement bancaire";
public override bool Valider() => _iban.StartsWith("FR") && _iban.Length == 27;
}
Paiement[] paiements =
[
new PaiementCarte (49.99m, "CMD-001", "1234567890123456"),
new PaiementCarte (150.00m, "CMD-002", "short"),
new PaiementVirement(1500m, "CMD-003", "FR7630006000011234567890189"),
];
foreach (var p in paiements)
Console.WriteLine(p.GenererRecu());
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Vous allez créer une hiérarchie de modes de livraison avec Template Method.
Étape 1 — Classe abstraite ModeLivraison
GenererBordereau() est identique pour tous les modes de livraison — elle est concrète dans la classe abstraite. CalculerCout() et NomMode varient selon le mode — elles sont abstraites. Ce découpage applique le principe DRY (Don't Repeat Yourself).
Étape 2 — Sous-classes concrètes
Si le tarif Standard change, on modifie uniquement LivraisonStandard. Si on ajoute un mode "Drone", on crée une nouvelle classe sans toucher aux existantes. C'est l'Open/Closed Principle.
Étape 3 — Utilisation polymorphe
Le code qui affiche les bordereaux n'a pas besoin de savoir si c'est une LivraisonStandard ou une LivraisonExpress. Ajouter un nouveau mode de livraison ne nécessite aucune modification de ce code client.