Aller au contenu principal

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

abstractvirtual
Corps de méthodeInterditObligatoire
Implémentation dans la sous-classeObligatoire (override)Optionnelle (override)
Classe doit êtreabstractN'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


Peut-on instancier directement une classe abstraite ?


Quelle est l'obligation imposée par une méthode abstraite ?


Une méthode abstraite peut-elle avoir un corps ?


Quelle est la différence principale entre `abstract` et `virtual` ?


Qu'est-ce que le Template Method Pattern ?


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


Bonne pratique - Abstraire ce qui varie, partager ce qui est commun

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


Bonne pratique - Chaque sous-classe = une variante clairement identifiée

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


Bonne pratique - Le code client travaille avec le type abstrait

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.

📌 Une solution