Aller au contenu
Accueil » [PHP] Héritage et classes abstraites

[PHP] Héritage et classes abstraites

Aujourd’hui, nous allons voir ensemble la notion d’héritage en PHP, et à quoi peuvent bien servir les classes abstraites. Vous avez sûrement déjà entendu parler de ce concept, mais ne peut-être jamais osé l’utiliser dans vos développements ? Il est temps de mettre fin à tout cela, et de se plonger dans l’abstraction. On y va !

Qu’est-ce qu’une classe abstraite

Avant toute chose, une définition s’impose, pour bien cerner le sujet dont on va parler. On dit qu’une classe et ses méthodes sont abstraites sont lorsque la classe parente a besoin de sa ou ses classes enfant pour s’exécuter. En pratique, vous allez avoir une classe parente qui définit des méthodes, et une ou plusieurs classes enfant qui en hériteront.

Si vous avez suivi ce billet à propos des interfaces, vous vous demandez peut-être quelle est la différence entre les deux, la définition étant assez similaire. Néanmoins, le concept est bien différent :

  • Les interfaces sont un squelette, et ce sont les classes enfants qui doivent tout implémenter.
  • Les classes abstraites peuvent contenir une information commune, et les classes enfants peuvent également ajouter ce qui leur manque.
  • Toutes les méthodes d’une interface sont privées, alors que celles des classes abstraites peuvent être de tout type.
  • On peut implémenter plusieurs interfaces, mais n’hériter que d’une seule classe abstraite.

C’est pour cela qu’on parlera d’héritage : Les enfants vont, en plus d’être des classes parfaitement classiques, hériter de toutes les propriétés et méthodes de leur classe parente.

Comment implémenter une classe abstraite

Maintenant que le « pourquoi » est établi, il nous reste à définir le « comment ». Un exemple direct de classe abstraite sera plus parlant qu’un long discours :

<?php

abstract class ParentClass
{
    private const CONTEXT = 'PARENT';

    public function useMeFromChildClass(): string
    {
        return 'I come from ' . $this->getContext();
    }

    public function getContext(): string
    {
        return self::CONTEXT;
    }

    abstract public function overrideMe(): string;
}

class ChildClass extends ParentClass
{
    private const CONTEXT = 'CHILD';

    public function overrideMe(): string
    {
        return 'I come from ' . self::CONTEXT . ', overriding ' . $this->getContext();
    }
}

$child = new ChildClass();

echo $child->useMeFromChildClass();
echo "<br>";
echo $child->overrideMe();

Décortiquons ensemble ce bout de code :

  • On commence par définir notre classe parente :
    • On la définit toujours avec le mot clé abstract.
    • À l »intérieur, on y entre nos méthodes, constantes et ce que l’on y veut. On peut également définir des méthodes abstraites, dont on fournit, comme dans les interfaces, simplement la déclaration. Dans ce cas, l’implémentation sera obligatoire dans la classe enfant.
  • Puis, on définit notre classe enfant :
    • Comme on veut qu’elle étende de notre parent, on le spécifiera avec le mot clé extends
    • On y implémente ce que l’on veut, comme une classe classique, avec la contrainte d’implémenter les méthodes abstraites, si existantes (dans notre cas, le méthode overrideMe doit l’être)
    • On peut y utiliser toutes les méthodes et propriétés de notre parent, si le contexte le permet
  • Ensuite, lorsque l’on crée une nouvelle instance de notre classe enfant, on appelle simplement nos méthodes, comme si les deux classes ne faisaient plus qu’une.

Ainsi, le bout de code suivant nous affichera :

I come from PARENT
I come from CHILD, overriding PARENT

Et voilà, nous avons écrit notre première classe abstraite. Facile non ?

Cas d’utilisation

Alors oui, je vous vois venir, c’est super tout ça, on a pu faire un exemple d’héritage, mais à quoi ça va bien pouvoir servir de faire ça dans la vie de tous les jours ? Et bien à peu près à tout, vous verrez que l’héritage est une notion très répandue. Pour vous convaincre et vous démontrer que ce concept est utile et puissant, essayons de l’appliquer à un exemple parlant :

Imaginons que vous vouliez créer un lecteur de fichiers. Vous aimeriez que tous vos fichiers soient construits un peu pareil histoire de savoir quelle méthodes appeler pour les lire, pour connaître leurs informations etc. On pourrait partir du fait que tous ces fichiers ont une base commune, et qu chacun aurait ses spécificités. Vous reconnaissez un peu la définition de l’introduction ? C’est parfait, voyons un peu comment se matérialiserait tout cela en code. Je vous le commente en direct pour que ce soit plus clair :

Créons tout d’abord notre parent, la classe PlayableFile :

<?php

abstract class PlayableFile
{
    public function __construct(private string $name, private string $extension)
    {
        // we instantiate 2 properties, as well as their respective getters
    }

    public function getExtension(): string
    {
        return $this->extension;
    }

    public function getName(): string
    {
        return $this->name;
    }

    // then we declare 2 abstract methods, which the children will have to implement

    abstract public function play(): void;

    abstract public function info(): string;
}

Maintenant que notre classe abstraite est disponible, allons lui construire deux enfants. Le premier, qui correspondrait par exemple à un fichier de type mp3 :

<?php

class Mp3 extends PlayableFile
{
    private const EXT = 'mp3';

    public function __construct(private string $name, private int $duration)
    {
        // We call our parent constructor with the specific information we have
        parent::__construct($this->name, self::EXT);
    }

    public function getDuration(): int
    {
        return $this->duration;
    }

    public function play(): void
    {
        $mp3 = file_get_contents('/etc/files/' . $this->getName() . '.' . $this->getExtension()); // name and extension from parent
        $this->playMp3File($mp3);
    }

    private function playMp3File(string $mp3): void
    {
        // do whatever is relevant for it
    }

    public function info(): string
    {
        return 'I am ' . $this->getName() . ', my duration is ' . $this->getDuration();
    }
}

Puis, plus simpliste, une classe correspondant au format gif :

<?php

class Gif extends PlayableFile
{
    public const EXT = 'gif';

    public function play(): void
    {
        $gif = file_get_contents('/etc/files/' . $this->getName() . '.' . $this->getExtension()); // name and extension from parent
    }

    public function info(): string
    {
        return 'I am ' . $this->getName();
    }
}

Et voilà, maintenant que tout notre écosystème est prêt, il n’y a plus qu’à le tester ! Prenons ce bout de code simple, qui boucle sur nos différents enfants :

<?php

use Mp3;
use Gif;
use PlayableFile;

$mp3 = new Mp3(
    name: 'highway-to-hell.mp3',
    duration: '3343'
);

$gif = new Gif(
    name: 'trololo.gif',
    extension: Gif::EXT
);

foreach ([$mp3, $gif] as $child) {
    assert($child instanceof PlayableFile);
    //
    echo $child->info() . '<br>';
}

Sans surprises, le résultat sera alors :

I am highway-to-hell.mp3, my duration is 3343
I am trololo.gif

Conclusion

En définitive, nous avons vu ce qu’était une classe abstraite, s’inscrivant dans la notion d’héritage, ainsi que son utilisation dans un cas pratique. N’hésitez pas à prendre en main se concept et à l’appliquer partout où son utilité s’en fait ressentir. Vous gagnerez ainsi en qualité de code et perdrez en dette technique.

Il est aussi très intéressant de se pencher sur les interfaces en PHP, que vous pouvez tout à fait coupler avec l’asbtraction, pour avoir des applications encore plus modulables et flexibles.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *