Chargement...

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 :


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


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