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
{
}
Ce comportement nous force à étendre la classe 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;
}
}
À 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);
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.