1. Introduction
Imagine un monde sans hiérarchies : des milliers de classes Person, Animal, Vehicle et tout ça complètement séparé. Pas étonnant que les devs dans ce monde ne survivraient même pas jusqu'au déjeuner — ils seraient complètement perdus ! Dans les vrais projets, on a souvent besoin d'objets qui savent faire des trucs en commun (genre, tous les animaux peuvent bouger), mais chacun a ses particularités (le poisson nage, l'oiseau vole).
C'est justement les hiérarchies de classes qui permettent d'exprimer les liens entre les entités, pour que le code devienne créatif et pas juste une bataille sans fin contre le copier-coller.
Regarde un exemple. Disons qu'on a une classe de base Animal. Tous les animaux peuvent faire un bruit. Mais seuls les chats miaulent, les chiens aboient, et les perroquets peuvent même raconter deux-trois blagues. On veut exprimer ça dans le code de façon hiérarchique.
Classe de base
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Méthode de base : peut être overridée dans les enfants
public virtual void Speak()
{
Console.WriteLine("L'animal fait un bruit quelconque...");
}
}
Ici, on a ajouté le mot virtual à la méthode Speak(). C'est comme un tag : "Hey, les classes enfants, si vous voulez — vous pouvez overrider cette méthode".
On crée la hiérarchie : classes dérivées
Maintenant, on va faire une classe Cat qui hérite de Animal :
public class Cat : Animal
{
public Cat(string name) : base(name) { }
// On override Speak — les chats ne peuvent pas juste grogner !
public override void Speak()
{
Console.WriteLine($"{Name} dit : Miaou !");
}
}
Et la classe Dog :
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} dit : Ouaf !");
}
}
Et si on a un animal normal qui ne sait pas parler ? Alors on peut utiliser la classe de base sans rien overrider.
Visualisation — arbre de hiérarchie
. Animal
/ \
Cat Dog
- Animal — classe de base
- Cat, Dog — classes enfants (dérivées)
2. Écrivons du code qui utilise cette hiérarchie
On continue à bosser sur notre appli console.
Disons qu'on a une collection d'animaux et on veut que chacun dise un truc qui lui est propre :
Animal[] zoo = new Animal[]
{
new Cat("Barsik"),
new Dog("Rex"),
new Animal("Créature mystérieuse")
};
foreach (Animal animal in zoo)
{
animal.Speak();
}
Sortie attendue :
Barsik dit : Miaou !
Rex dit : Ouaf !
L'animal fait un bruit quelconque...
Voilà, grâce à la hiérarchie et au polymorphisme (on va voir ça en détail bientôt, mais en gros — c'est la bonne version de la méthode qui est appelée selon le vrai type de l'objet), ton appli devient flexible et facile à faire évoluer.
3. On ajoute de nouvelles méthodes et champs
Ok, on a géré les "voix". Mais tous les animaux, c'est un peu boring. Par exemple, un chat peut avoir neuf vies, et un chien sait rapporter un bâton.
On ajoute un comportement unique
Dans une classe enfant, tu peux ajouter tes propres méthodes et champs :
public class Cat : Animal
{
public int Lives { get; private set; } = 9;
public Cat(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} dit : Miaou ! J'ai {Lives} vies.");
}
public void LoseLife()
{
if (Lives > 0)
{
Lives--;
Console.WriteLine($"{Name} a perdu une vie. Il en reste : {Lives}");
}
else
{
Console.WriteLine($"{Name} a déjà utilisé toutes ses vies !");
}
}
}
On l'utilise dans le code :
var barsik = new Cat("Barsik");
barsik.Speak(); // Barsik dit : Miaou ! J'ai 9 vies.
barsik.LoseLife(); // Barsik a perdu une vie. Il en reste : 8
On ajoute de nouvelles classes : on agrandit le "zoo"
Tu sais déjà créer des classes dérivées. Ajoutons, par exemple, un perroquet :
public class Parrot : Animal
{
public Parrot(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} dit : Salut, humain !");
}
public void Repeat(string phrase)
{
Console.WriteLine($"{Name} répète : {phrase}");
}
}
Maintenant tu peux facilement étendre le système sans toucher au vieux code :
var keshka = new Parrot("Kesha");
keshka.Speak(); // Kesha dit : Salut, humain !
keshka.Repeat("Apprends, étudiant !"); // Kesha répète : Apprends, étudiant !
4. Comparaison du comportement des animaux
| Type | Méthode Speak() | Champ propre | Comportement additionnel |
|---|---|---|---|
| Animal | Oui (virtual) | Name | — |
| Cat | Oui (override) | Lives | LoseLife() |
| Dog | Oui (override) | — | — |
| Parrot | Oui (override) | — | Repeat(string) |
À quoi ressemble la hiérarchie des classes en mémoire (schéma bloc)
Animal (Name)
├── Cat (Lives)
├── Dog
└── Parrot (Repeat)
5. Pratique dans notre appli
Relions l'idée de hiérarchie à l'appli — par exemple, on a des tâches de différents types :
- Task (classe de base) : N'importe quelle tâche — elle a un titre et un statut d'exécution.
- WorkTask (de boulot) : En plus, elle a une deadline.
- HomeTask (à la maison) : Peut avoir une priorité ("Très important", "Bof").
On commence par la classe de base :
public class Task
{
public string Title { get; set; }
public bool IsCompleted { get; private set; }
public Task(string title)
{
Title = title;
}
public virtual void Complete()
{
IsCompleted = true;
Console.WriteLine($"Tâche \"{Title}\" terminée !");
}
}
Maintenant on ajoute une tâche de boulot :
public class WorkTask : Task
{
public DateTime Deadline { get; set; }
public WorkTask(string title, DateTime deadline)
: base(title)
{
Deadline = deadline;
}
public override void Complete()
{
base.Complete();
Console.WriteLine($"Date limite : {Deadline:d}");
}
}
Et une tâche à la maison :
public class HomeTask : Task
{
public string Priority { get; set; }
public HomeTask(string title, string priority)
: base(title)
{
Priority = priority;
}
// Pas besoin d'override Complete si le comportement de la classe de base suffit
}
On crée une liste de tâches dans le programme :
List<Task> tasks = new List<Task>
{
new WorkTask("Envoyer le rapport", DateTime.Today.AddDays(2)),
new HomeTask("Faire la vaisselle", "Très important"),
new Task("Lire la leçon sur l'héritage")
};
foreach (Task task in tasks)
{
Console.WriteLine($"Tâche : {task.Title}");
task.Complete();
}
Sortie attendue :
Tâche : Envoyer le rapport
Tâche "Envoyer le rapport" terminée !
Date limite : 13.07.2025
Tâche : Faire la vaisselle
Tâche "Faire la vaisselle" terminée !
Tâche : Lire la leçon sur l'héritage
Tâche "Lire la leçon sur l'héritage" terminée !
Tu vois comme c'est pratique : toutes les tâches sont ensemble, on les traite pareil, et les particularités ressortent là où il faut.
6. Erreurs typiques avec l'héritage
Erreur n°1 : essayer d'override une méthode qui n'est pas déclarée virtual.
Si une méthode dans la classe de base n'est pas marquée virtual, tu ne peux pas l'override dans les classes dérivées. Du coup, tout le côté flexible du polymorphisme disparaît, et la hiérarchie ne sert plus à rien.
Erreur n°2 : héritage sans lien logique entre les entités.
Faut pas utiliser l'héritage si les objets n'ont pas de vrai rapport. Par exemple, Cercle est bien une Figure, mais Cheval comme Véhicule — c'est tiré par les cheveux. Sauf contexte spécial (genre un jeu médiéval) où ça peut se justifier.
Erreur n°3 : hiérarchies trop profondes.
Quand la structure des classes descend trop loin (5–6 niveaux ou plus), le code devient dur à lire, à maintenir et à tester. C'est un signe qu'il vaut mieux penser à la composition au lieu de l'héritage.
Erreur n°4 : oublier d'appeler le constructeur de base.
Quand tu ajoutes de nouvelles propriétés dans une classe dérivée, c'est facile d'oublier d'appeler explicitement base(...) dans le constructeur. Ça peut causer une initialisation incomplète ou foireuse de la partie de base de l'objet, et des bugs bien galère à trouver.
GO TO FULL VERSION