CodeGym /Cours /C# SELF /Lien du polymorphisme avec les méthodes abstraites

Lien du polymorphisme avec les méthodes abstraites

C# SELF
Niveau 21 , Leçon 4
Disponible

1. Introduction

Revenons à notre zoo. On a une classe de base Animal (Animal) et ses dérivés : Dog (Chien), Cat (Chat), Fish (Poisson).

Dans les leçons précédentes, on a ajouté à Animal une méthode virtual MakeSound() :

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public virtual void MakeSound() // Méthode virtuelle
    {
        Console.WriteLine("Un animal quelconque fait un bruit."); // Implémentation par défaut
    }

    public void Sleep() // Méthode normale
    {
        Console.WriteLine($"{Name} dort.");
    }
}

public class Dog : Animal
{
    public override void MakeSound() // On redéfinit le son pour le chien
    {
        Console.WriteLine("Ouaf-ouaf !");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // On redéfinit le son pour le chat
    {
        Console.WriteLine("Miaou !");
    }
}

Ça marche super bien ! Quand on crée un Dog ou un Cat et qu’on appelle MakeSound(), on entend leur son unique. Mais si on crée juste un Animal ?

Animal genericAnimal = new Animal();
genericAnimal.Name = "Créature inconnue";
genericAnimal.MakeSound(); // Affichera : "Un animal quelconque fait un bruit."

Ça paraît logique. Mais parfois, l’implémentation de base par défaut n’a juste aucun sens. Et si Animal n’est pas un animal concret, mais plutôt un concept ? "Animal" en soi ne fait pas de bruit précis, ce sont les espèces concrètes qui le font. Ou imagine qu’on a une classe Shape (Figure) avec une méthode CalculateArea() (CalculerSurface). Quelle surface doit calculer juste Figure ? Aucune ! Un cercle ou un carré ont une surface, mais pas une "Figure" abstraite.

Dans ces cas-là, quand la classe de base ne peut pas (ou ne doit pas) fournir d’implémentation par défaut sensée, mais qu’elle oblige quand même toutes ses classes dérivées à implémenter cette méthode, c’est là que les méthodes abstraites et les classes abstraites entrent en jeu.

2. Classes abstraites : Quand le plan n’est pas encore une maison

Imagine que tu es architecte et que tu crées le plan d’une maison type. Mais pas juste une maison, une "Maison Conceptuelle". Elle a des trucs communs : murs, toit, fondations. Mais tu ne sais pas encore si ce sera un pavillon ou un gratte-ciel. Certaines parties du plan seront concrètes (genre la hauteur du plafond au rez-de-chaussée), d’autres — juste suggérées (genre "nombre d’étages", à définir plus tard).

Dans le monde C#, cette "Maison Conceptuelle" s’appelle une classe abstraite.
Classe abstraite — c’est une classe marquée avec le mot-clé abstract.


public abstract class Animal // Maintenant Animal est abstraite
{
    // ...
}
Déclaration d’une classe abstraite

Le point clé des classes abstraites :

  • On ne peut pas les instancier directement. Tu ne peux pas écrire new Animal(). Pourquoi ? Parce que Animal est maintenant un truc indéfini, un concept. Tu ne peux pas construire une "Maison Conceptuelle", tu peux construire seulement un pavillon concret ou un gratte-ciel.
    Si tu essaies new Animal(), le compilateur va direct te taper sur les doigts :
    Cannot create an instance of the abstract type or interface 'Animal'
    (Impossible de créer une instance du type abstrait ou de l’interface 'Animal')
    C’est une restriction super importante !
  • Elles peuvent contenir des membres abstraits. Et là, ça devient vraiment intéressant !

3. Méthodes abstraites : un contrat sans implémentation

Si la classe abstraite est une "Maison Conceptuelle", alors une méthode abstraite — c’est la partie du plan où il est écrit "faire ça", mais sans instructions concrètes "comment". Par exemple, "Construire la fondation" — c’est obligatoire pour une maison, mais la taille et les matériaux dépendent du type de maison.

Une méthode abstraite — c’est une méthode qui :

  • Est marquée avec le mot-clé abstract.
  • N’a pas de corps (donc pas de bloc {}). Elle finit par un point-virgule ;.
  • Peut être déclarée seulement dans une classe abstraite.

Faisons de notre méthode MakeSound() une méthode abstraite :


public abstract class Animal // On a rendu Animal abstraite
{
    public string Name { get; set; }
    public int Age { get; set; }

    public abstract void MakeSound(); // Voilà, méthode abstraite ! Pas de corps !

    public void Sleep() // Cette méthode reste normale, "concrète"
    {
        Console.WriteLine($"{Name} dort.");
    }
}

Regarde comment MakeSound() a changé ! Il n’a plus d’accolades ni d’implémentation par défaut. Maintenant il dit juste : "Tout animal doit savoir faire un bruit. Mais comment — à ceux qui héritent de moi de décider."

Règle importante : Si ta classe hérite d’une classe abstraite et n’est pas elle-même abstraite, elle doit redéfinir (avec override) toutes les méthodes abstraites de la classe de base. Ce n’est pas optionnel, c’est une exigence, un contrat ! Le compilateur C# est super strict là-dessus. Si tu oublies, il va te le rappeler direct :

public class Dog : Animal // Classe normale, pas abstraite
{
    // ERREUR DE COMPILATION !
    // 'Dog' does not implement inherited abstract member 'Animal.MakeSound()'
    // (La classe 'Dog' n’implémente pas le membre abstrait hérité 'Animal.MakeSound()')
    // Le compilateur attend MakeSound() avec override !
}

Pour enlever l’erreur, Dog et Cat doivent obligatoirement redéfinir MakeSound() :

public class Dog : Animal
{
    public override void MakeSound() // Obligé de redéfinir !
    {
        Console.WriteLine("Ouaf-ouaf !");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // Ici aussi !
    {
        Console.WriteLine("Miaou !");
    }
}

Comparaison entre virtual, abstract et override

Caractéristique virtual méthode abstract méthode override mot-clé
Où ça se place Dans une classe normale ou abstraite Seulement dans une classe abstraite Dans une classe dérivée (enfant)
Corps de la méthode A un corps (implémentation par défaut) Pas de corps (finit par ;) A un corps (nouvelle implémentation)
But Fournit une implémentation par défaut, mais permet aux classes dérivées de la changer Déclare un contrat : les classes dérivées doivent fournir leur propre implémentation Fournit une implémentation spécifique pour une méthode virtual ou abstract de la classe de base
Créer une instance de la classe de base ? Oui (si la classe de base n’est pas abstraite) Non (si la classe de base est abstraite) N/A (concerne la méthode, pas la classe)
Obligation de la classe dérivée Peut redéfinir (override) ou pas Doit redéfinir (override), si la classe dérivée n’est pas abstraite N/A

4. Le polymorphisme en action avec les méthodes abstraites

Là, ça devient vraiment fun ! On sait déjà que le polymorphisme permet de bosser avec des objets de différentes classes dérivées via une référence commune à la classe de base. Et ça marche même si la classe de base est abstraite !

Même si on ne peut pas créer une instance de Animal directement (rappelle-toi, new Animal() va planter), on peut utiliser le type Animal comme référence vers des objets des classes dérivées. C’est super puissant !

Continuons notre zoo. Imaginons qu’on a une ferme avec différents animaux. On veut que chaque animal fasse son bruit.


using System;

// Classe abstraite Animal
public abstract class Animal
{
    public string Name;
    public int Age;
    public Animal(string name, int age) { Name = name; Age = age; }
    public abstract void MakeSound();
    public void Sleep() { Console.WriteLine($"{Name} dort."); }
}

public class Dog : Animal
{
    public Dog(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Ouaf-ouaf !"); }
}

public class Cat : Animal
{
    public Cat(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Miaou !"); }
}

public class Fish : Animal
{
    public Fish(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Bloup-bloup"); }
}

class Program
{
    static void Main()
    {
        Animal[] animals = {
            new Dog("Rex", 3),
            new Cat("Whiskers", 5),
            new Fish("Nemo", 1)
        };

        foreach (Animal animal in animals)
        {
            Console.WriteLine($"\nSalut, je suis {animal.Name}, j’ai {animal.Age} ans.");
            animal.MakeSound();
            animal.Sleep();
        }
    }
}

Qu’est-ce qui se passe dans ce code ?

  1. On a déclaré Animal comme abstract class. Ça dit au compilateur : "Cette classe est un modèle, on ne peut pas l’instancier, mais on peut en hériter".
  2. On a déclaré public abstract void MakeSound(); dans Animal. Ça veut dire : "Toute classe qui hérite de Animal (et qui n’est pas abstraite elle-même) doit implémenter la méthode MakeSound()". C’est notre contrat !
  3. Dog, Cat et Fish suivent ce contrat sans broncher, en redéfinissant MakeSound() avec leur propre version. Si on oubliait de le faire pour l’un d’eux, le compilateur refuserait de compiler.
  4. Dans la méthode Main, on crée un tableau Animal[]. Même si Animal est abstraite, le tableau peut contenir des références vers des objets des classes dérivées (Dog, Cat, Fish), car ils sont des Animal !
  5. Quand on parcourt le tableau avec foreach et qu’on appelle animal.MakeSound(), grâce au polymorphisme, C# "sait" quelle version de MakeSound() appeler : Dog.MakeSound(), Cat.MakeSound() ou Fish.MakeSound(). C’est la méthode du type réel de l’objet référencé par Animal à ce moment-là, pas le type de la référence. C’est toute la magie du polymorphisme !
  6. Par contre, animal.Sleep() appelle l’implémentation concrète de la classe de base Animal, car cette méthode n’a pas été marquée comme virtual ou abstract, et n’a pas été redéfinie dans les classes enfants.

5. À quoi ça sert dans la vraie vie ?

"Ok, le zoo, les animaux... Mais à quoi ça sert quand je vais coder une vraie appli pour une banque ou un magasin ?" — tu te demandes. Et c’est une super question ! Les classes et méthodes abstraites sont un outil ultra puissant pour concevoir des systèmes flexibles et évolutifs.

Forcer l’implémentation d’un contrat : C’est l’avantage principal. Imagine que tu développes un framework pour des systèmes de paiement. Tu as une classe de base abstract class PaymentProcessor (ProcesseurDePaiement). Et tu sais que tout processeur de paiement doit savoir ProcessPayment() (TraiterPaiement), RefundPayment() (RembourserPaiement) et CheckStatus() (VérifierStatut). Mais comment ça marche pour PayPal, une carte bancaire ou Bitcoin — c’est totalement différent.
Tu déclares ces méthodes comme abstract dans PaymentProcessor.


public abstract class PaymentProcessor
{
    public abstract bool ProcessPayment(decimal amount, string currency, string cardNumber);
    public abstract bool RefundPayment(string transactionId);
    public abstract string CheckStatus(string transactionId);
    // ... d’autres méthodes qui peuvent être concrètes, genre le logging
    public void LogTransaction(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

public class PayPalProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // Ici, logique complexe pour bosser avec l’API PayPal
        Console.WriteLine($"PayPal : on traite {amount} {currency}...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "Terminé"; }
}

public class CreditCardProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // Ici, logique pour bosser avec la banque acquéreur
        Console.WriteLine($"CreditCard : {amount} {currency} de la carte {cardNumber.Substring(0,4)}XXXX...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "En cours"; }
}

Maintenant, tout dev qui voudra créer un nouveau processeur de paiement (genre BitcoinProcessor) sera obligé d’implémenter ces trois méthodes. Il ne pourra pas oublier RefundPayment() par accident, car le compilateur ne le laissera pas faire ! Ça garantit la cohérence dans ton système.

Flexibilité et évolutivité : Tu peux écrire du code qui bosse avec PaymentProcessor sans savoir quelle implémentation concrète sera utilisée. Par exemple, dans le code du panier d’un e-shop, tu appelles juste currentProcessor.ProcessPayment(), et le système choisit le bon processeur selon le mode de paiement choisi par l’utilisateur. Demain, un nouveau mode de paiement arrive — tu crées juste une nouvelle classe, tu hérites de PaymentProcessor, tu implémentes les méthodes abstraites, et tu n’as même pas à toucher au code principal du shop !

Éviter les implémentations vides : Si on utilisait des méthodes virtual au lieu de abstract, il faudrait leur donner une implémentation vide par défaut, ce qui pourrait être trompeur. abstract dit clairement : "Pas d’implémentation ici, et il ne peut pas y en avoir — va voir les héritiers !"

Améliorer l’architecture du code : Les classes abstraites aident à bien séparer la logique commune et la logique spécifique. La logique commune (genre LogTransaction dans PaymentProcessor) vit dans la classe de base, la spécifique (genre ProcessPayment) — dans les dérivées. Ça rend le code plus lisible, maintenable et testable. Ça permet aux designers de frameworks et de libs de définir des "points d’extension" que les utilisateurs de leurs libs doivent remplir.

6. Erreurs courantes et subtilités

Erreur n°1 : essayer de créer une instance d’une classe abstraite.
C’est toujours une erreur de compilation. Une classe abstraite — c’est un concept, pas un objet concret. Tu peux l’utiliser comme type de référence, mais tu ne peux pas écrire new Animal().

Erreur n°2 : oublier de redéfinir une méthode abstraite.
Si la classe dérivée n’est pas abstraite, elle doit implémenter toutes les méthodes abstract de la classe de base. Sinon — erreur de compilation. Le seul moyen de contourner ça — rendre la classe dérivée abstraite aussi, mais en général ce n’est pas ce que tu veux.

Erreur n°3 : confusion entre abstract et virtual.
abstract oblige à redéfinir la méthode, elle n’a pas de corps. virtual fournit une implémentation par défaut et peut être redéfinie. Tu ne peux pas déclarer une méthode abstract avec un corps ni une méthode virtual sans corps — c’est une erreur de syntaxe.

Erreur n°4 : utiliser new au lieu de override.
Si tu n’utilises pas override mais tu crées juste une méthode du même nom dans l’héritier, tu ne redéfinis pas, tu masques la méthode de la classe de base. Ça peut donner des comportements inattendus avec les appels polymorphes : c’est la méthode de la classe de base qui sera appelée.


public class Base
{
    public void DoSomething() { Console.WriteLine("Base"); }
}

public class Derived : Base
{
    public new void DoSomething() { Console.WriteLine("Derived"); }
}

Base obj = new Derived();
obj.DoSomething(); // Affichera : Base
2
Mission
C# SELF, niveau 21, leçon 4
Bloqué
Création d'une classe abstraite et son implémentation
Création d'une classe abstraite et son implémentation
2
Mission
C# SELF, niveau 21, leçon 4
Bloqué
Polymorphisme avec des méthodes abstraites
Polymorphisme avec des méthodes abstraites
1
Étude/Quiz
Notion de polymorphisme, niveau 21, leçon 4
Indisponible
Notion de polymorphisme
Polymorphisme et surcharge de méthodes
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION