1. Le polymorphisme en pratique
En programmation, polymorphisme — c’est comme une télécommande universelle : tu appuies sur le même bouton "Volume+", et ça contrôle la télé, la chaîne hi-fi ou la clim — chaque appareil réagit à sa façon, mais l’interface est la même ! Pareil, des objets de types différents peuvent réagir différemment à un même appel de méthode, si cette méthode est définie dans leur ancêtre commun comme virtual.
En C#, le polymorphisme se manifeste quand une variable du type de la classe de base (ou d’une interface) peut "contenir" un objet de n’importe lequel de ses descendants, et les appels de méthodes virtuelles sur cette variable exécutent l’implémentation spécifique, la "vraie" — celle définie par l’objet lui-même. C’est la base des architectures où la logique change dynamiquement.
Ça sert à quoi, à part dans les bouquins ?
Oui ! Pratiquement dans n’importe quel projet où tu bosses avec des objets différents mais similaires — des animaux dans un zoo aux éléments d’UI, des gestionnaires d’événements aux systèmes de gestion documentaire.
- Permet de créer des algos universels — du code qui bosse avec des entités abstraites sans se soucier des détails de l’implémentation concrète.
- Garanti l’évolutivité — ajoute autant de nouveaux types "d’animaux", "de figures", "de gestionnaires" que tu veux, sans toucher au code existant.
- Réduit la dépendance entre les composants du programme (c’est un des sujets importants pour les entretiens et l’architecture !).
2. Syntaxe de base et mécanique
Allez, on se rappelle et on améliore nos classes Animal, Dog, Cat pour montrer le polymorphisme en action. Puisqu’on a commencé avec l’appli "Zoo Virtuel", on va la pimper un peu.
Classes de base avec méthodes virtuelles
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Méthode virtuelle — on peut la redéfinir
public virtual void MakeSound()
{
Console.WriteLine($"{Name} fait un certain bruit...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} dit : Ouaf-ouaf !");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} dit : Miaou !");
}
}
Utiliser le polymorphisme : exemple avec une collection d’animaux
Imaginons qu’on a une liste d’animaux — domestiques ou pas — et on veut tous les faire "parler". Sans polymorphisme, il faudrait checker le type et écrire plein de code en double. Avec le polymorphisme — c’est clean et compact !
// On crée un tableau d’animaux de différents types
Animal[] animals = new Animal[]
{
new Dog("Spot"),
new Cat("Whiskers"),
new Dog("Rover"),
new Cat("Mittens"),
};
// On parcourt tout le tableau et on demande à chacun de faire un bruit
foreach (var animal in animals)
{
animal.MakeSound(); // Ça va appeler la méthode de Dog ou Cat, pas celle de Animal !
}
Résultat :
Spot dit : Ouaf-ouaf !
Whiskers dit : Miaou !
Rover dit : Ouaf-ouaf !
Mittens dit : Miaou !
Voilà la magie : un code universel — des résultats différents selon le vrai type de l’objet.
Diagramme : comment marche le polymorphisme
Animal (classe de base)
/ \
Dog Cat
Quand tu appelles animal.MakeSound(), où Animal peut contenir une instance de Dog ou Cat, le runtime .NET se débrouille tout seul pour savoir quelle méthode MakeSound() il doit appeler.
3. Résoudre des tâches typiques avec le polymorphisme
Exemple 1 : Liste universelle, actions différentes
Imaginons que tu codes un jeu. Tu as une classe de base GameObject, des dérivés — ennemis, alliés, obstacles. Ils peuvent tous bouger, ils ont tous une méthode Update(), mais l’implémentation change.
public class GameObject
{
public virtual void Update() { }
}
public class Enemy : GameObject
{
public override void Update()
{
Console.WriteLine("L’ennemi attaque !");
}
}
public class Friend : GameObject
{
public override void Update()
{
Console.WriteLine("L’allié aide !");
}
}
GameObject[] objects = new GameObject[]
{
new Enemy(),
new Friend(),
new Enemy()
};
foreach (var obj in objects)
{
obj.Update();
}
// Ça affiche :
// L’ennemi attaque !
// L’allié aide !
// L’ennemi attaque !
Exemple 2 : Passer des objets dans des méthodes
Tu peux accepter un paramètre du type de base, et utiliser n’importe quel type héritier. Ça fait gagner un temps fou, surtout si tu ajoutes de nouveaux descendants plus tard.
public static void FeedAnimal(Animal animal)
{
Console.Write($"{animal.Name} : ");
animal.MakeSound();
Console.WriteLine("Et reçoit de la nourriture.");
}
FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Bella"));
// Résultat :
// Rex : Rex dit : Ouaf-ouaf !
// Et reçoit de la nourriture.
// Bella : Bella dit : Miaou !
// Et reçoit de la nourriture.
À noter : écrire une telle méthode sans polymorphisme serait super lourd — il faudrait vérifier le type de l’animal, faire plein de if et appeler les bonnes méthodes à la main.
Remarque importante : liaison à l’exécution
Le polymorphisme marche parce que les appels de méthodes virtuelles sont faits dynamiquement, c’est-à-dire à l’exécution du programme. On appelle ça le late binding. Même si on connaît la variable comme Animal, la méthode appelée sera celle définie dans l’objet réel.
Si la méthode n’est pas marquée virtual, l’appel ira toujours sur le code déclaré pour la variable (et pas pour l’objet). Donc n’hésite pas à mettre le mot-clé virtual si tu veux de la flexibilité !
4. Pratique : on fait évoluer notre appli
Imaginons que tu veux ajouter une nouvelle feature à ton zoo virtuel : maintenant chaque animal doit non seulement faire MakeSound(), mais aussi se déplacer (Move()). Mais chacun le fait à sa façon.
1. On ajoute une méthode virtuelle dans la classe de base
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void MakeSound()
{
Console.WriteLine($"{Name} fait un certain bruit...");
}
public virtual void Move()
{
Console.WriteLine($"{Name} se déplace d’une manière non précisée...");
}
}
2. Dans les classes dérivées, on fait Move() à notre sauce
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} dit : Ouaf-ouaf !");
}
public override void Move()
{
Console.WriteLine($"{Name} court après le bâton.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} dit : Miaou !");
}
public override void Move()
{
Console.WriteLine($"{Name} avance sur la pointe des pattes.");
}
}
3. On utilise les deux méthodes dans une collection
Animal[] animals = new Animal[]
{
new Dog("Buster"),
new Cat("Lucy")
};
foreach (var animal in animals)
{
animal.MakeSound();
animal.Move();
}
Résultat :
Buster dit : Ouaf-ouaf !
Buster court après le bâton.
Lucy dit : Miaou !
Lucy avance sur la pointe des pattes.
Dans les vraies applis, cette approche permet d’écrire des modules super puissants et réutilisables. Par exemple, dans les jeux, c’est la base de tous les managers d’objets — des NPC aux effets.
5. Exercice pratique : dessiner différentes figures
Voyons un exemple hors du zoo, côté graphique. On crée une classe de base Shape avec une méthode virtuelle Draw(), puis on l’étend.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("On dessine une figure indéfinie.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("On dessine un cercle.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("On dessine un rectangle.");
}
}
// Collection de figures
Shape[] shapes = new Shape[]
{
new Circle(),
new Rectangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
Ici, Draw() est appelée sur une variable de type Shape, mais en vrai ce sont les méthodes de Circle ou Rectangle qui sont exécutées. Dans les vrais éditeurs graphiques et libs, genre WinForms ou WPF — c’est exactement comme ça que ça marche.
6. Approche générale : écrire des algos universels
Le polymorphisme transforme ton code en outil flexible et évolutif. Par exemple, dans une collection tu peux avoir non seulement Dog et Cat, mais aussi Hamster, Parrot, et tout ce que tu veux — et tu n’écris pas une ligne de plus pour les traiter tous pareil.
En plus, tu peux sans souci passer des objets hérités dans des méthodes qui prennent le type de base :
void PrintAnimalInfo(Animal animal)
{
Console.WriteLine($"Nom : {animal.Name}");
animal.MakeSound();
animal.Move();
}
Animal hamster = new Animal("Hammy");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // Utilise la méthode de Animal
PrintAnimalInfo(dog); // Utilise la version Dog
7. Astuces utiles
Questions fréquentes et pièges classiques
Beaucoup de débutants pensent que si tu crées un objet Dog, puis déclares la variable Dog myDog, c’est pareil que Animal myDog. En vrai, si tu déclares la variable comme Animal, elle "voit" seulement ce qui est dans Animal (sauf les méthodes overridées), alors que déclarée comme Dog — elle voit tout : Bark() et les propriétés spécifiques.
À savoir aussi : si une méthode dans la classe de base n’est pas marquée virtual, tu ne peux pas la redéfinir. Si tu tentes d’écrire override dans le descendant pour une telle méthode — le compilateur va râler et sortir une erreur.
D’ailleurs, si tu veux vraiment remplacer une méthode non virtuelle, utilise le mot-clé new, mais ça c’est pour les cas avancés (et souvent source de bugs !).
Pourquoi on adore te demander ça en entretien ?
Parce que le polymorphisme, c’est le couteau suisse du dev : si tu piges comment l’utiliser, tu sais construire des systèmes évolutifs et maintenables, ton code va pas juste marcher, il va durer ! Par exemple, on peut te demander d’implémenter la gestion de différents moyens de paiement (classique : BankCard, PayPal, Bitcoin) — et on attend que tu fasses une interface commune (ou une classe de base abstraite) avec une méthode Pay(), et ensuite les clients pourront appeler Pay(BankCard), Pay(PayPal), Pay(Bitcoin) — sans savoir ce qui se passe à l’intérieur.
8. Erreurs typiques des débutants
Une des erreurs les plus fréquentes — essayer d’appeler une méthode spécifique à la classe dérivée via une variable du type de base. Genre :
Animal animal = new Dog("Fido");
animal.Bark(); // Erreur ! Animal n’a pas de méthode Bark.
Pourquoi ça marche pas ? Parce qu’une variable de type Animal "voit" seulement ce qui est déclaré dans Animal, même si c’est en fait un Dog. Tu peux appeler seulement ce qui a été défini dans la classe de base — et overridé (override) dans les descendants.
Si tu veux vraiment appeler une méthode qui existe que sur Dog — il faut caster :
Animal animal = new Dog("Fido");
if (animal is Dog dog)
{
dog.Bark();
}
Mais souvent, si tu fais ça, c’est qu’il y a un souci d’architecture (ou que tu abuses de l’héritage).
GO TO FULL VERSION