T: / Corrigés des challenges / PHP
Présentation de ces 2 architectures et considérations pour le choix à réaliser.
Dans le challenge MARIO_1 il était question de faire sauter de plateforme en plateforme Peach et Mario. Ils ont chacun des propriétés spécifiques mais ont surtout des actions communes ! On va donc voir dans ce corrigé comment structurer notre code à l’aide de 2 outils : les interfaces et les méthodes abstraites au travers de l’héritage.
Au programme :
On n’est pas ici sur un diagramme UML au sens strict mais il va permettre de visualiser comment les fichiers vont être organisés :
On retrouve donc à gauche l’organisation des fichiers avec une mécanique d’héritage et à droite l’organisation des fichiers avec une mécanique d’interface.
Quand on regarde de plus près, il y a très peu de différence. Il y a un construct côté heritage avec une propriété readonly public alors qu’il y a une méthode getName dans l’interface. On verra dans les explications ce qui justifie cette différence.
La classe Level, au milieu est identique pour les 2 mécaniques d’un point de vue structure. Dans le détails des méthodes il y aura (seulement) une différence.
Si jamais tu n’as pas encore résolu le challenge par toi même, tu peux te baser sur ce schéma pour tenter de le résoudre.
Nos 2 classes Mario et Peach vont donc hériter d’une classe abstraite Jumper.
Une classe abstraite est une classe qui ne peut pas être instanciée, elle est utile dans le cadre de l’héritage, mais on pourrait la considérer comme « insuffisante » pour être instanciée elle même.
On ne pourra donc jamais faire « new Jumper() » au risque de déclencher une erreur fatale.
Voici le code de la classe abstraite Jumper :
abstract class Jumper
{
public function __construct(
public readonly string $name
) {}
abstract public function canJump(int $gapLength): bool;
}
Un peu d’explications :
Maintenant la classe Mario :
class Mario extends Jumper
{
public function __construct()
{
parent::__construct('M');
}
public function canJump(int $gapLength): bool
{
return $gapLength <= 3;
}
}
Un peu d’explications :
Voici le code de la classe Peach :
class Peach extends Jumper
{
public function __construct()
{
parent::__construct('P');
}
public function canJump(int $gapLength): bool
{
return $gapLength >= 3 && $gapLength <= 5;
}
}
Quelles sont les différences ?
La classe abstraite Jumper nous a donc contraint à créer une méthode canJump dans chacune des classes qui en héritait. Mais le contenu de canJump est propre à chaque classe. Je pourrais par exemple avoir une classe Yoshi, avec un Yoshi qui ne saute que les espaces pairs :
public function canJump(int $gapLength): bool
{
return $gapLength % 2 === 0;
}
Nos 2 classes Mario et Peach vont cette fois-ci implémenter l’interface Jumper.
Une interface est un « contrat » que la classe qui l’implémente doit suivre absolument. C’est à dire que l’interface indique la ou les méthodes qui devront obligatoirement être déclarées dans les classes associées.
Voici notre interface :
interface Jumper
{
public function canJump(int $gapLength): bool;
public function getName(): string;
}
Un peu d’explications :
Maintenant la classe Mario :
class Mario implements Jumper
{
public function canJump(int $gapLength): bool
{
return $gapLength <= 3;
}
public function getName(): string
{
return 'M';
}
}
Un peu d’explications :
Voici le code de la classe Peach :
class Peach implements Jumper
{
public function canJump(int $gapLength): bool
{
return $gapLength >= 3 && $gapLength <= 5;
}
public function getName(): string
{
return 'P';
}
}
Important à noter :
On pourrait imaginer une classe Wario, avec un Wario qui ne peut sauter que les espaces correspondant à des nombres premiers :
class Wario implements Jumper
{
public function canJump(int $gapLength): bool
{
// Code spécifique au comportement de Wario
$possibleGaps = [1, 3, 5, 7, 11, 13, 17];
return in_array($gapLength, $possibleGaps);
}
public function getName(): string
{
return 'W'; // Retour spécifique à Wario
}
}
La classe Level va se décomposer en plusieurs sections :
Voici la classe (dans sa version liée à l’héritage) :
class Level
{
private const PLATFORM = 'P';
private string $sequence = '';
private Jumper $currentJumper;
public function __construct(
private string $platfoms,
private Jumper $jumper1,
private Jumper $jumper2
) {
$this->currentJumper = $jumper1;
}
public function run(): void
{
$gaps = explode(self::PLATFORM, $this->platfoms);
foreach ($gaps as $gap) {
$gapLength = strlen($gap);
if ($gapLength === 0) {
// Bords, personne ne saute
continue;
}
// Si les 2 peuvent sauter
if ($this->jumper1->canJump($gapLength) && $this->jumper2->canJump($gapLength)) {
// On incrémente la séquence avec le jumper courant
$this->sequence .= $this->currentJumper->name;
// On change de currentJumper
if ($this->currentJumper->name === $this->jumper1->name) {
$this->currentJumper = $this->jumper2;
} else {
$this->currentJumper = $this->jumper1;
}
continue;
}
if ($this->jumper1->canJump($gapLength)) {
$this->sequence .= $this->jumper1->name;
continue;
}
if ($this->jumper2->canJump($gapLength)) {
$this->sequence .= $this->jumper2->name;
continue;
}
}
}
public function getSequence(): string
{
return $this->sequence;
}
}
Un peu d’explications de la méthode « run » :
Dans la version liée à l’interface, la différence se trouve sur ->name et ->getName(). Dans un premier temps, j’avais gardé une propriété $name dans les classes Mario et Peach. Et tout fonctionnait très bien. Mais lors de mon analyse statique avec PHPStan, j’ai eu l’erreur suivante :
« Access to an undefined property Challenges\MARIO_1_interfaces\Jumper::$name. »
En effet, dans ma classe Level, jumper1 et jumper2 sont typés « Jumper », c’est à dire que le code attend une classe qui implémente cette interface. Mais avec seulement cette déclaration, on ne peut pas deviner que les classes Mario et Peach ont une propriété $name, d’où cette alerte PHPStan.
Cette alerte peut être corrigée en spécifiant à l’interface qu’on aura une propriété $name, grâce à la PHPDoc :
/**
* @property string $name
*/
interface Jumper
{
public function canJump(int $gapLength): bool;
// Et donc plus besoin de getName()
}
Mais je ne trouvais pas ça terrible… Bizarre de « rajouter » la déclaration d’une propriété qui pourrait ne pas être là… Rien ne m’y contraint dans mon code. Une interface ne pouvant pas contenir la déclaration d’une propriété, j’ai donc préféré passer par cette méthode getName(), qui ressemble à un getteur mais qui n’en est pas tout à fait un car on retourne une chaine de caractères directement et non la valeur d’une propriété.
Il est important de noter qu’une classe ne peut hériter que d’une seule autre classe. Alors qu’une classe peut implémenter autant d’interfaces que nécessaires. Les interfaces seront donc à favoriser quand les méthodes à produire ont des contextes différents, on pourra ranger les méthodes dans des interfaces dédiés.
Par exemple :
interface Walkable {
public function walk();
}
interface Jumpable {
public function jump();
}
// Mario peut marcher ET voler, on a donc plusieurs interfaces
class Mario implements Walkable, Jumpable {
// Implémentation des méthodes walk() et jump() ici.
}
On l’a vu avec la mutualisation du constructeur et d’une propriété dans notre exemple. Là où une interface ne permet que des déclarations de méthode, avec l’héritage, la classe parente peut contenir d’autres éléments :
Par exemple :
abstract class Enemy {
protected int $health;
abstract public function attack();
public function takeDamage(int $amount) {
// Réduire la santé de l'ennemi ici.
$this->health -= $amount; // La même mécanique pour tous les enfants
}
}
class Koopa extends Enemy {
public function attack() {
// Attaque spécifique de Koopa ici.
}
}
class PiranhaPlant extends Enemy {
public function attack() {
// Attaque spécifique de PiranhaPlant ici.
}
}
Et oui, on n’a pas toujours besoin de choisir ! On peut cumuler l’héritage (avec classes abstraites ou non) et l’implémentation de une ou plusieurs interfaces.
Un dernier exemple avec le fameux poisson volant du monde de Mario.
C’est un ennemi, qui peut voler et nager :
// Interfaces
interface Flyable {
public function fly();
}
interface Swimable {
public function swim();
}
// Classe parente
// Cf. exemple précédent
// Classe qui hérite et implémente des interfaces
class FlyingFish extends Enemy implements Flyable, Swimable {
public function attack() {
// Attaque spécifique du FlyingFish.
}
public function fly() {
// Implémentation de la capacité de voler.
}
public function swim() {
// Implémentation de la capacité de nager.
}
}
Quelques éléments de réflexion peut choisir la bonne architecture :
Dans cet article on a parlé de :
Et n’hésite pas à tester tout ça sur un autre challenge de code 😉
Le code complet sur Github :
Other content to discover
Corrections, challenges, news, technical monitoring... no spam.