Le polymorphisme
Notions théoriques
Le polymorphisme (du grec « plusieurs formes ») est la capacité d'un objet à se comporter différemment selon son type réel, même lorsqu'il est manipulé via un type parent ou une interface.
En C#, le polymorphisme s'exprime principalement de deux façons :
- Polymorphisme par héritage : méthodes
virtual/overridesur une hiérarchie de classes - Polymorphisme par interface : plusieurs classes implémentent le même contrat
Opérateurs is et as
L'opérateur is vérifie si un objet est d'un certain type (retourne bool).
L'opérateur as tente de convertir un objet vers un type ; retourne null si la conversion échoue (ne lève pas d'exception).
Animal animal = new Chien();
if (animal is Chien chien) // is avec pattern variable (C# 7+)
{
chien.Aboyer(); // on peut utiliser chien directement
}
// Équivalent "ancien style" :
Chien? autreFacon = animal as Chien;
if (autreFacon != null)
{
autreFacon.Aboyer();
}
Depuis C# 7, is supporte la déclaration de variable directement dans la condition : if (animal is Chien chien). C'est la forme recommandée.
Ne jamais utiliser un cast direct (Chien)animal sans vérification préalable : si la conversion est impossible, une InvalidCastException est levée.
GetType() et typeof()
GetType(): méthode d'instance, retourne le type réel de l'objet à l'exécutiontypeof(): opérateur, retourne le type d'une classe connue à la compilation
Animal animal = new Chien();
Console.WriteLine(animal.GetType()); // Chien
Console.WriteLine(typeof(Chien)); // Chien
Console.WriteLine(animal.GetType() == typeof(Chien)); // True
Console.WriteLine(animal.GetType() == typeof(Animal)); // False (type réel = Chien)
Pattern matching avec switch
Le switch expression (C# 8+) permet de discriminer par type de façon concise :
string Decrire(Animal animal) => animal switch
{
Chien c => $"{c.Nom} aboie",
Chat c => $"{c.Nom} miaule",
_ => "animal inconnu",
};
On peut combiner avec des conditions supplémentaires (when) :
string Classer(Animal animal) => animal switch
{
Chien c when c.Taille > 50 => "grand chien",
Chien c => "petit chien",
Chat c => "chat",
_ => "autre",
};
Le patron de conception Stratégie (Strategy Pattern)
Le Strategy Pattern exploite le polymorphisme pour rendre un algorithme interchangeable. On définit une interface commune et on passe la stratégie souhaitée à l'objet client.
// Interface = contrat commun
interface ICalculateurTaxe
{
decimal CalculerTaxe(decimal prixHT);
string Libelle { get; }
}
// Plusieurs stratégies concrètes
class TvaNormale : ICalculateurTaxe
{
public string Libelle => "TVA normale (20%)";
public decimal CalculerTaxe(decimal prixHT) => prixHT * 0.20m;
}
class TvaReduite : ICalculateurTaxe
{
public string Libelle => "TVA réduite (5,5%)";
public decimal CalculerTaxe(decimal prixHT) => prixHT * 0.055m;
}
class Exonere : ICalculateurTaxe
{
public string Libelle => "Exonéré (0%)";
public decimal CalculerTaxe(decimal prixHT) => 0m;
}
// Client : ne connaît que l'interface
class Produit
{
public string Nom { get; }
public decimal PrixHT { get; }
private readonly ICalculateurTaxe _taxe;
public Produit(string nom, decimal prixHT, ICalculateurTaxe taxe)
{
Nom = nom;
PrixHT = prixHT;
_taxe = taxe;
}
public decimal PrixTTC => PrixHT + _taxe.CalculerTaxe(PrixHT);
public override string ToString() =>
$"{Nom} — HT : {PrixHT:C} | {_taxe.Libelle} | TTC : {PrixTTC:C}";
}
L'intérêt : pour ajouter un nouveau taux de TVA, il suffit de créer une nouvelle classe implémentant ICalculateurTaxe — le code existant n'est pas modifié.
Exemple pratique
using System;
using System.Collections.Generic;
interface ICalculateurTaxe
{
decimal CalculerTaxe(decimal prixHT);
string Libelle { get; }
}
class TvaNormale : ICalculateurTaxe
{
public string Libelle => "TVA normale (20 %)";
public decimal CalculerTaxe(decimal prixHT) => prixHT * 0.20m;
}
class TvaReduite : ICalculateurTaxe
{
public string Libelle => "TVA réduite (5,5 %)";
public decimal CalculerTaxe(decimal prixHT) => prixHT * 0.055m;
}
class TvaSuper : ICalculateurTaxe
{
public string Libelle => "TVA super-réduite (2,1 %)";
public decimal CalculerTaxe(decimal prixHT) => prixHT * 0.021m;
}
class Exonere : ICalculateurTaxe
{
public string Libelle => "Exonéré (0 %)";
public decimal CalculerTaxe(decimal prixHT) => 0m;
}
class Produit
{
public string Nom { get; }
public decimal PrixHT { get; }
private readonly ICalculateurTaxe _taxe;
public Produit(string nom, decimal prixHT, ICalculateurTaxe taxe)
{
Nom = nom;
PrixHT = prixHT;
_taxe = taxe;
}
public decimal PrixTTC => PrixHT + _taxe.CalculerTaxe(PrixHT);
public string AfficherDetail() =>
$"{Nom,-20} HT : {PrixHT,8:C} {_taxe.Libelle,-25} TTC : {PrixTTC,8:C}";
}
// Programme principal
List<Produit> catalogue = new()
{
new Produit("Ordinateur", 800.00m, new TvaNormale()),
new Produit("Livre scolaire", 15.00m, new TvaReduite()),
new Produit("Médicament", 12.50m, new TvaSuper()),
new Produit("Don associatif", 50.00m, new Exonere()),
};
Console.WriteLine("=== Catalogue de produits ===\n");
foreach (Produit p in catalogue)
{
Console.WriteLine(p.AfficherDetail());
}
decimal totalHT = catalogue.Sum(p => p.PrixHT);
decimal totalTTC = catalogue.Sum(p => p.PrixTTC);
Console.WriteLine($"\nTotal HT : {totalHT:C} Total TTC : {totalTTC:C}");
catalogue.Sum(p => p.PrixTTC) utilise LINQ pour sommer une propriété sur toute la liste. Sum fait partie de System.Linq, importé automatiquement avec ImplicitUsings.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Vous allez construire un calculateur de taxes utilisant le polymorphisme par interface. Le programme affiche le détail TVA de chaque produit d'un catalogue, puis le total.
Voici la structure de départ :
using System;
using System.Collections.Generic;
interface ICalculateurTaxe
{
decimal CalculerTaxe(decimal prixHT);
string Libelle { get; }
}
class Produit
{
public string Nom { get; }
public decimal PrixHT { get; }
private readonly ICalculateurTaxe _taxe;
public Produit(string nom, decimal prixHT, ICalculateurTaxe taxe)
{
Nom = nom;
PrixHT = prixHT;
_taxe = taxe;
}
public decimal PrixTTC => PrixHT + _taxe.CalculerTaxe(PrixHT);
public string AfficherDetail() =>
$"{Nom,-20} HT : {PrixHT,8:C} {_taxe.Libelle,-25} TTC : {PrixTTC,8:C}";
}
Étape 1 — Implémenter TvaNormale
Créez la classe TvaNormale qui implémente ICalculateurTaxe. Le taux est de 20 %.
m pour les littéraux décimauxEn C#, les littéraux à virgule sont double par défaut. Pour un decimal, ajoutez toujours le suffixe m (ex : 0.20m, 15.50m). Omettre ce suffixe provoque une erreur de compilation lorsque le type attendu est decimal.
Étape 2 — Implémenter TvaReduite et Exonere
Créez TvaReduite (5,5 %) et Exonere (0 %, CalculerTaxe retourne 0m).
En C#, le séparateur décimal est toujours le point (.) dans le code source, quelle que soit la langue du système. La virgule sert de séparateur dans les listes de paramètres. Ne confondez pas 0,055 (erreur de syntaxe) et 0.055m (correct).
Étape 3 — Créer le catalogue et afficher les produits
Créez une List<Produit> avec au moins 3 produits utilisant des taux différents, puis affichez chaque ligne.
En déclarant List<Produit> (et non List<TvaNormale>), chaque produit peut utiliser n'importe quelle stratégie de taxe. C'est le cœur du polymorphisme : le code client travaille avec le contrat (ICalculateurTaxe), pas avec l'implémentation concrète.
Étape 4 — Ajouter le total HT et TTC
Calculez et affichez le total HT et le total TTC de tous les produits.
Sum() plutôt qu'une boucle manuelleLINQ fournit Sum(), Average(), Min(), Max(), Count() pour éviter les boucles d'accumulation manuelles. Le code est plus lisible et moins sujet aux erreurs d'initialisation de l'accumulateur.