Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 919 minIntermédiaire

Classe abstraite et interface : différence et usages

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire une classe abstraite et des membres abstraits ;
  • définir et implémenter une interface ;
  • programmer pour une interface plutôt que pour une classe ;
  • rendre une classe triable avec IComparable<T> ;
  • choisir entre classe abstraite et interface.

4 exercices corrigés1 schéma

Une classe abstraite est une classe qu'on ne peut pas instancier, qui sert de base commune à des classes filles et peut leur imposer des méthodes à écrire (méthodes abstraites). Une interface est un contrat : une liste de méthodes et de propriétés qu'une classe s'engage à fournir, sans dire comment. Les deux mettent en œuvre l'abstraction, le quatrième pilier de la POO. Ce chapitre montre comment écrire l'une et l'autre en C#, à quoi servent les interfaces de .NET comme IComparable<T>, et comment choisir entre classe abstraite et interface.

L'abstraction : manipuler des concepts

Dans le parc de Sorbier Logistique, il n'existe aucun « matériel » tout court : il y a des postes, des portables, des imprimantes. Materiel est un concept, utile pour écrire du code commun (la valeur nette, l'étiquette), mais créer un objet Materiel n'a pas de sens. De même, beaucoup de traitements n'ont besoin que d'une capacité (« peut s'exporter en CSV », « peut se comparer ») sans s'intéresser à ce qu'est l'objet.

L'abstraction consiste à ne retenir que l'essentiel pour un usage donné, et à cacher le reste. Le code qui travaille avec une abstraction ne dépend pas des détails : il fonctionne avec toutes les classes, présentes et futures, qui la respectent.

La classe abstraite

Une classe abstract ne peut pas être instanciée : new Materiel(…) provoque l'erreur CS0144, « Impossible de créer une instance du type abstract ou de l'interface 'Materiel' ». Elle peut contenir du code ordinaire (champs, constructeurs, méthodes complètes) et des membres abstract, sans corps, que chaque classe fille concrète doit redéfinir.

csharp
abstract class Materiel : IExportableCsv, IComparable<Materiel>
{
    protected Materiel(string numInventaire, string marque, string modele, decimal prixAchat)
    {
        NumInventaire = numInventaire;
        Marque = marque;
        Modele = modele;
        PrixAchat = prixAchat;
    }

    public string NumInventaire { get; }
    public string Marque { get; }
    public string Modele { get; }
    public decimal PrixAchat { get; }

    public abstract int DureeAmortissementAns { get; }      // chaque fille doit la donner

    public abstract string Description();

    public string LigneCsv() =>
        string.Join(';', NumInventaire, GetType().Name, Marque, Modele,
                    PrixAchat.ToString(System.Globalization.CultureInfo.InvariantCulture));

    public int CompareTo(Materiel? autre) => autre is null ? 1 : PrixAchat.CompareTo(autre.PrixAchat);
}

class Poste : Materiel
{
    public Poste(string numInventaire, string marque, string modele, decimal prixAchat)
        : base(numInventaire, marque, modele, prixAchat)
    {
    }

    public override int DureeAmortissementAns => 4;

    public override string Description() => "poste fixe";
}

class Portable : Poste
{
    public Portable(string numInventaire, string marque, string modele, decimal prixAchat)
        : base(numInventaire, marque, modele, prixAchat)
    {
    }

    public override int DureeAmortissementAns => 3;

    public override string Description() => "ordinateur portable";
}

class Imprimante : Materiel
{
    public Imprimante(string numInventaire, string marque, string modele, decimal prixAchat, bool couleur)
        : base(numInventaire, marque, modele, prixAchat)
    {
        Couleur = couleur;
    }

    public bool Couleur { get; }

    public override int DureeAmortissementAns => 5;

    public override string Description() => Couleur ? "imprimante couleur" : "imprimante noir et blanc";
}

Par rapport au chapitre précédent, DureeAmortissementAns n'a plus de valeur par défaut de 5 ans : elle est abstract, et le compilateur oblige chaque classe concrète à la fournir. Une classe fille qui oublie provoque l'erreur CS0534 : « 'Imprimante' n'implémente pas le membre abstrait hérité 'Materiel.Description()' ». Oublier une règle devient impossible.

MembreMot-clé dans la mèreDans la fille
Méthode complète, non modifiable(aucun)héritée telle quelle
Méthode avec un comportement par défaut, modifiablevirtualoverride facultatif
Méthode sans comportement, à fournirabstract (seulement dans une classe abstraite)override obligatoire dans une classe concrète

Le constructeur d'une classe abstraite est souvent protected : seules les classes filles l'appellent, via base(...). Une classe fille peut elle-même rester abstraite si elle ne redéfinit pas tous les membres abstraits.

L'interface : un contrat

Une interface déclare ce qu'une classe sait faire, sans rien dire de ce qu'elle est. Par convention, son nom commence par un I majuscule.

csharp
interface IExportableCsv
{
    string LigneCsv();
}

Une classe qui implémente l'interface s'engage à fournir tous ses membres, en public. L'implémentation s'écrit avec deux-points, comme l'héritage :

csharp
class Ticket : IExportableCsv
{
    public Ticket(int numero, string titre, string priorite)
    {
        Numero = numero;
        Titre = titre;
        Priorite = priorite;
    }

    public int Numero { get; }
    public string Titre { get; }
    public string Priorite { get; }

    public string LigneCsv() => $"{Numero};{Titre};{Priorite}";
}

Un membre manquant provoque l'erreur CS0535 : « 'Ticket' n'implémente pas le membre d'interface 'IExportableCsv.LigneCsv()' ». Dans une interface, les membres sont publics par défaut et n'ont pas besoin du mot-clé public ; dans la classe, l'implémentation doit l'avoir.

Une interface ne contient ni champ ni constructeur, et on ne l'instancie pas. Mais on peut déclarer une variable, un paramètre ou une liste de son type : ils accepteront tout objet d'une classe qui l'implémente.

Programmer pour une interface

Materiel et Ticket n'ont aucun lien d'héritage : l'un est un équipement, l'autre une demande d'assistance. Grâce à l'interface, un même code les exporte tous les deux.

Export+ VersCsv(…) : string«interface»IExportableCsv+ LigneCsv() : stringTicket+ LigneCsv() : stringMateriel{abstract}+ DureeAmortissementAns : int+ Description() : string+ LigneCsv() : stringPosteImprimantePortableutiliseréaliseréalisehéritage : trait pleinréalisation : pointillés
Materiel et Ticket n'ont rien en commun, sauf le contrat IExportableCsv
csharp
static class Export
{
    public static string VersCsv(IEnumerable<IExportableCsv> elements) =>
        string.Join('\n', elements.Select(e => e.LigneCsv()));
}
csharp
List<Materiel> parc =
[
    new Poste("PC-0001", "Dell", "OptiPlex 7020", 899m),
    new Imprimante("IM-0001", "Brother", "HL-L8360", 529m, couleur: true),
    new Portable("PC-0002", "Lenovo", "ThinkPad T14", 1290m),
];
List<IExportableCsv> aExporter = [.. parc, new Ticket(4, "Serveur de fichiers inaccessible", "critique")];
foreach (IExportableCsv e in aExporter)
{
    Console.WriteLine(e.LigneCsv());
}
text
PC-0001;Poste;Dell;OptiPlex 7020;899
IM-0001;Imprimante;Brother;HL-L8360;529
PC-0002;Portable;Lenovo;ThinkPad T14;1290
4;Serveur de fichiers inaccessible;critique

[.. parc, autre] est une expression de collection avec l'opérateur d'étalement .. : il recopie les éléments de parc, puis ajoute le ticket. La classe Export ne connaît que IExportableCsv : demain, un Logiciel exportable s'ajoutera sans modifier une ligne d'Export. Programmer pour une interface, pas pour une implémentation est l'un des deux principes posés dès le premier chapitre du livre fondateur des patrons de conception (1994), et le cœur des chapitres 12 à 14.

Implémenter plusieurs interfaces

Une classe n'a qu'une classe mère, mais elle peut implémenter autant d'interfaces qu'elle veut. On écrit la classe mère en premier, puis les interfaces : abstract class Materiel : IExportableCsv, IComparable<Materiel>. Materiel sait ainsi à la fois s'exporter et se comparer, deux capacités indépendantes.

Les interfaces de .NET

Le framework définit des interfaces que tes classes peuvent implémenter pour s'intégrer aux outils existants.

InterfaceCe qu'elle prometCe qu'elle débloque
IComparable<T>int CompareTo(T? autre) : négatif, zéro ou positifList<T>.Sort(), Order(), Min(), Max()
IEquatable<T>bool Equals(T? autre)une égalité rapide et typée (chapitre 11)
IEnumerable<T>pouvoir être parcouruforeach et toutes les méthodes LINQ
IDisposablevoid Dispose() : libérer une ressourcel'instruction using (chapitre 10)

Avec IComparable<Materiel>, parc.Sort() trie le matériel par prix d'achat croissant :

csharp
parc.Sort();
foreach (Materiel m in parc)
{
    Console.WriteLine($"{m.NumInventaire} {m.PrixAchat,8:N2} {m.DureeAmortissementAns} ans {m.Description()}");
}
text
IM-0001   529,00 5 ans imprimante couleur
PC-0001   899,00 4 ans poste fixe
PC-0002 1 290,00 3 ans ordinateur portable

Sans l'interface, Sort() lève une InvalidOperationException : .NET ne sait pas comparer deux matériels. CompareTo renvoie un nombre négatif si l'objet courant vient avant l'autre, zéro s'ils sont à égalité, positif s'il vient après. Le plus simple est de déléguer à la comparaison d'une propriété, comme ici PrixAchat.CompareTo(autre.PrixAchat).

Les membres par défaut d'une interface

Depuis C# 8, une interface peut fournir une implémentation par défaut à un membre. Les classes existantes n'ont alors pas à l'écrire, ce qui permet de faire évoluer une interface sans casser tout le code qui l'implémente.

csharp
interface IExportableCsv
{
    string LigneCsv();

    // Implémentation par défaut (C# 8 et plus)
    string Entete() => "donnees";
}

Particularité : ce membre par défaut n'est visible qu'à travers une variable du type de l'interface. premier.Entete() fonctionne si premier est déclaré IExportableCsv, pas s'il est déclaré Materiel. Garde cette possibilité pour les bibliothèques qui évoluent ; dans ton code, une interface reste d'abord un contrat sans code.

Classe abstraite ou interface : comment choisir

CritèreClasse abstraiteInterface
Relation exprimée« est un » : une famille d'objets« sait faire » : une capacité
Combien par classeune seule (héritage simple)autant que nécessaire
Peut contenir des champs, un étatouinon
Peut contenir des constructeursouinon
Peut contenir du codeoui, librementseulement des membres par défaut, avec restrictions
Visibilité des membrestoutes (protected compris)publics par défaut
Classes concernéesdes classes de même naturedes classes sans lien entre elles
Exemple GestParcMateriel : numéro, prix, amortissement communsIExportableCsv, INotificateur, ITicketRepository

En UML

ÉlémentNotation
Classe abstraitenom en italique, ou suivi de {abstract}
Opération abstraiteen italique, ou suivie de {abstract}
Interfacerectangle avec le stéréotype «interface» au-dessus du nom (ou un petit cercle, notation « sucette »)
Héritage d'une classeflèche pleine à triangle vide, de la fille vers la mère
Réalisation d'une interfaceflèche en pointillés à triangle vide, de la classe vers l'interface

Sur papier, le correcteur ne voit pas toujours l'italique : écris {abstract} pour lever le doute.

Exercices

Exercice 1 sur 4 Ça compile ? Réussi À revoir

Avec les classes du chapitre (Materiel abstraite, Poste, Portable, Imprimante, l'interface IExportableCsv), dis si chaque ligne compile. Si non, donne la raison.

  1. Materiel m = new Poste("PC-0001", "Dell", "OptiPlex 7020", 899m);
  2. Materiel m = new Materiel("PC-0001", "Dell", "OptiPlex 7020", 899m);
  3. IExportableCsv e = new Imprimante("IM-0001", "Brother", "HL-L8360", 529m, true);
  4. IExportableCsv e = new IExportableCsv();
  5. List<IExportableCsv> l = [new Ticket(1, "Écran noir", "haute")];
  6. class Scanner : Materiel { public override string Description() => "scanner"; }
Voir la réponseMasquer la réponse de l'exercice 1
  1. Oui : un Poste est un Materiel.
  2. Non : Materiel est abstraite, erreur CS0144. De plus son constructeur est protected.
  3. Oui : Imprimante hérite de Materiel, qui implémente l'interface ; les filles l'implémentent donc aussi.
  4. Non : on n'instancie pas une interface (CS0144).
  5. Oui : Ticket implémente IExportableCsv.
  6. Non, pour deux raisons : Scanner ne fournit pas DureeAmortissementAns, membre abstrait (CS0534), et n'a pas de constructeur qui appelle base(...) (CS7036, Materiel n'a pas de constructeur sans paramètre).

Tu avais trouvé ?

Exercice 2 sur 4 Écrire une interface Réussi À revoir

Le service informatique veut envoyer des notifications à un employé ou à tout un service. Écris l'interface INotifiable, avec une méthode IEnumerable<string> Adresses(), et deux classes qui l'implémentent : Employe (une adresse) et Service (la liste des adresses de ses membres). Combien d'adresses distinctes reçoivent un message envoyé à Julie Lefèvre et au service Comptabilité (Karim Morel et Julie Lefèvre) ?

Voir la réponseMasquer la réponse de l'exercice 2
csharp
interface INotifiable
{
    IEnumerable<string> Adresses();
}

class Employe(string email) : INotifiable
{
    public string Email { get; } = email;
    public IEnumerable<string> Adresses() => [Email];
}

class Service(string code, List<string> emails) : INotifiable
{
    public string Code { get; } = code;
    public IEnumerable<string> Adresses() => emails;
}
csharp
List<INotifiable> destinataires =
[
    new Employe("julie.lefevre@sorbier.example"),
    new Service("COMPTA", ["karim.morel@sorbier.example", "julie.lefevre@sorbier.example"]),
];
int nb = destinataires.SelectMany(d => d.Adresses()).Distinct().Count();   // 2

2 adresses distinctes : Julie apparaît deux fois, Distinct() la dédoublonne. SelectMany aplatit les listes d'adresses en une seule séquence. L'expéditeur ne sait pas s'il écrit à une personne ou à un service : il ne connaît que le contrat INotifiable. Un Employe et un Service ne partagent aucune nature commune, d'où le choix d'une interface plutôt que d'une classe abstraite.

Tu avais trouvé ?

Exercice 3 sur 4 Classe abstraite ou interface ? Réussi À revoir

Pour chaque besoin, choisis une classe abstraite ou une interface, et justifie en une phrase.

  1. Les frais d'un déplacement : frais kilométriques, nuitée, repas. Tous ont une date, un montant et un justificatif ; chacun calcule son montant remboursable à sa façon.
  2. Pouvoir journaliser un message, dans un fichier, dans la console ou dans une base.
  3. Pouvoir être affiché dans une liste déroulante avec un libellé : des employés, des services, des logiciels.
  4. Les comptes d'une application : utilisateur, administrateur, partenaire externe, avec un identifiant, un mot de passe haché et une méthode de vérification commune.
Voir la réponseMasquer la réponse de l'exercice 3
  1. Classe abstraite Frais : les frais sont de même nature (« est un frais »), partagent un état (date, montant, justificatif) et une méthode abstraite MontantRemboursable().
  2. Interface IJournal : une capacité, avec des implémentations sans code commun ; c'est le cas typique de l'injection de dépendances (chapitre 14).
  3. Interface IAffichable (propriété Libelle) : des classes sans lien de nature partagent une capacité.
  4. Classe abstraite Compte : un état commun (identifiant, empreinte du mot de passe) et une vérification commune, écrite une fois.

Tu avais trouvé ?

Exercice 4 sur 4 Trier des tickets Réussi À revoir

Fais implémenter IComparable<Ticket> à la classe Ticket (propriétés Numero et Priorite, une énumération Basse, Normale, Haute, Critique) pour que Sort() range les tickets par priorité décroissante, puis par numéro croissant à priorité égale. Donne l'ordre obtenu pour les tickets 3 (normale), 1 (haute), 7 (haute), 2 (critique) et 5 (basse).

Voir la réponseMasquer la réponse de l'exercice 4
csharp
class Ticket(int numero, Priorite priorite) : IComparable<Ticket>
{
    public int Numero { get; } = numero;
    public Priorite Priorite { get; } = priorite;

    public int CompareTo(Ticket? autre)
    {
        if (autre is null) return 1;
        int parPriorite = autre.Priorite.CompareTo(Priorite);    // inversé : décroissant
        return parPriorite != 0 ? parPriorite : Numero.CompareTo(autre.Numero);
    }
}

Ordre obtenu : #2 (critique), #1 (haute), #7 (haute), #3 (normale), #5 (basse). Pour un ordre décroissant, on compare autre à l'objet courant au lieu de l'inverse. Avec LINQ, sans toucher à la classe : tickets.OrderByDescending(t => t.Priorite).ThenBy(t => t.Numero).

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Exceptions : try, catch et exceptions personnaliséesRevoir le sommaire du cours