Top Code 2024, les challenges sont de nouveau disponibles dans les boards pour les participant(e)s => Boards

Refactoring de code PHP en appliquant un principe de SOLID

Le S de SOLID implique que chaque classe ait un seul rôle. On se base sur ce principe pour refacto du code.

→ Corrigé du Challenge : WALL-E #1

Dans un précédent corrigé, pour le challenge Wall-E, j’ai créé une seule classe pour résoudre le challenge.

La classe Robot permettait de tout gérer :

  • La vitesse du robot
  • La batterie du robot
  • La mécanique de traitement des déchets

Plusieurs méthodes de cette classe ont été créées en « public » alors qu’elles pourraient être « private » car il n’y a que la classe qui les utilise. C’est en général un bon indicateur qui révèle qu’on ne respecte pas le « S » de « SOLID », à savoir le principe de « SINGLE RESPONSABILITY« . Ce principe implique qu’une classe doit avoir une seule responsabilité. Notre classe Robot gère donc trop de choses.

On va essayer de refactoriser tout ça. On va en profiter pour tout (re)coder en anglais. Pour bien comprendre ce corrigé, il est important de découvrir la première version, qui détaille la logique du challenge et les explications du code.

De une à plusieurs classes PHP

En réfléchissant un peu, on peut découper notre unique classe Robot en plusieurs :

  • On garde une classe Robot, qui restera notre classe principale
  • On crée une classe Battery car la batterie a des interactions qui lui sont propres
  • On crée une classe Lifter (= lever) qui va gérer la mécanique de traitement des déchets

La classe Robot va donc évoluer :

class Robot
{
    private int $speed;

    private Battery $battery;
    private Lifter $lifter;

    public function __construct(int $force, int $speed, int $batteryLevel)
    {
        $this->speed = $speed;
        $this->lifter = new Lifter($force);
        $this->battery = new Battery($batteryLevel);
    }
}
  • La vitesse ne change pas, c’est une information propre au Robot, on garde donc une propriété dédiée, privée.
  • Par contre, la batterie et la mécanique de « levage » vont être définis par leur propre classe.
  • Toutes les constantes ont disparues, elles étaient d’ailleurs préfixées « BATTERIE_ » ce qui nous indique aussi qu’elles sont liées à la batterie, et non au robot.

Classe Battery :

class Battery
{
    private const LEVEL_FOR_RECHARGE = 20;
    private const MAX = 100;
    private const MIN = 0;
    
    public function __construct(
        private int $level
    )
    {
    }

    public function recharge(int $speed): void
    {
        // Si la batterie de Wall-E passe sous les 20%, il doit aller se recharger
        if ($this->level < self::LEVEL_FOR_RECHARGE) {

            // Mais si la vitesse de Wall-E est supérieure à la batterie restante, alors il tombe en panne et le petit robot s'arrête.
            if ($this->level - $speed <= self::MIN) {
                $this->level = self::MIN;
                return;
            }

            //  Il se recharge à 100% et utilise à nouveau de la batterie pour revenir, le même montant (vitesse)
            $this->level = self::MAX - $speed;
        }
    }

    public function consume(int $battery): void
    {
        $this->level -= $battery;
    }

    public function getLevel(): int
    {
        return $this->level;
    }

    public function isDown(): bool
    {
        return $this->level <= self::MIN;
    }
}
  • On utilise la promotion de propriétés dans le constructeur, disponible depuis PHP 8.
  • La méthode consume permet de décrémenter le niveau de batterie
  • La méthode getLevel est un getteur classique qui permet d’accéder à la propriété level qu’on a défini private
  • La méthode isDown renvoie un booléen et permet de savoir si la batterie est tombé à zéro (ou moins)
  • Enfin, la méthode recharge gère la mécanique de recharge, elle a besoin de récupérer l’information de la vitesse (speed) du robot. C’est pour ça qu’on la retrouve en paramètre.

Classe Lifter :

class Lifter
{
    private const BATTERY_RATIO_TO_LIFT_WEIGHT = 2;
    private const BATTERY_IF_NOT_POSSIBLE_TO_LIFT_WEIGHT = 2;
    private const BATTERY_IF_OK_TO_LIFT_WEIGHT = 1;

    public function __construct(
        private int $force
    )
    {
    }

    public function getConsumedBattery(int $weight, int $batteryCurrentLevel): int
    {
        // Force supérieure ou égale au poids du déchet => 1% de batterie
        if ($this->force >= $weight) {
            return self::BATTERY_IF_OK_TO_LIFT_WEIGHT;
        }
        
        // Sinon, la différence * 2
        $battery = ($weight - $this->force) * self::BATTERY_RATIO_TO_LIFT_WEIGHT;

        // Je ne dois pas dépasser la moitié de la batterie
        if ($battery > ($batteryCurrentLevel / 2)) {
            return self::BATTERY_IF_NOT_POSSIBLE_TO_LIFT_WEIGHT;
        }
        
        return $battery;
    }
}
  • J’ai créé des constantes dédiées pour toutes les valeurs numériques propres à la mécanique
  • Pour savoir combien de batterie va être consommée, il faut 2 informations : le poids du déchet et la batterie courante. On retrouve donc ces valeurs en paramètres.

Et pour finir la classe Robot et la méthode principale de traitement de déchets :

/**
 * @param int[] $trash
 */
public function handleTrash(array $trash): void
{
    foreach ($trash as $trashWeight) {
        if ($this->battery->isDown()) {
            break;
        }

        $this->battery->consume(
            $this->lifter->getConsumedBattery($trashWeight, $this->battery->getLevel())
        );

        $this->battery->recharge($this->speed);
    }
}
  • Au lieu d’appeler d’autres méthodes de ma classe Robot, on passe par les autres objets $this->battery et $this->lifter pour atteindre leurs méthodes.

Et les tests unitaires ?

Ce qui devient très intéressant en séparant correctement la logique dans différentes classes, c’est que je vais pouvoir aussi répartir mes tests en plusieurs fichiers : (lien vers le code à la fin)

  • BatteryTest.php
  • LifterTest.php
  • RobotTest.php

Pour bien comprendre les différents tests unitaires, la notion de dataprovider, n’hésite pas à te référer à la première version du corrigé qui explique tout ça en détails : Tests unitaires en PHP #3 : TDD avec PHPUnit et des dataprovider.

SOLID

On a vu ici le S de SOLID et ce principe qui consiste à faire en sorte qu’une classe n’ait qu’une seule responsabilité. On aurait pu pousser encore plus loin en créant une classe Trash dédiée à chaque déchet. Cette classe aurait eu une propriété weight et un getteur getWeight notamment.

Les points clés de ce premier principe SOLID :

  • Du code mieux organisé, en plusieurs fichiers, de façon logique
  • Du nommage plus naturel, moins de préfixe ou autre
  • Des propriétés très souvent privées et beaucoup de méthodes publiques
  • Des tests qui sont également répartis en plusieurs fichiers

Il reste OLID à creuser maintenant 😉

Voici 2 ressources très intéressantes permettant d’aller plus loin :

N’hésite pas à mettre en pratique ces principes avec d’autres challenges de programmation !

Le code complet de ce corrigé est disponible sur Github.


Qui a codé ce superbe contenu ?

Keep learning

Autres contenus à découvrir


Ta newsletter chaque mois

Corrigés, challenges, actualités, veille technique... aucun spam.