• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

Ce cours existe en livre papier.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 21/08/2023

Contraignez l’usage de vos classes

Imposez l'héritage d’une classe

Lorsque vous voulez garantir un usage particulier, mais laisser les classes enfants décider de la manière dont le code doit fonctionner, alors l’abstraction est LA mécanique désirée !

Jusqu’à maintenant, nous avons appris à créer des classes, les étendre, surcharger les méthodes, et gérer la visibilité. À présent, ajoutons  des mécanismes pour contraindre et assouplir en même temps l’héritage avec l’abstraction. C'est très utile pour anticiper des variations futures dans notre code, sans les connaître à l'avance. Le code est ainsi beaucoup moins fermé, et nous ne serons pas contraints à dupliquer des pans entiers de code. 

Pensons un instant à notre logiciel. L’utilisateur est étendu par un administrateur, puis nous avons travaillé avec des classes Player  et QueuingPlayer  dans les TP. Finalement, on se rend compte que les Player  pourraient eux aussi hériter de User, et que jamais nous n’allons instancier la classe User  seule puisque nos utilisateurs seront forcément soit des joueurs, soit des administrateurs. Une idée serait d’interdire l’usage de User seul, et dans le même temps, de forcer son héritage. La classe servira de “squelette” pour les classes suivantes. Elle contient les bases minimalistes nécessaires.

Dans notre code, ceci est représenté par le mot clé abstract. Il va venir en préfixe de la déclaration d'une classe.

<?php
 
abstract class User
{
}

Tester ce code.

Ce comportement nous force à étendre la classe pour utiliser son contenu.

Comme les morceaux d'un puzzle, il faut qu'une classe abstrait soit étendue pour l'utiliser. Une classe enfant peut accéder à ses propriétés publics et protégés, comme pour une classe normale.
Il faut étendre une classe abstraite pour utiliser son contenu

Mais ça ne signifie pas pour autant que cette abstraction doive être vide.

Notre classe User peut rester identique à ce qu’elle était, mais avec le mot clé   abstract  en plus.

<?php

declare(strict_types=1);

abstract class User
{
    public const STATUS_ACTIVE = 'active';
    public const STATUS_INACTIVE = 'inactive';

    public function __construct(public string $username, public string $status = self::STATUS_ACTIVE)
    {
    }

    public function setStatus(string $status): void
    {
        assert(
            in_array($status, [self::STATUS_ACTIVE, self::STATUS_INACTIVE]),
            sprintf('Le status %s n\'est pas valide. Les status possibles sont : %s', $status, implode(‘, ‘,[self::STATUS_ACTIVE, self::STATUS_INACTIVE]))
        );
    
        $this->status = $status;
    }
    
    public function getStatus(): string
    {
        return $this->status;
    }
}

Ce qui est pratique avec le mot clé   abstract  qui se situe devant le mot clé class, c’est qu’une fois qu’il est là, on peut aussi l’utiliser devant une méthode de classe ! Cela permet de déclarer la signature de la méthode (visibilité, statique ou non, nom, arguments, et type de retour) comme nécessaire, mais nous n'écrivons pas son comportement. Nous terminons là par un ‘;’, nous ne mettons pas d’accolades, et pas de code non plus. Pas tout de suite. Nous exprimons juste que cette méthode doit exister dans les classes enfants, sans dire comment elle fonctionne. C’est à la classe enfant de porter cette responsabilité.

Pour le démontrer, imaginons la méthode   getUsername()  dont le retour serait l’e-mail pour un administrateur, et le pseudo pour un joueur.

Allons-y.

<?php

declare(strict_types=1);

abstract class User
{
    public const STATUS_ACTIVE = 'active';
    public const STATUS_INACTIVE = 'inactive';

    public function __construct(public string $email, public string $status = self::STATUS_ACTIVE)
    {
    }

    public function setStatus(string $status): void
    {
        assert(
            in_array($status, [self::STATUS_ACTIVE, self::STATUS_INACTIVE]),
            sprintf('Le status %s n\'est pas valide. Les status possibles sont : %s', $status, [self::STATUS_ACTIVE, self::STATUS_INACTIVE])
        );
    
        $this->status = $status;
    }
    
    public function getStatus(): string
    {
        return $this->status;
    }
    
    abstract public function getUsername(): string;
}

class Admin extends User
{
    // Ajout d'un tableau de roles pour affiner les droits des administrateurs :)
    public function __construct(string $email, string $status = self::STATUS_ACTIVE, public array $roles = [])
    {
        parent::__construct($email, $status);
    }

    // ...
    
    public function getUsername(): string
    {
        return $this->email;
    }
}

class Player extends User
{
    // Ajout d'un tableau de roles pour affiner les droits des administrateurs :)
    public function __construct(string $email, public string $username, string $status = self::STATUS_ACTIVE)
    {
        parent::__construct($email, $status);
    }

    // ...
    
    public function getUsername(): string
    {
        return $this->username;
    }
}

Tester ce code

À présent que la classe est étendue et que nous avons déclaré la méthode  getUsername  avec sa logique propre, nous pouvons instancier notre classe   Admin  ou   Player, et utiliser la méthode.

Autrement dit, chaque classe enfant de   User  devra posséder une méthode getUsername  . Ou alors cette classe devra elle-même être déclarée abstraite.

Grâce à cette mécanique, vous êtes à présent capable de créer des structures préparées, avec des attentes particulières, qui devront être étendues et écrites plus tard. 

Il nous reste encore une situation : celle où nous souhaitons indiquer à un développeur que nous estimons que le code ne devrait plus évoluer.

Empêchez l'héritage

Nous avons notre utilisateur, que nous souhaitons étendre. C'est chose faite avec l’administrateur. Mais une fois que nous avons notre administrateur, que pourrait-on lui apporter de plus ? À priori rien. Je décide donc d'interdire à quiconque d'en hériter.

Pour interdire l'héritage d'une classe, nous allons utiliser le mot clé final. Il va venir préfixer le mot clé class, de la même manière que le mot clé abstract.

<?php

declare(strict_types=1);

abstract class User
{
    public const STATUS_ACTIVE = 'active';
    public const STATUS_INACTIVE = 'inactive';

    public function __construct(public string $email, public string $status = self::STATUS_ACTIVE)
    {
    }

    public function setStatus(string $status): void
    {
        assert(
            in_array($status, [self::STATUS_ACTIVE, self::STATUS_INACTIVE]),
            sprintf('Le status %s n\'est pas valide. Les status possibles sont : %s', $status, implode(', ',[self::STATUS_ACTIVE, self::STATUS_INACTIVE]))
        );
    
        $this->status = $status;
    }
    
    public function getStatus(): string
    {
        return $this->status;
    }
    
    abstract public function getUsername(): string;
}

final class Admin extends User
{
    // Ajout d'un tableau de roles pour affiner les droits des administrateurs :)
    public function __construct(string $email, string $status = self::STATUS_ACTIVE, public array $roles = [])
    {
        parent::__construct($email, $status);
    }
    
    public function getUsername(): string
    {
        return $this->email;
    }

    // ...
}

// Ceci est impossible maintenant que la classe Admin est marquée comme finale.
// class SuperAdmin extends Admin {} 


$admin = new Admin('trompete@guy.com', 'Ibrahim Maalouf');
var_dump($admin);

Tester ce code.

Testez ce code. Essayez de créer une nouvelle classe qui étend l'Admin. Qu'avez-vous obtenu ? C'est exact, une erreur. Vous avez correctement empêché l'héritage de la classe !

Il est temps de vous exercer !

Exercez-vous

Reprenons la solution du précédent exercice. :)

Modifiez le code pour garantir l'extensibilité des méthodes, créez une classe abstraite pour les joueurs, et utilisez le mot clé "final".

Si vous voyez d'autres modifications utiles, effectuez-les !

Vous trouverez le code sur la branche P2C4, et la correction sur la branche P2C4-correction. :)

‌En résumé

  • En utilisant le mot clé abstract, vous pouvez imposer à une classe d'être héritée.

  • Une classe abstraite ne peut plus être instanciée seule, et peut contenir des méthodes abstraites.

  • Une méthode abstraite doit être implémentée dans les classes enfants, ou alors celle-ci doit aussi être abstraite.

  • Vous pouvez également interdire l’héritage à l’aide du mot clé  final  sur une classe ou sur une méthode.

Définir des classes, et en hériter, c’est pratique. Et comme nous avons pu le voir dans le chapitre 2 de cette partie, il arrive que des méthodes ou des propriétés définies dans une classe parente ne correspondent plus tout à fait au besoin de la classe enfant. Il faut peut-être ajouter un comportement, ou parfois même le réécrire ! Il existe dans certaines situations une alternative. Voyons-la dans le prochain chapitre. :)

Exemple de certificat de réussite
Exemple de certificat de réussite