Classe abstraite ou interface ?

Ce week-end en discutant avec un ami, je me suis aperçu que l’utilité des classes abstraites et des interfaces n’était pas forcément très simple à saisir pour tout le monde. Que font-elles ? Quand utiliser l’une ou l’autre ? C’est le sujet du jour !

 

Classes abstraites

Tout d’abord, une classe abstraite est une classe qui ne peut être instanciée : Elle doit forcément être étendue.

La réelle utilité de ces classes est la factorisation : Chaque classe qui en hérite hérite également de ses attributs et méthodes déjà implémentées, comme toute autre classe. La différence réside dans le fait qu’elles puissent forcer l’utilisateur à implanter certaines méthodes.

Un exemple :

Ici, la classe ParentClass force ses héritiers à définir eux mêmes la méthode showValue() grâce au mot clé abstract devant cette méthode. En revanche, rien n’empêche ChildClass de ré-implémenter le constructeur si le mot clé final n’est pas présent dans la classe.

Si vous aviez une dizaine de classes ‘enfant’, imaginez le gain de code et de maintenabilité 🙂

Interfaces

Passons maintenant aux interfaces. J’aime voir une interface comme un contrat que doit remplir chaque classe qui l’implémente, c’est à dire que l’interface va fixer les contraintes de la classe, sans en définir le comportement interne. En effet, toutes les méthodes d’une interface doivent être déclarées publiques, libre à chaque classe de remplir le contrat comme elle le souhaite.

Une interface et une classe qui l’implémente :

L’exemple est ici très scolaire mais suffisant à la compréhension : L’interface  ExempleInterface  définit un contrat simple : Chaque classe qui l’implémente doit obligatoirement définir 2 fonctions :  showValue()  et setValue($var) .

La classe ABC  implémente ces fonctions comme elle le souhaite, tant qu’elle respecte la visibilité publique des fonctions et les arguments passés aux méthodes. D’ailleurs, nous allons maintenant voir cela en profondeur.

Les arguments de méthodes

Les interfaces et les classes abstraites qui définissent des méthodes abstraites peuvent imposer de recevoir un ou plusieurs arguments :

Cependant, il faut impérativement que chaque classe qui les implémente ou les étend respecte ces arguments, la seule liberté laissée à la classe concrète étant de renommer ces arguments.

Dans le cas contraire, une erreur fatale sera levée.

Typage des arguments

Intéressons nous maintenant au type-hinting, c’est à dire le typage des arguments passés à nos méthodes.

Il en va de même que pour les arguments : Chaque méthode concrète doit respecter le type d’argument définit par la méthode abstraite (d’une classe ou d’une interface).

Aussi, ce genre de bidouillage soulèvera une erreur fatale :

Héritage

Parlons maintenant un peu d’héritage. En effet, les classes abstraites et les interfaces ont un sens de l’héritage quelque peu différent l’un de l’autre.

Classes abstraites

Concernant l’héritage, les classes abstraites ont le même comportement qu’une classe concrète, c’est à dire qu’elles ne peuvent hériter que d’une seule classe, et ne peuvent en étendre qu’une seule également. En revanche, une classe abstraite peut très bien en étendre une autre :

Notez que, comme les classes concrètes, si la méthode testValue($var)  avait une visibilité ‘private’, la classe Concrete  n’y aurait pas accès.

 Interfaces et implémentation

Comme les pour traits, une classe, même abstraite, peut implémenter plusieurs interfaces. De plus, une interface peut très bien en étendre elle-même une autre. Voici un petit exemple d’implémentations et d’héritage :

Comme d’habitude, ce cas est très simple. Notez juste l’implémentation de AnotherInterface  , qui étend elle-même FooInterface . La classe doit donc déclarer toutes les fonctions de ces 2 interfaces.

Ca va aller ? 😉

La compréhension par l’exercice

Afin de vérifier si vous avez bien compris quand utiliser l’un ou l’autre, je vous propose un petit exercice (avec ma correction :p ) tout simple :

Nous possédons 3 véhicules : Une voiture, un camion-benne et un vélo attelé d’une remorque. Ces 3 engins doivent pouvoir remplir la fonction seDeplacer . La voiture et le camion doivent implémenter une fonction ouvrirLeVehicule() .Pour ouvrir la voiture, nous utiliserons une télécommande et pour le camion, la clé.
Enfin, le camion et le vélo peuvent embarquer du matériel donc doivent pouvoir charger()  et decharger() .

Bien sûr il est possible de créer 3 classes et tout définir à l’intérieur, mais évidemment, le but est ici de structurer et factoriser tout cela. A vous de jouer !

Je vous donne ma solution, et j’explique ensuite :

Ici, j’ai choisi d’utiliser 2 interfaces pour remplir les rôles majeurs. Notez que je n’ai pas mis la fonction ouvrirLeVehicule()  dans une interface, car son rôle n’est pas majeur.

Le reste se passe de commentaires, excepté peut-être qu’il était possible de mettre une autre classe abstraite comme VehiculeNonMotorise. D’ailleurs, il existe une multitude de solutions possibles 😉

En résumé

J’espère que ce billet plus long que prévu permettra à ceux qui hésitent encore de mieux définir le rôle des classes abstraites et des interfaces, et savoir quand utiliser l’un ou l’autre.
Le plus important à retenir est qu’une classe abstraite ne doit pas définir le contrat que les classes concrètes vont remplir, mais à les ‘guider’ en leur fournissant des méthodes abstraites ou pas généralistes, que les classes concrètes viendront spécialiser, contrairement aux interfaces, qui ne peut contenir aucun code.

6 Commentaires

Ajouter un commentaire

  1. Et n’oublions pas, aussi, les traits !

    • Je n’ai volontairement pas parlé des traits ici car ils ne sont pas des sujets d’abstraction, comme les interfaces ou classes abstraites.
      Selon moi, ils sont l’exact opposé des interfaces : Une interface ne comporte aucun code, mais donne un contrat à plusieurs classes qui ont forcément des points communs. En revanche, un trait n’est qu’un ensemble de méthode destinée à plusieurs classes qui n’ont peut-être aucun ancêtre commun.

  2. C’est une bonne introduction, sur un sujet qui est, comme tu le notes, trop peu abordé par les développeurs. Je ne sais pas si tu l’as fait pour cet article, mais tu devrais probablement prendre l’habitude de poster des liens vers tes articles sur des sites agrégateurs comme http://news.humancoders.com ou http://reddit.com/r/programmation – ça peut t’attirer du monde et créer le débat.

    Pour revenir au sujet, deux remarques : malheureusement le côté « dynamiquement typé » de PHP fait que la distinction entre interfaces et classes abstraites n’est pas forcément plus clair. De toute façon, arrête-moi bien sûr si je me trompe (je ne développe pas en PHP), il n’y a pas de vérification de types à la compilation dans ce langage, donc pas non plus de vérification que les contrats-interfaces sont bien respectés. L’intérêt est alors essentiellement documentatif.

    Par ailleurs, le problème des classes abstraites et, plus généralement, de l’héritage, est qu’il est à la fois un mécanisme de partage de code (de l’amont vers l’aval), mais aussi un mécanisme de sous-typage (la classe fille est un sous-type de la classe mère). C’est donc deux relations très différentes qui sont implémentées de la même façon (au sein du langage), et la confusion est parfois néfaste. Bien sûr, les interfaces ne présentent pas ce problème. Je te renvoie à cette discussion sur le même sujet : https://zestedesavoir.com/forums/sujet/1288/le-concept-de-loriente-objet/#p36398 .

    • Merci pour ce commentaire riche.

      Tout d’abord, non je ne poste pas sur ce genre de sites. En revanche, je cherchais justement un moyen de faire connaître un peu plus ce site, et je trouve l’idée excellente. Ce sera donc chose faite sous peu, merci de l’info 😉

      Actuellement, et ce jusqu’à l’arrivée de PHP 7 et la possibilité de définir un typage de retour obligatoire, la seule contrainte liée au typage est le type-hinting (d’ailleurs je comptais en faire un billet prochainement), qui définit un type d’argument, comme ceci :

      Ici ce code lancera une fatal error au moment de la compilation à la volée :
      Fatal error: Declaration of Child::Foo() must be compatible with In::Foo(Bar $bar) in …

      Il y a donc moyen de vérifier que chaque contrat soit respecté, encore faut-il utiliser ces moyens (interfaces et type-hinting).

      Enfin, comme dit dans l’article que tu cites, certains langages de programmation ont une séparation claire entre héritage et sous-typage. Pour PHP, ce n’est pas le cas.
      C’est donc au développeur de ne pas tomber dans la facilité et mélanger des types de classe pour la factorisation (il y a d’ailleurs les traits pour cela).
      Personnellement, j’utilise une méthode assez simple pour gérer cela : Je commence par créer mes classes concrètes, sans chercher d’héritage, puis je cherche à factoriser, et je sors mes classes abstraites.
      Puis vient le moment de créer l’héritage en lui-même, et enfin les contrats.
      Dans la plupart des situations (comme dans l’exemple final du billet), cette application suffit à avoir un code à peu près propre.

  3. Bonjour,

    Ma solution est différente de la vôtre, du point de vue, que ça soit Vélo ou Camion ou Voiture, les trois se déplace, donc j’imagine une classe abtraite VehiculeMotorise. et étant donné que vélo ne s’ouvre pas.

  4. Bonjour,

    Merci pour ce commentaire et désolé du temps de latence 😉

    Comme dit plus haut, ma solution n’est qu’une parmi une multitude envisageables.
    En revanche, ta classe VeloPlusRemorque étend la classe VehiculeMotorise, mais le vélo ne possède pas de moteur 😉

    Mais je titille, ta solution est parfaitement viable si les 3 classes peuvent réellement étendre la même classe abstraite.

Laisser un commentaire

Votre adresse mail ne sera pas publiée.

*

© 2017 Max-Koder — Propulsé par WordPress

Theme par Anders NorenHaut ↑