Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 26

Interfaces vs Classes Abstraites : Quand les Utiliser ?

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons exploré les classes abstraites. Vous avez appris à créer des classes de base non-instanciables qui servent de modèle à une famille de classes partageant une identité forte (relation "est un") et du code commun.

Aujourd'hui, nous allons compléter ce tableau en mettant les classes abstraites et les interfaces côte à côte. L'objectif est de vous armer pour prendre l'une des décisions d'architecture les plus importantes en programmation orientée objet : quand utiliser l'une plutôt que l'autre ? Maîtriser ce choix est un pas de géant vers la conception de systèmes logiciels robustes et flexibles, une compétence clé pour un développeur backend.

1. Le cœur de la différence : "Être" contre "Pouvoir faire"

La distinction la plus fondamentale entre une classe abstraite et une interface repose sur la nature de la relation que vous souhaitez modéliser.

La classe abstraite : une relation d'identité ("est un")

Comme nous l'avons vu, on utilise une classe abstraite pour définir le socle commun d'une famille d'objets. Il s'agit de factoriser l'état (attributs) et le comportement (méthodes) qui sont intrinsèques à l'identité même de cette famille.

  • Un Carré est une Forme.
  • Une Voiture est un Vehicule.

Cette relation d'héritage est forte et justifie de partager une base commune. La vidéo suivante résume parfaitement cette idée.

JAVA (Intermédiaire) - 34 - Differences entre classes abstraites et Interfaces

La chaîne "LES TEACHERS DU NET" explique très bien cette idée de relation forte.

Regardez ce passage qui distingue le rôle des classes abstraites de celui des interfaces. Relation forte : L'analogie avec l'héritage dans la vie réelle illustre pourquoi l'héritage de classe (et donc les classes abstraites) est réservé aux liens de parenté étroits entre les concepts.

L'interface : une relation de capacité ("peut faire")

Une interface, quant à elle, ne définit pas ce qu'un objet est, mais ce qu'il peut faire. Elle définit un contrat de comportement ou une capacité qui peut être appliquée à des classes potentiellement très différentes.

  • Un Avion et un Oiseau peuvent tous les deux voler(). Ils implémentent l'interface Volant.
  • Un Bouton et une Image peuvent être cliquables. Ils implémentent l'interface Cliquable.

Notez qu'un Bouton n'est pas une Image, mais ils peuvent partager la même capacité. L'interface est l'outil parfait pour modéliser ces propriétés transversales.

JAVA (Intermédiaire) - 34 - Differences entre classes abstraites et Interfaces

La même vidéo illustre ce point avec un excellent exemple.

Regardez de l'interface Cliquable. Voyez comment cette capacité peut être appliquée à la fois à un Bouton et à une Image, deux classes qui n'ont aucun autre lien de parenté.


2. Comparaison technique

Au-delà du concept, des différences techniques importantes guident le choix. Voici un tableau récapitulatif :

Caractéristique Classe Abstraite Interface
Héritage Une classe ne peut hériter que d'une seule classe (abstraite ou non). Une classe peut implémenter plusieurs interfaces.
Attributs Peut avoir des attributs d'instance de tout type (private, protected...). Ne peut contenir que des constantes (public static final implicitement).
Constructeurs Oui. Ils sont appelés par les constructeurs des classes filles via super(). Non. Une interface ne peut pas être instanciée.
Méthodes Peut contenir un mélange de méthodes abstraites et concrètes. Avant Java 8 : uniquement des méthodes abstraites. Depuis Java 8 : peut aussi avoir des méthodes default (concrètes) et static.
Visibilité Les membres peuvent être public, protected, ou private. Tous les membres sont public par défaut.
Cette image simple résume la différence syntaxique fondamentale : on utilise `extends` pour hériter d'une classe abstraite et `implements` pour implémenter une interface.

3. Scénarios de décision

Appliquons ces connaissances à des scénarios concrets de développement.

Scénario 1 : Vous voulez partager du code commun

Si vos classes partagent non seulement une signature de méthode mais aussi une implémentation commune et des attributs, la classe abstraite est le meilleur choix.

Imaginez les classes Voiture et Camion. Elles héritent toutes les deux d'une classe VehiculeAMoteur. Cette classe abstraite peut contenir un attribut niveauCarburant et une méthode concrète faireLePlein() qui fonctionne de la même manière pour les deux.

public abstract class VehiculeAMoteur {
    protected int niveauCarburant;

    public void faireLePlein(int quantite) {
        this.niveauCarburant += quantite;
        System.out.println("Le plein est fait. Niveau actuel : " + this.niveauCarburant);
    }
    
    public abstract void demarrer(); // Chaque véhicule démarre différemment
}

Une interface ne pourrait pas contenir l'attribut niveauCarburant ni l'implémentation de faireLePlein() (avant les méthodes default).

Scénario 2 : Vous définissez un contrat public

Si vous voulez spécifier un comportement qu'une classe doit avoir, sans vous soucier de son implémentation, utilisez une interface. C'est le cas typique lorsque vous concevez une API.

L'exemple du pilote d'impression est un classique de l'industrie.

Méthodes abstraites, classes abstraites et interfaces

L'article du site Koor.fr illustre parfaitement comment les interfaces sont utilisées pour créer des systèmes "pluggables".

Lisez la section Un cas concret. Concentrez-vous sur l'architecture : L'interface PrinterDriver définit le contrat : printHeader(), printBody(), etc. Le système d'exploitation ne connaît que l'interface PrinterDriver. Différents fabricants (PrinterManufacturer1, PrinterManufacturer2) fournissent leurs propres implémentations. C'est ainsi que des APIs comme JDBC (pour les bases de données) ou les collections Java (List, Map) fonctionnent.

Scénario 3 : Vous avez besoin de flexibilité et de "multi-héritage"

Puisqu'une classe ne peut hériter que d'une seule autre classe, les interfaces sont la seule solution lorsque vous voulez qu'une classe adopte plusieurs "types" ou comportements.

Un OiseauDeGuerre dans un jeu pourrait être :

  • extends Personnage (pour hériter des points de vie, de l'inventaire...)
  • implements Volant (pour la méthode decoller())
  • implements Combattant (pour la méthode attaquer())
public class OiseauDeGuerre extends Personnage implements Volant, Combattant {
    // ... implémentation des méthodes de Volant et Combattant
}

Ceci serait impossible avec des classes abstraites seules.


4. Comment choisir : une règle simple

Posez-vous la question suivante :

Est-ce que mes classes partagent une relation "est un" fondamentale et du code commun ?

  • Oui -> Envisagez une classe abstraite.

Est-ce que je veux définir une capacité ou un contrat qu'une classe "peut faire", indépendamment de ce qu'elle est ?

  • Oui -> Utilisez une interface.

5. Exercice pratique : Conception d'un mini-jeu

Appliquons ces principes pour concevoir la structure des entités d'un jeu de rôle.

Contexte :

  • Le jeu a des Guerriers et des Magiciens. Ce sont tous des Personnages. Ils ont un nom et des points de vie.
  • Les Magiciens peuvent lancer des sorts. Des entités non-joueur, comme un CristalMagique, peuvent aussi être affectées par la magie. Tout ce qui peut être ciblé par un sort doit avoir une méthode subirSort(int degats).
  • Les Guerriers peuvent effectuer une parade. Certains monstres, comme le GobelinAgile, le peuvent aussi. Tout ce qui peut parer doit avoir une méthode parer().

Votre mission :

  1. Classe abstraite Personnage :

    • Créez une classe abstraite Personnage avec les attributs protected String nom et protected int pointsDeVie.
    • Ajoutez un constructeur pour initialiser ces attributs.
    • Ajoutez une méthode concrète afficherInfos() qui affiche le nom et les points de vie.
  2. Interface CiblableParMagie :

    • Créez une interface CiblableParMagie avec une seule méthode : void subirSort(int degats).
  3. Interface PeutParer :

    • Créez une interface PeutParer avec une seule méthode : void parer().
  4. Classes concrètes :

    • Guerrier : hérite de Personnage et implémente PeutParer.
    • Magicien : hérite de Personnage et implémente CiblableParMagie.
    • CristalMagique : n'hérite de rien, mais implémente CiblableParMagie.
  5. Implémentation :

    • Fournissez des implémentations simples pour les méthodes d'interface (par exemple, avec un System.out.println). Pour subirSort, déduisez les dégâts des points de vie du personnage.
  6. Test dans main :

    • Créez des instances de Guerrier, Magicien et CristalMagique.
    • Appelez parer() sur votre guerrier.
    • Appelez subirSort() sur votre magicien et sur votre cristal magique.

Cette structure vous forcera à utiliser une classe abstraite pour la relation d'identité (Guerrier est un Personnage) et des interfaces pour les capacités partagées par des entités hétérogènes.


Conclusion

Vous avez maintenant une compréhension approfondie de deux des outils de conception les plus puissants de la POO. Savoir quand utiliser une classe abstraite ou une interface est ce qui distingue un simple codeur d'un architecte logiciel.

Points clés à retenir :

  • Classe abstraite : Pour une relation d'identité forte ("est un"), partage de code et d'état au sein d'une même famille.
  • Interface : Pour définir une capacité ("peut faire") ou un contrat, applicable à des classes diverses et pour permettre un "multi-héritage" de types.

Dans le prochain module, nous aborderons la gestion des exceptions. Maintenant que vous savez structurer votre code de manière robuste, nous allons apprendre à gérer les situations où les choses ne se passent pas comme prévu, une compétence indispensable pour tout développeur backend professionnel.