Aller au contenu principal

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/override sur 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();
}
info

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.

attention

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écution
  • typeof() : 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}");
info

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


Que retourne l'opérateur `as` si la conversion échoue ?


Quelle est la différence entre `GetType()` et `typeof()` ?


Qu'est-ce que le Strategy Pattern ?


Avec le pattern matching `switch`, que signifie `_` ?


Pourquoi le Strategy Pattern facilite-t-il l'ajout d'un nouveau comportement ?


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 %.


Bonne pratique - Suffixe m pour les littéraux décimaux

En 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).


Bonne pratique - Virgule décimale vs séparateur de milliers

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.


Bonne pratique - Polymorphisme par liste d'interfaces

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.


Bonne pratique - Sum() plutôt qu'une boucle manuelle

LINQ 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.

📌 Une solution