4) Polymorphisme
Objectifs de la séance
- Déclarer et implémenter une interface
ISoignable - Comprendre la convention de nommage des interfaces en C# (préfixe
I) - Utiliser le pattern matching
if (p is Guerrier g)etswitch (p) - Manipuler une
List<Personnage>hétérogène de façon polymorphique
Notions théoriques
Interfaces en C#
Une interface est un contrat pur : elle ne contient que des déclarations de membres (pas d'état, sauf constantes). Une classe peut implémenter plusieurs interfaces.
interface ISoignable
{
int Mana { get; }
void Soigner(Personnage cible, int montant);
}
La convention C# est de préfixer les interfaces par I (majuscule). Cela permet de distinguer immédiatement une interface d'une classe abstraite dans le code. Cette convention est universellement respectée dans l'écosystème .NET.
class Mage : Personnage, ISoignable
{
public int Mana { get; private set; }
public Mage(string nom, int pv, int force, int mana)
: base(nom, pv, force) => Mana = mana;
public override void Attaquer(Personnage cible)
=> Console.WriteLine($"{Nom} lance un sort sur {cible.Nom} pour {Force * 2} dégâts !");
public void Soigner(Personnage cible, int montant)
{
// On suppose que PointsDeVie a un setter protected
Console.WriteLine($"{Nom} soigne {cible.Nom} de {montant} PV.");
}
}
Pattern matching
Le pattern matching permet de tester et de caster un objet en une seule expression.
// Syntaxe is-pattern (C# 7+)
if (p is Guerrier g)
{
Console.WriteLine($"Armure du guerrier : {g.Armure}");
}
// switch avec patterns typés (C# 8+)
switch (p)
{
case Guerrier guerrier:
Console.WriteLine($"{guerrier.Nom} est un Guerrier, armure : {guerrier.Armure}");
break;
case Mage mage:
Console.WriteLine($"{mage.Nom} est un Mage, mana : {mage.Mana}");
break;
case Archer archer:
Console.WriteLine($"{archer.Nom} est un Archer, portée : {archer.Portee}");
break;
default:
Console.WriteLine($"{p.Nom} est un type inconnu");
break;
}
Le pattern matching est puissant mais ne remplace pas le polymorphisme. Si vous vous retrouvez à faire un switch sur le type dans de nombreux endroits, c'est souvent le signe qu'il faut ajouter une méthode abstraite à la classe parent.
List<Personnage> hétérogène
Grâce au polymorphisme, on peut mettre des objets de types différents dans une même liste, à condition qu'ils partagent un type commun.
List<Personnage> equipe = new()
{
new Guerrier("Kael", 100, 15, 8),
new Mage("Aria", 80, 10, 50),
new Archer("Theron", 90, 12, 30),
};
foreach (Personnage p in equipe)
{
Console.WriteLine(p.Decrire()); // appelle la bonne version de Decrire()
p.Attaquer(equipe[0]); // appelle la bonne version de Attaquer()
}
Exemple pratique
using System;
using System.Collections.Generic;
interface ISoignable
{
int Mana { get; }
void Soigner(Personnage cible, int montant);
}
abstract class Personnage
{
public string Nom { get; protected set; }
public int PointsDeVie { get; protected set; }
public int Force { get; protected set; }
protected Personnage(string nom, int pv, int force)
{ Nom = nom; PointsDeVie = pv; Force = force; }
public abstract void Attaquer(Personnage cible);
public void RecevoirSoins(int montant)
{
PointsDeVie += montant;
Console.WriteLine($"{Nom} récupère {montant} PV (total : {PointsDeVie})");
}
public virtual string Decrire()
=> $"[{GetType().Name}] {Nom} — PV : {PointsDeVie} Force : {Force}";
}
class Guerrier : Personnage
{
public int Armure { get; private set; }
public Guerrier(string nom, int pv, int force, int armure)
: base(nom, pv, force) => Armure = armure;
public override void Attaquer(Personnage cible)
=> Console.WriteLine($"{Nom} frappe {cible.Nom} pour {Force} dégâts !");
public override string Decrire() => base.Decrire() + $" Armure : {Armure}";
}
class Mage : Personnage, ISoignable
{
public int Mana { get; private set; }
public Mage(string nom, int pv, int force, int mana)
: base(nom, pv, force) => Mana = mana;
public override void Attaquer(Personnage cible)
=> Console.WriteLine($"{Nom} lance un sort sur {cible.Nom} pour {Force * 2} dégâts !");
public void Soigner(Personnage cible, int montant)
{
Console.Write($"{Nom} soigne {cible.Nom}. ");
cible.RecevoirSoins(montant);
}
public override string Decrire() => base.Decrire() + $" Mana : {Mana}";
}
class Archer : Personnage
{
public int Portee { get; private set; }
public Archer(string nom, int pv, int force, int portee)
: base(nom, pv, force) => Portee = portee;
public override void Attaquer(Personnage cible)
=> Console.WriteLine($"{Nom} tire une flèche sur {cible.Nom} à {Portee}m pour {Force} dégâts !");
}
// Programme principal
List<Personnage> equipe = new()
{
new Guerrier("Kael", 100, 15, 8),
new Mage ("Aria", 80, 10, 50),
new Archer ("Theron", 90, 12, 30),
};
Console.WriteLine("=== Équipe ===");
foreach (Personnage p in equipe)
{
Console.WriteLine(p.Decrire());
}
Console.WriteLine("\n=== Identification par pattern matching ===");
foreach (Personnage p in equipe)
{
switch (p)
{
case Guerrier g:
Console.WriteLine($"{g.Nom} est un Guerrier (armure : {g.Armure})");
break;
case Mage m:
Console.WriteLine($"{m.Nom} est un Mage (mana : {m.Mana})");
m.Soigner(equipe[0], 20); // le Mage soigne le Guerrier
break;
case Archer a:
Console.WriteLine($"{a.Nom} est un Archer (portée : {a.Portee}m)");
break;
}
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Étape 1 — Déclarer l'interface ISoignable
Déclarez l'interface avec une propriété Mana (lecture) et une méthode Soigner.
Le principe I de SOLID (Interface Segregation) recommande de créer des interfaces petites et spécifiques. ISoignable ne contient que ce qui concerne le soin. Si demain vous ajoutez la capacité de se téléporter, créez ITeleportable plutôt que d'alourdir ISoignable.
Étape 2 — Implémenter ISoignable dans Mage
Modifiez Mage pour qu'il implémente ISoignable en plus d'hériter de Personnage.
Vous pouvez stocker un Mage dans une variable ISoignable : ISoignable soigneur = new Mage(...). Cela force le code consommateur à ne voir que les méthodes de ISoignable, ce qui réduit le couplage. Si demain un Pretre implémente aussi ISoignable, rien ne change dans le code consommateur.
Étape 3 — Boucle avec pattern matching
Parcourez la liste des personnages et, pour chaque Mage, faites-lui soigner le premier Guerrier de la liste.
Avant C# 7, on écrivait : if (p is Mage) { Mage mage = (Mage)p; ... }. Le pattern matching if (p is Mage mage) fusionne le test et le cast, éliminant le risque d'InvalidCastException. Toujours préférer cette syntaxe moderne.
📌 Une solution
Ce qu'il faut retenir
| Notion | Résumé |
|---|---|
| Interface | Contrat pur (interface INom). Préfixe I obligatoire par convention. |
| Implémenter | class Mage : Personnage, ISoignable — parent d'abord, interfaces ensuite. |
Pattern matching is | if (p is Guerrier g) — teste et caste en une opération. |
switch sur le type | case Guerrier g: — trier les comportements selon le type réel. |
| Liste hétérogène | List<Personnage> peut contenir Guerriers, Mages, Archers grâce au polymorphisme. |
Aperçu de la prochaine séance
Dans la prochaine séance, vous structurerez le combat en deux équipes. Vous utiliserez un enum TypeAttaque, une boucle while de combat, et LINQ pour trier les personnages par force. Vous découvrirez aussi le type record pour encapsuler le résultat d'un combat de façon immuable.