POO en PHP : design Pattern Strategy

Présentation et implémentation du design pattern Strategy en PHP.

→ Corrigé du Challenge : Coupe du monde de rugby #1 La mêlée

Dans le challenge RUGBY_1, l’objectif est de calculer l’impact total de la mêlée, en appliquant une méthode de calcul différente pour chaque ligne. Il y a en effet 3 lignes de joueurs avec chacune un comportement différent. Pour autant chaque ligne a une fonctionnement similaire, mais un nombre de joueurs différents, et un calcul de l’impact différent.

L’idée ici est donc de structurer ces différentes méthodes de calcul avec le design pattern Strategy.

Au programme de ce corrigé :

Qu’est ce que le design pattern Strategy ?

Le design pattern Strategy permet de définir une famille d’algorithmes, de les encapsuler chacun dans une classe séparée, et de les rendre interchangeables. Cela signifie que le choix de l’algorithme peut varier dynamiquement selon le contexte d’exécution, sans que l’utilisateur du code n’ait besoin de connaître les détails de l’implémentation.

Chaque Ligne de la Mêlée aura donc sa propre stratégie de calcul d’impact.

Structures des classes en UML

On n’est pas ici sur un diagramme UML au sens strict mais il va permettre de visualiser comment les classes sont organisées :

Quelques explications sur ce schéma et quelques rappels sur la programmation orientée objet au passage :

  • A gauche, on retrouve les différentes stratégies. Elles implémentent toutes une même interface. Cela permet de s’assurer chaque classe, chaque Strategy disposera bien d’une méthode qui pourra être appelée, peu importe la stratégie utilisée.
  • A droite, on retrouve les classes liées à la structure du challenge, tout est bien découpé :
    • Scrum : la mêlée qui a 3 lignes
    • Line : la ligne qui a des joueurs et une stratégie d’impact
    • Player : le joueur qui a des caractéristiques et un impact
    • Chacune de ces classes a une méthode impact, elles vont s’appeler les unes les autres pour calculer l’impact total.

Implémentation du design pattern Strategy en PHP

On commence donc par l’interface. Une interface ne contient que les déclarations des méthodes qui seront à implémenter.

interface ImpactStrategy
{
    public function calculateImpact(Player $player): int;
}

Et chaque stratégie, une par ligne. Chaque stratégie doit implémenter la méthode calculateImpact, avec chacune sa version :

Première ligne :

class FirstLineStrategy implements ImpactStrategy
{
    public function calculateImpact(Player $player): int
    {
        return (int) floor($player->impact() * 1.5);
    }
}

Seconde ligne :

class SecondLineStrategy implements ImpactStrategy
{
    public function calculateImpact(Player $player): int
    {
        return $player->impact();
    }
}

Troisième ligne :

class ThirdLineStrategy implements ImpactStrategy
{
    public function calculateImpact(Player $player): int
    {
        return (int) floor($player->impact() * 0.75);
    }
}

Chaque ligne applique donc son coefficient. La seconde ligne n’a pas de coefficient, on retourne donc directement l’impact du joueur.

Ici le contenu de chaque version de la méthode calculateImpact est simple. Mais on pourrait bien entendu avoir des choses beaucoup plus complexes et surtout beaucoup plus éloignées d’un point de vue fonctionnel pour les différentes stratégies.

Utilisation de la Strategy dans les autres classes

Commençons pas la classe Player :

class Player
{
    public function __construct(
        public readonly int $poids,
        public readonly int $force
    ) {}

    public static function createFromText(string $informations): Player
    {
        $values = explode(':', $informations);
        return new self(
            (int) $values[0],
            (int) $values[1]
        );
    }

    public function impact(): int
    {
        return $this->poids * $this->force;
    }
}

Un peu d’explications :

  • On utilise la promotion de propriétés dans le constructeur
  • Le parsing est fait au travers d’une méthode statique qui retourne une instance de la classe elle même. Cela permet de bien séparer le parsing du constructeur, et donc de ne pas lier le fait que les données arrivent sous la forme d’une chaine de caractères avec la structure de la classe.
  • La méthode impact retourne le produit du poids et de la force, c’est le même calcul pour chaque joueur. C’est au niveau de la ligne qu’aura lieu l’application du coefficient.

Voici la classe Line, c’est à ce niveau qu’est utilisée le pattern Strategy :

class Line
{
    /**
     * @var Player[] $players
     */
    private array $players = [];
    private ImpactStrategy $strategy;

    /**
     * @param string[] $playersInformations
     */
    public function __construct(array $playersInformations, ImpactStrategy $strategy)
    {
        foreach ($playersInformations as $playerInformations) {
            $this->players[] = Player::createFromText($playerInformations);
        }

        $this->strategy = $strategy;
    }

    public function impact(): int
    {
        $impact = 0;

        foreach ($this->players as $player) {
            $impact += $this->strategy->calculateImpact($player);
        }

        return $impact;
    }
}

Un peu d’explications :

  • Dans le constructeur, le premier paramètre est un tableau de joueur, on utilisera la méthode createFromTexte pour parser les informations.
  • Le second paramètre du constructeur est typé selon l’interface. C’est un des points clés de ce design pattern ! Cela permettra d’accepter n’importe quelle ImpactStrategy. On ne passera donc jamais en paramètre l’interface, mais bien une classe qui l’implémente, par exemple FirstLineStrategy.
  • Dans la méthode impact() on fait donc appel au calcul d’impact de la stratégie en fonction de chaque joueur de la ligne

Puis la mêlée, la classe Scrum :

class Scrum
{
    public function __construct(
        private Line $firstLine,
        private Line $secondLine,
        private Line $thirdLine,
    ) {}

    public function impact(): int
    {
        return $this->firstLine->impact() + $this->secondLine->impact() + $this->thirdLine->impact();
    }
}

Un peu d’explications :

  • Encore de la promotion de propriétés dans le constructeur pour instancier les 3 Line
  • La méthode impact() retourne ici la somme des impacts des 3 lignes
  • On aurait pu créer un tableau de lignes mais une mêlée (dans notre contexte) a toujours 3 lignes organisées de cette façon. Ce n’était donc pas nécessaire.

Et enfin, le programme principal :

// $line1, $line2 et $line3 sont les données du challenge
$scrum = new Scrum(
    new Line($line1, new FirstLineStrategy),
    new Line($line2, new SecondLineStrategy),
    new Line($line3, new ThirdLineStrategy),
);

// Impact total
$scrum->impact();

Le code complet est disponible sur Github.

Conclusion

On peut se demander pourquoi on passe des « new FirstLineStrategy » en argument du constructeur, et pourquoi ne fait pas par exemple :

new Line($line1, 'FirstLineStrategy');

// Pour faire ensuite dans le constructeur de Line
$strategy = new $strategyName;

En passant directement l’instance en paramètres, on bénéficie de ces 2 avantages :

  • Sécurité de type : Notre constructeur de Line attend comme second paramètre un type « ImpactStrategy ». Si on passe autre chose, on aura tout de suite une erreur. En passant le nom de la classe, on aurait typé « string », sans beaucoup de contrôle donc.
  • Découplage : Ce principe consiste à réduire la dépendance entre les classes. L’idée est de rendre les « composants » du système les moins dépendants les uns à des autres, de manière à ce qu’un changement à un endroit du système ne contraigne pas à réaliser des changements à un autre endroit. Cela rend l’ensemble plus modulaire, plus facile à comprendre, à maintenir, à tester, à étendre, etc. C’est vraiment une pratique à respecter et à appliquer.

Pour conclure, on a vu comment le design pattern Strategy permet de créer une famille d’algorithme qui ont un comportement similaire, de les organiser correctement dans différentes classes, et de les utiliser avec d’autres classes qui en ont besoin. Quelques cas pratiques pour lesquels il peut être judicieux d’utiliser le design pattern Strategy (à adapter bien sûr selon le contexte de TON code et de TON besoin) :

  • Systèmes de paiement : choisir dynamiquement entre différentes méthodes de paiement
  • Algorithmes de tri : choisir un type de tri en fonction du format ou du volume de données
  • Gestion des remises : appliquer différentes mécaniques de remise selon le profil du client ou de la commande
  • Validation des données d’un formulaire : Appliquer différents contrôles selon le type de données saisies (prénom, email, date, infos bancaires, etc.)
  • Recommandations de contenus : selon le statut, l’historique d’un utilisateur, récupérer des contenus à proposer selon différentes méthodes et algorithmes
  • Etc.

Pour t’entrainer sur le design pattern Strategy en PHP, tu peux par exemple l’appliquer sur le challenge Pôle Express, en définissant des stratégies pour chaque type d’élément d’une commande.

Bonus : les tests unitaires avec Pest PHP

Le code a été entièrement testé avec Pest PHP. Les tests sont disponibles sur Github également.


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.