PHP : Classe abstraite ou Interface

PHP : Classe abstraite ou Interface

Posté le 27/10/2023 | 0 commentaire dans PHP Développement | Retour à la liste

Table des matières

    1. Classes abstraites
    2. Interfaces
    3. Les arguments de méthodes
      1. Typage des arguments
    4. Héritage
      1. Classes abstraites
      2.  Interfaces et implémentation
    5. La compréhension par l’exercice
    6. En résumé

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 :

abstract class ParentClass
{
    protected $value;
    
    public function __construct($value)
    {
        $this->value = $value;
    }
    
    abstract public function showValue();
}

class ChildClass extends ParentClass
{
    public function showValue()
    {
        echo $this->value;
    }
}

$boy = new ChildClass(18);
echo $boy->showValue();
// 18

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 :

interface ExampleInterface
{
    public function showValue();
        
    public function setValue($param);    
}

class ABC implements ExampleInterface
{
    private $value;
    
    public function setValue($var)
    {
        if ($this->testValue($var))
        {
            $this->value = $var;
        }
        else
        {
            $this->value = 'defaut';
        }
    }
    
    public function showValue()
    {
        echo $this->value;
    }
    
    private function testValue($var)
    {
        return is_string($var) ? $var : false;
    }
}

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 :

abstract class Foo
{
    abstract public function bar($var);
}


interface BobInterface
{
    public function bar($var, $var2);
}

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 :

interface BobInterface
{
    public function bar(\Foo $foo);
}

class Plop implements BobInterface
{
    public function bar(\OtherClass $foo)
    {
        echo 'Houston, nous avons un problème';
    }
}

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 :

abstract class Foo
{
    abstract public function showValue();
}

abstract class Bar extends Foo
{
    abstract public function showValue();
    
    protected function testValue($var)
    {
        return is_int($var) ? $var : false ;
    }
}

class Concrete extends Bar
{
    public function showValue()
    {
        if ($this->testValue($var))
        {
            echo $var;
        }
    }
}

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 :

interface FooInterface
{
    public function showValue();
}

interface BarInterface
{
    public function setValue($var);
}

interface AnotherInterface extends FooInterface
{
    public function prepareValuetoShow();
}

class TestClass implements BarInterface, AnotherInterface
{
    private $value;
    
    public function prepareValuetoShow()
    {
        $this->value = 'La valeur est ' . $this->value . '.<br/>';
    }
    
    public function showValue()
    {
        echo $this->value;
    }
    
    public function setValue($var)
    {
        $this->value = $var;
    }
}

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.

Ça 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 :

interface TransportInterface
{
    public function seDeplacer();
}

interface ChargementInterface
{
    public function charger();
    
    public function decharger();
}

abstract class VehiculeMotorise
{
    abstract public function ouvrirLeVehicule();

    public function seDeplacer()
    {
        echo 'Appuyer sur la pédale d\'accélérateur';
    }
}

class Voiture extends VehiculeMotorise implements TransportInterface
{
    public function ouvrirLeVehicule()
    {
        echo 'Appuyer sur la télécommande';
    }
}

class CamionBenne extends VehiculeMotorise implements TransportInterface, ChargementInterface
{
    public function ouvrirLeVehicule()
    {
        echo 'Mettre la clé dans la serrure, tourner la clé';
    }
    
    public function charger()
    {
        echo 'Ouvrir la benne, charger, fermer la benne';
    }
    
    public function decharger()
    {
        echo 'Ouvrir la benne, tout décharger, fermer la benne';
    }
}

class VeloPlusRemorque implements TransportInterface, ChargementInterface
{
    public function seDeplacer()
    {
        echo 'Pédaler...';
    }
    
    public function charger()
    {
        echo 'Mettre le chargement dans la remorque';
    }
    
    public function decharger()
    {
        echo 'Tout ôter de la remorque';
    }
}

$camion = new CamionBenne();
$camion->ouvrirLeVehicule();
$camion->charger();
$camion->seDeplacer();
$camion->decharger();

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.

Max Koder
Max Koder

Développeur en mauvaise herbe, électronicien et bricoleur à mes heures de hobby perdues, compteur de grains de riz, tenteur de tiramisu, mais surtout papa.

Principal développeur de 299Ko, j'essaye de maintenir ce CMS qui me tient à coeur.

Commentaires

Il n'y a pas de commentaires

Ajouter un commentaire