Interfaces et Classes Abstraites en C#
Découvrez les interfaces et classes abstraites en C#, leurs différences et quand les utiliser pour concevoir un code maintenable.
En C#, les classes abstraites et les interfaces sont utilisées pour construire des architectures orientées objet flexibles et extensibles.
Toutes deux aident à définir des comportements communs, mais il existe des différences importantes.
Une abstract class peut fournir une implémentation partielle (corps de méthodes), tandis qu’une interface ne déclare que
des signatures et laisse l’implémentation entièrement aux classes dérivées.
Classe Abstraite
Une classe abstraite est définie avec le mot-clé abstract et ne peut pas être instanciée directement.
Elle définit des propriétés et des méthodes communes, mais certaines méthodes peuvent être marquées comme abstract afin d’obliger les sous-classes à les implémenter.
Les classes abstraites peuvent également inclure des méthodes normales — c’est-à-dire un comportement partiellement implémenté.
public abstract class Shape
{
public string Color { get; set; } = "Noir";
// Doit être implémentée par les sous-classes
public abstract double GetArea();
// Une méthode commune
public void PrintColor()
{
Console.WriteLine($"Couleur : {Color}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
Ici, la classe abstraite Shape fournit une propriété commune Color,
mais laisse l’implémentation de GetArea() aux sous-classes.
La classe Circle l’implémente à sa manière.
Interface
Une interface est définie avec le mot-clé interface.
Elle contient uniquement des signatures de méthodes et de propriétés, mais pas de corps (avant C# 8).
Une classe peut implémenter plusieurs interfaces en même temps (permettant une forme d’héritage multiple).
Cela établit un « contrat commun » entre différentes classes.
public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Enregistré dans un fichier : {message}");
}
}
public class DbLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Enregistré dans la base de données : {message}");
}
}
Les classes qui implémentent ILogger effectuent la journalisation de différentes manières,
mais elles doivent toutes exposer la même signature de méthode : Log(string).
Classe Abstraite vs Interface
Différences clés :
- Classe Abstraite : Peut définir un comportement commun et obligatoire. Peut contenir des propriétés et des corps de méthodes.
- Interface : Déclare uniquement des signatures. Supporte les implémentations multiples.
- Classe Abstraite : Une classe ne peut hériter que d’une seule classe abstraite (classe de base).
- Interface : Une classe peut implémenter plusieurs interfaces.
- Classe Abstraite : Peut contenir un état partagé, des champs et un constructeur.
- Interface : Ne contient ni champs ni constructeur.
Scénario réel : Ligne de production (Interface + Classe Abstraite)
Dans l’exemple ci-dessous, nous définissons une interface IProduct pour modéliser un flux de fabrication de produits et une
abstract class (ProductBase) qui l’implémente partiellement.
IProduct déclare le contrat commun (propriétés + signatures de méthodes), tandis que ProductBase
fournit le flux partagé (méthode modèle : Production()) et une implémentation partielle.
MetalProduct, WoodProduct et PlasticProduct personnalisent cette structure.
Nous utilisons un enum (SizeOption) pour des valeurs de taille cohérentes comme « S/M/L »,
ainsi qu’une interface simple de contrôle qualité IQualityCheck que les produits implémentent.
using System;
using System.Collections.Generic;
// Enum pour standardiser les tailles
public enum SizeOption
{
Small,
Medium,
Large
}
// Contrat commun : propriétés et comportements que tous les produits doivent avoir
public interface IProduct
{
string Name { get; set; }
string SKU { get; set; }
SizeOption Size { get; set; }
decimal UnitCost { get; set; } // Coût unitaire
int Stock { get; set; } // Stock disponible
// Flux de production et calcul des coûts
void Production();
decimal CalculateCost(int quantity);
string GetSpecs();
}
// Contrat simple pour le contrôle qualité
public interface IQualityCheck
{
bool QualityCheck(out string reason);
}
// CLASSE ABSTRAITE : Implémentation partielle + flux partagé (Pattern Méthode Modèle)
public abstract class ProductBase : IProduct, IQualityCheck
{
public string Name { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public SizeOption Size { get; set; }
public decimal UnitCost { get; set; }
public int Stock { get; set; }
// Facteur basé sur le matériau (les sous-classes peuvent le surcharger)
protected virtual decimal MaterialFactor => 1.00m;
// Méthode modèle : exécute les étapes de production dans un ordre fixe
public void Production()
{
LogStep($"Production démarrée : {Name} ({SKU})");
PrepareMaterials();
Assemble();
Finish();
LogStep("Production terminée.\n");
}
// Calcul de coût de base (les sous-classes peuvent le surcharger si nécessaire)
public virtual decimal CalculateCost(int quantity)
{
if (quantity <= 0) return 0m;
// Modèle simple : coût unitaire * quantité * facteur matériau * frais généraux (10 %)
decimal baseCost = UnitCost * quantity * MaterialFactor;
decimal overhead = baseCost * 0.10m;
return baseCost + overhead;
}
// Spécification technique commune
public virtual string GetSpecs()
=> $"SKU: {SKU}, Taille: {Size}, Coût unitaire: {UnitCost:0.00} EUR, Stock: {Stock}";
// Contrôle qualité par défaut : coût et stock sont-ils plausibles ?
public virtual bool QualityCheck(out string reason)
{
if (UnitCost <= 0) { reason = "Le coût unitaire ne peut pas être nul/négatif."; return false; }
if (Stock < 0) { reason = "Le stock ne peut pas être négatif."; return false; }
reason = "OK";
return true;
}
// Étapes que les sous-classes doivent implémenter
protected abstract void PrepareMaterials();
protected abstract void Assemble();
protected virtual void Finish()
{
LogStep("Contrôle qualité général et emballage");
}
protected void LogStep(string message) => Console.WriteLine($" - {message}");
}
// PRODUIT MÉTALLIQUE
public class MetalProduct : ProductBase
{
public string Alloy { get; set; } = "Acier 304L";
public bool RequiresHeatTreatment { get; set; } = true;
protected override decimal MaterialFactor => 1.45m; // traitement du métal plus coûteux
protected override void PrepareMaterials()
{
LogStep($"Préparation de la tôle/bille métallique (Alliage: {Alloy})");
if (RequiresHeatTreatment) LogStep("Préparation du four pour le traitement thermique");
}
protected override void Assemble()
{
LogStep("Découpe, pliage, soudage et meulage de surface");
if (RequiresHeatTreatment) LogStep("Application d’un traitement thermique (trempe)");
}
protected override void Finish()
{
LogStep("Protection anticorrosion et revêtement de surface");
base.Finish();
}
public override string GetSpecs()
=> $"[Métal] {Name} | {base.GetSpecs()} | Alliage: {Alloy} | Traitement thermique: {(RequiresHeatTreatment ? "Oui" : "Non")}";
public override bool QualityCheck(out string reason)
{
if (!base.QualityCheck(out reason)) return false;
// Règle supplémentaire pour le métal : le nom de l’alliage ne doit pas être vide
if (string.IsNullOrWhiteSpace(Alloy)) { reason = "Les informations sur l’alliage ne peuvent pas être vides."; return false; }
return true;
}
}
// PRODUIT EN BOIS
public class WoodProduct : ProductBase
{
public string WoodType { get; set; } = "Chêne";
public bool IsVarnished { get; set; } = true;
protected override decimal MaterialFactor => 1.20m;
protected override void PrepareMaterials()
{
LogStep($"Sélection et séchage du bois (Type: {WoodType})");
LogStep("Découpe et ponçage");
}
protected override void Assemble()
{
LogStep("Assemblage : tenon/cheville et colle");
}
protected override void Finish()
{
LogStep(IsVarnished ? "Application de vernis" : "Application d’huile/protection");
base.Finish();
}
public override string GetSpecs()
=> $"[Bois] {Name} | {base.GetSpecs()} | Type de bois: {WoodType} | Vernis: {(IsVarnished ? "Oui" : "Non")}";
}
// PRODUIT EN PLASTIQUE
public class PlasticProduct : ProductBase
{
public string Polymer { get; set; } = "PP";
public bool Recyclable { get; set; } = true;
protected override decimal MaterialFactor => 0.95m; // plastiques généralement moins coûteux
protected override void PrepareMaterials()
{
LogStep($"Préparation de granulés (Polymère: {Polymer})");
LogStep("Mélange avec masterbatch colorant");
}
protected override void Assemble()
{
LogStep("Injection plastique et ébarbage");
}
protected override void Finish()
{
LogStep("Contrôle de surface et étiquetage");
base.Finish();
}
public override string GetSpecs()
=> $"[Plastique] {Name} | {base.GetSpecs()} | Polymère: {Polymer} | Recyclable: {(Recyclable ? "Oui" : "Non")}";
}
// DÉMONSTRATION
class Program
{
static void Main()
{
var items = new List<IProduct>
{
new MetalProduct
{
Name = "Étagère en acier",
SKU = "M-SH-001",
Size = SizeOption.Large,
UnitCost = 500m,
Stock = 40,
Alloy = "304L",
RequiresHeatTreatment = true
},
new WoodProduct
{
Name = "Table en bois",
SKU = "W-TB-002",
Size = SizeOption.Medium,
UnitCost = 350m,
Stock = 12,
WoodType = "Chêne",
IsVarnished = true
},
new PlasticProduct
{
Name = "Caisse en plastique",
SKU = "P-CR-003",
Size = SizeOption.Small,
UnitCost = 80m,
Stock = 200,
Polymer = "PP",
Recyclable = true
}
};
const int qty = 10;
foreach (var p in items)
{
Console.WriteLine($"\n=== {p.Name} ===");
Console.WriteLine(p.GetSpecs());
p.Production();
Console.WriteLine($"Coût total ({qty} unités): {p.CalculateCost(qty):0.00} EUR");
if (p is IQualityCheck qc)
{
Console.WriteLine(qc.QualityCheck(out var reason)
? "Contrôle qualité : OK"
: $"Contrôle qualité : ÉCHEC - {reason}");
}
}
}
}
Résumé : L’interface IProduct fournit un contrat commun pour différents types de produits.
La classe abstraite ProductBase implémente partiellement ce contrat et standardise le flux de production (méthode modèle).
Les sous-classes (Métal/Bois/Plastique) personnalisent les étapes spécifiques aux matériaux et les hypothèses de coût.
En combinant interface + classe abstraite, on obtient à la fois flexibilité et réutilisabilité.
Avantages
- Interface : Flexibilité grâce aux implémentations multiples.
- Classe Abstraite : Réduit la duplication de code grâce à l’état partagé et à l’implémentation partielle.
- Les deux : Réduisent le couplage et augmentent la testabilité.
TL;DR
abstract class: Définit un comportement commun + obligatoire ; peut inclure des corps de méthodes.interface: Déclare uniquement des signatures ; prend en charge des implémentations multiples.- Une classe ne peut hériter que d’une seule classe abstraite/de base mais peut implémenter plusieurs interfaces.
Articles connexes
Classes Sealed, Static et Partial en C#
Apprenez l’objectif, les différences et les cas d’utilisation des classes sealed, static et partial en C#.
Classes, Objets, Propriétés et Méthodes en C#
Découvrez comment les classes, objets, propriétés et méthodes en C# constituent les fondements de la programmation orientée objet.
Délégués et Événements en C#
Apprenez les délégués et événements en C# pour comprendre la programmation événementielle avec des exemples concrets.
Encapsulation, Héritage et Polymorphisme en C#
Apprenez l’encapsulation, l’héritage et le polymorphisme en C# avec des exemples pour maîtriser les bases de la POO.
Espaces de noms et assemblies en C#
Apprenez les concepts de namespaces et d’assemblies en C# pour structurer le code et gérer efficacement les dépendances.
Méthodes d’extension en C#
Apprenez les méthodes d’extension en C# pour ajouter des fonctionnalités aux types existants sans modifier leur code.
Principes de l’Injection de Dépendances en C#
Apprenez les principes de l’Injection de Dépendances en C#, la gestion des dépendances et le couplage faible avec exemples.
Principes SOLID en C#
Application des principes SOLID en C# avec des exemples : pour un code flexible, maintenable et testable.