CodeGym /Cours /C# SELF /Comparaison des interfaces et des classes abstraites

Comparaison des interfaces et des classes abstraites

C# SELF
Niveau 24 , Leçon 2
Disponible

1. Les différences classiques en bref

Si quelqu’un te sort : « Une interface, c’est juste un ensemble de signatures », demande-lui : « Tu codes sur quelle version de C# ? » Depuis C# 8 et plus, les interfaces sont devenues bien plus puissantes. Il est temps de les comparer aux classes abstraites — pas seulement sur les propriétés classiques, mais aussi avec toutes les nouveautés de la plateforme .NET.

Si on remonte quelques années en arrière — à C# 7 — c’était simple. Une classe abstraite peut définir des champs et des méthodes partiellement implémentées, une interface — juste des signatures (méthodes, propriétés, événements, indexeurs).
L’héritage d’une classe abstraite, c’est une relation "est-un" (Is-a), les interfaces permettent l’héritage multiple de comportements ("peut-faire", can-do).

Caractéristique Classe abstraite Interface (jusqu’à C# 8)
Relation is-a can-do
Héritage Un seul Multiple
Champs Peut en avoir Ne peut pas
Implémentation de méthodes Peut Ne peut pas
Constructeurs Peut Ne peut pas
Modificateurs d’accès Différents (public, protected, ...) Seulement implicitement public

Comme tu vois, avant, les classes abstraites étaient vraiment les "grands frères" des interfaces — plus puissantes et plus flexibles. Mais tout change !

2. Interfaces avec implémentation par défaut

Avec C# 8 (et encore plus avec C# 14 et .NET 9), les interfaces ont chopé un super-pouvoir — des méthodes avec une implémentation par défaut, qu’on appelle "Default Interface Methods" (DIM).

À quoi ça ressemble ?


public interface IAnimal
{
    void DireBonjour();

    // Méthode avec implémentation par défaut !
    void Marcher()
    {
        Console.WriteLine("Je marche...");
    }
}

Wow ! D’un coup, une interface peut contenir l’implémentation d’une méthode. Et pas qu’une seule, autant que tu veux. Mais attention : ces méthodes doivent obligatoirement être déclarées avec un corps, et tout le reste (champs, méthodes privées, constructeurs) — toujours pas possible.

3. Les possibilités modernes des interfaces

Les nouveautés à connaître pour tout dev .NET moderne :

  • Méthodes avec implémentation par défaut.
  • Méthodes privées dans l’interface (juste pour aider les autres méthodes de la même interface).
  • Méthodes statiques (depuis C# 8).
  • Propriétés avec implémentation par défaut.
  • Champs statiques (depuis C# 14 — "static interface members").
  • Membres statiques abstraits ("abstract static members" — ouais, maintenant une interface peut exiger certaines méthodes statiques dans l’implémentation !).

Exemple d’interface moderne complète :


public interface ILogger
{
    static int CompteurLogger { get; set; } // C# 14

    void Log(string message); // Signature (contrat)

    // Implémentation par défaut
    void LogAvertissement(string avertissement)
    {
        Log("[AVERTISSEMENT]: " + avertissement);
    }
    
    // Méthode privée d’aide dans l’interface (C# 8+)
    private void FormatterEtLog(string niveau, string msg)
    {
        Log($"{niveau}: {msg}");
    }

    // Méthode statique dans l’interface (C# 8+)
    static void AfficherInfoLogger()
    {
        Console.WriteLine("L’interface ILogger — ton meilleur pote !");
    }
}

Imagine — avant, c’était juste impossible, c’est comme si on laissait un chat faire la sécu du serveur.

4. Classes abstraites : quoi de neuf ?

Les classes abstraites... comment dire... elles n’ont pas trop évolué ces dix dernières années. Elles peuvent toujours contenir :

  • Des champs (y compris privés, protégés et statiques).
  • Des méthodes implémentées et abstraites.
  • Des constructeurs (oui, tu peux créer des classes abstraites avec de la logique d’initialisation).
  • Des propriétés, événements, indexeurs.
  • Des membres statiques et d’instance.

Exemple de classe abstraite :


public abstract class Animal
{
    public string Nom { get; set; }

    public abstract void Parler();

    public virtual void Marcher()
    {
        Console.WriteLine($"{Nom} marche sur ses pattes !");
    }

    protected void Manger()
    {
        Console.WriteLine($"{Nom} mange de la nourriture.");
    }
}

La classe abstraite reste toujours un super endroit pour stocker la logique commune, l’état et le comportement pour une hiérarchie de classes.

5. Comparatif moderne : tableau avec les nouvelles possibilités

Caractéristique Classe abstraite Interface (C# 14+, .NET 9)
Relation is-a (est-un) can-do (peut-faire)
Héritage Un seul Multiple
Champs Oui, tous Seulement statiques* (C# 14+)
Constructeurs Oui Non
Implémentation de méthodes Oui (virtual/abstract) Oui (default, static, abstract static)
Propriétés avec implémentation Oui Oui (implémentation par défaut)
Membres privés Oui Oui (seulement méthodes, C# 8+)
Membres statiques Oui Oui (C# 8+, avec limites)
Champs statiques Oui Oui * (C# 14+)
Modificateurs d’accès Tous Par défaut public ou private

* — Dans les interfaces, les champs statiques sont utilisés dans des cas particuliers, et c’est une nouveauté toute fraîche du langage.

6. Quand utiliser une interface ou une classe abstraite : conseils modernes

Les interfaces (et maintenant avec implémentation par défaut) — c’est l’outil pour créer des contrats entre composants. Leur point fort : la multiplicité. Ta classe peut implémenter dix interfaces différentes si tu veux, c’est le couteau suisse du dev.

La classe abstraite reste ton choix si :

  • Tu as besoin d’un état commun (champs), d’une logique et d’un comportement que d’autres classes vont hériter.
  • Tu veux une logique standard mais surchargable (utilise virtual).
  • Tu veux centraliser l’initialisation via un constructeur.

Dans les vrais projets, on voit souvent ce schéma : les "contrats purs" sont dans les interfaces, et si tu veux du code commun ou de l’infra pour les sous-classes — tu fais une classe de base abstraite.


.                     ┌────────────────────────┐
                      │      Interface         │
                      │  (contrat : ce qu’on sait faire) │
                      └─────────┬──────────────┘
                                │
                 ┌──────────────┼──────────────┐
                 │              │              │
           Implémentation 1   Implémentation 2 ...  Implémentation N
             MonLogger     CloudLogger     FileLogger
    
        (tu peux combiner avec l’héritage d’une classe abstraite)
Schéma : où utiliser quoi

7. Scénarios — qui gagne quand

Implémentation multiple :
Imaginons que tu as une interface IDrivable et une classe abstraite Vehicle. Maintenant, la classe Car peut hériter de la base — Vehicle — et en même temps implémenter plusieurs interfaces (IDrivable, IRepairable, IInsurable). Si tu avais une classe abstraite Repairable, il aurait fallu choisir — Vehicle ou Repairable ! Les interfaces gagnent clairement ici.

Logique et état communs :
Imaginons que tous les "véhicules" ont un champ "numéro". Ça doit être un champ de classe abstraite. Dans une interface, pas de champ (sauf statique).

Évolution de l’API :
Un des trucs révolutionnaires avec les Default Interface Methods — maintenant tu peux faire évoluer tes interfaces sans casser les utilisateurs existants.
Par exemple, tu ajoutes une nouvelle méthode avec une implémentation par défaut dans l’interface — tout roule, toutes les anciennes implémentations de l’interface continuent de marcher ! Avant, c’était la galère (ou même impossible).

8. Exemples pratiques

Dans notre appli d’apprentissage, on commence à ajouter du logging. Créons notre interface ILogger avec une implémentation par défaut :


public interface ILogger
{
    void Log(string message);

    // Implémentation par défaut dispo pour toutes les implémentations de l’interface !
    void LogInfo(string info)
    {
        Log("[INFO] " + info);
    }

    // Méthode statique de l’interface
    static void AfficherAide()
    {
        Console.WriteLine("Utilise ILogger pour logger les événements");
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

// Quelque part dans le code :
ILogger logger = new ConsoleLogger();
logger.LogInfo("Le système est lancé !"); // marche grâce à l’implémentation par défaut

// Appel de la méthode statique de l’interface
ILogger.AfficherAide();

Si on ajoutait une nouvelle méthode avec implémentation par défaut dans l’interface, toutes les implémentations existantes (genre ConsoleLogger) auraient automatiquement cette nouvelle méthode — pas de panique, pas de code cassé.

9. Erreurs et subtilités : pratique et pièges

Faut savoir que tout n’est pas si rose. Par exemple, si ton interface a une implémentation par défaut, mais que tu utilises l’objet via le type de la classe et pas via le type interface, l’implémentation par défaut n’est dispo que via l’interface.


ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // Ne compile pas : LogInfo n’est pas défini dans la classe !

ILogger log2 = log;
log2.LogInfo("Hello"); // Nickel !

Ça ressemble à un cas particulier d’implémentation explicite d’interface. Parfois c’est pratique pour cacher de l’API "en trop", parfois c’est surprenant pour les débutants.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION