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.