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

Design Pattern en PHP #1 Singleton et Factory

Découverte de 2 design pattern en PHP : Singleton et Factory

→ Corrigé du Challenge : YOLO les Pizzaïolos

Dans ce corrigé, basé sur le challenge PIZZAS, je vais utiliser 2 design pattern pour structurer mon code : Singleton et Factory. On va voir, pas à pas, pourquoi et comment les utiliser.

Pour faciliter la lecture de l’article, le code complet peut être consulté sur Github.

Analyse du problème

Dans ce challenge, il est question de pizzaiolos qui déterminent chacun de façon différente le prix d’une pizza. Une pizza est composée de différents ingrédients. Au départ, les ingrédients ont des prix bien définis.

Pour y voir plus clair, on peut représenter le déroulé du challenge de cette façon :

2 structures de données émergent de cette première analyse :

  • Les prix des ingrédients sont les mêmes pour tout le monde, pour chaque pizzaïolo, pour chaque pizza. Il faudrait donc qu’on les gère globalement.
  • Les pizzaiolos ont des actions communes, puis des actions spécifiques. Il faudrait donc qu’on structure ça de façon à partager des fonctionnalités, tout en laissant de la place à ces actions spécifiques.

Avec un peu d’habitude, on peut déceler 2 design pattern, que l’on va prendre le temps de présenter :

  • Singleton : pour les prix des ingrédients, on a besoin d’instancier les données de l’énoncé, puis d’y accéder à d’autres endroits de notre code. On va en avoir besoin pour chaque pizza, mais on ne va pas recréer un objet à chaque fois, lui repasser l’ensemble des prix, etc. L’objectif du Singleton est de renvoyer en permanence la même instance. Ce qui fait que l’objet n’existe qu’une seule fois.
  • Factory : pour les pizzaiolos, on veut donc définir une structure commune et pouvoir dynamiquement instancier le bon pizzaiolo. L’objectif de la Factory est de créer une classe dédiée à la création d’un pizzaiolo, qui sera chargé de vérifier que la classe existe bien, puis qu’il l’instanciera. De cette façon, on appelle les pizzaiolos toujours de la même façon.

Mettons tout ça en pratique !

Design pattern SINGLETON

On cherche à créer une classe qui contient les prix des ingrédients, appelons la « IngredientsPrices » :

namespace Challenges\PIZZAS;

final class IngredientsPrices
{
    /**
     * Elements propres au Pattern SINGLETON --------
     */
    protected function __construct() {}
    
    private static ?IngredientsPrices $instance = null;

    public static function getInstance(): IngredientsPrices
    {
        if (is_null(self::$instance)) {
            self::$instance = new static();
        }

        return self::$instance;
    }
    /**
     * ----------------------------------------------
     */

    private array $prices;

    public function setPrices(array $informations)
    {
        $this->prices = [];

        foreach ($informations as $information) {
            [$name, $price] = explode(':', $information);

            $this->prices[$name] = (int) $price;
        }
    }

    public function getPricesForIngredients(array $ingredients): array
    {
        return array_map([$this, 'getPriceByName'], $ingredients);
    }

    public function getPriceByName(string $name): int
    {
        if (empty($this->prices)) {
            throw new \Exception('Les prix n\'ont pas encore été déclarés.');
        }

        return $this->prices[$name] ?? 0;
    }
}

Les premiers éléments de la classe sont propres à la logique du Singleton. Quelques explications :

  • Le __construct est protégé, de façon à ne pas pouvoir l’appeler. Pour « construire » le singleton on utilisera la méthode statique getInstance.
  • Cette méthode getInstance vérifie si l’instance est nulle (is_null(self::$instance)). Si c’est le cas alors une nouvelle instance de la classe est attribuée à $instance.
  • getInstance finit par retourner $instance, soit qui vient d’être instanciée, soit qui l’était déjà.

Les autres propriétés et méthodes :

  • $prices contient les prix, dans un tableau, sous la forme clé valeur : [‘mozarella’ => 3, …]
  • La méthode setPrices permet de parser les éléments de l’énoncé pour remplir $prices. Elle sera à appeler dans le programme principal, une seule fois, pour initialiser les valeurs.
  • Les méthodes getPricesForIngredients et getPricesByName vont servir aux pizzaiolos pour récupérer les prix des ingrédients de leur pizza.

Ces méthodes sont utilisées juste après et vont prendre tout leur sens.

Design Pattern FACTORY

On commence par créer la classe parente Pizzaiolo, celle-ci contient à la fois :

  • Des éléments mutualisés (propriété et méthode)
  • Une méthode abstraite qui servira de structure aux enfants
namespace Challenges\PIZZAS;

abstract class Pizzaiolo
{
    protected array $prices;

    abstract function calculPrice(): int;

    public function setPrices(array $ingredients): void
    {
        $this->prices = IngredientsPrices::getInstance()->getPricesForIngredients($ingredients);
    }
}

A noter que la classe elle-même est abstraite, car elle n’a pas de raison d’être instanciée directement. Ce seront toujours les enfants (chacun des pizzaiolos) qui seront instanciés.

La méthode setPrices récupère une liste d’ingrédients [‘oignons’,’tomates’,’basilic’], qu’elle transforme en une liste de prix [1,2,3]. Elle fait appel à notre Singleton. A chaque Pizzaiolo, j’irais donc chercher les prix de mes ingrédients, sans avoir besoin de repasser en paramètre la liste première qui définit les prix.

Puis je crée les classes enfants, qui porteront chacune le nom du pizzaiolo : Donatello, Leonardo, Michelangelo et Raphael. Ci-dessous l’exemple avec Donatello, qui est contenu dans un fichier Donatello.php, rangé dans un dossier dédié PIZZAIOLOS :

namespace Challenges\PIZZAS\PIZZAIOLOS;

use Challenges\PIZZAS\Pizzaiolo;

final class Donatello extends Pizzaiolo
{
    public function calculPrice(): int
    {
        return max($this->prices) * 5;
    }
}

Cette classe ne contient rien d’autre, le reste de la logique du Pizzaiolo est déjà dans la classe parente.

Les 3 autres classes, disponibles ici, sont organisées de la même façon. Chacune ayant sa propre logique de calcul dans la méthode calculPrice.

Et enfin la classe PizzaioloFactory qui nous sert à instancier les pizzaiolos à la volée :

namespace Challenges\PIZZAS;

final class PizzaioloFactory
{
    public static function engage(string $name): Pizzaiolo
    {
        $classPizzaiolo = 'Challenges\\PIZZAS\\PIZZAIOLOS\\' . ucfirst($name);

        if (! class_exists($classPizzaiolo)) {
            throw new \Exception('Ce Pizzaiolo n\'est pas paramétré');
        }

        return new $classPizzaiolo;
    }
}

Elle ne contient qu’une seule méthode, permettant « d’engager » un pizzaiolo, c’est à dire retourner une instance du pizzaiolo souhaité :

  • On commence par construire le chemin vers la classe.
  • On vérifie que cette classe existe bien.
  • Puis on l’instancie dynamiquement et on la retourne en même temps.

Cette méthode va fonctionner pour nos 4 classes de Pizzaiolos.

Voyons voir maintenant comment mettre tout ça en musique !

La classe PIZZA

Une pizza est composée d’ingrédients, et est préparée par un Pizzaiolo, qui définit ensuite son prix.

On peut la coder de cette façon :

namespace Challenges\PIZZAS;

final class Pizza
{
    private array $ingredients;

    private Pizzaiolo $pizzaiolo;
    
    public function __construct(string $informations)
    {
        $this->ingredients = explode(',', $informations);
    }

    public function setPizzaiolo(string $name): void
    {
        $this->pizzaiolo = PizzaioloFactory::engage($name);
        $this->pizzaiolo->setPrices($this->ingredients);
    }

    public function getPrice(): int
    {
        return $this->pizzaiolo->calculPrice();
    }
}

Quelques explications :

  • On a donc une propriété Pizzaiolo. Même si le Pizzaiolo sera précisé, la propriété peut être typée avec la classe parente.
  • La méthode setPizzaiolo fait donc appel à notre Factory, et initialise notre propriété pizzaiolo.
  • Dans la foulée, on définit les prix (via la méthode contenue dans la classe Pizzaiolo).
  • Et une autre méthode, getPrice, permet de faire appel à calculPrice de chacun des pizzaiolos. On est sûr que cette méthode existe dans chaque des enfants de part la déclaration abstraite dans la classe parente.

La classe Addition

Pour répondre au challenge, il faut retourner la somme des prix de chacune des pizzas. On peut regrouper cette logique dans une classe dédiée :

namespace Challenges\PIZZAS;

final class Addition
{
    private int $total = 0;
    private array $pizzas = [];

    public function __construct(array $informationsPizzas, array $informationsPizzaiolos)
    {
        foreach ($informationsPizzas as $key => $informationsPizza) {
            $pizza = new Pizza($informationsPizza);
            $pizza->setPizzaiolo($informationsPizzaiolos[$key]);

            $this->pizzas[] = $pizza;
            $this->total += $pizza->getPrice();
        }
    }

    public function getTotal(): int
    {
        return $this->total;
    }
}

Quelques explications :

  • Le constructeur reçoit en paramètres les informations du challenge, à savoir les compositions des pizzas (dans un tableau) et les pizzaiolos correspondant (dans un autre tableau).
  • On boucle donc sur les pizzas et on instancie une nouvelle Pizza à chaque itération.
    • On définit ensuite le pizzaiolo associé
    • On stocke la pizza dans une propriété dédiée (pas forcément utile pour juste résoudre le challenge)
    • On calcule le prix de la pizza et on incrémente le total
  • Et un getteur à la fin pour récupérer ce total

Le programme principal

En raccourcissant un peu, celui-ci tient en 2 lignes :

IngredientsPrices::getInstance()->setPrices($ingredients);
$total = (new Addition($pizzas, $pizzaiolos))->getTotal();

Conclusion

Au premier abord, les design pattern peuvent paraitre assez complexes, voir abstraits. Le meilleur moyen de les « apprivoiser » est de le mettre en œuvre sur des exemples concrets. On a vu ici comment mettre en place un Singleton et une Factory pour résoudre le challenge.

  • Singleton : renvoyer en permanence la même instance. Ce qui fait que l’objet n’existe qu’une seule fois. Idéal pour mutualiser des informations et des opérations.
  • Factory : cadrer la création de classes d’une même « famille » qui seront alors instanciées dynamiquement, selon d’autres informations ou déroulé du programme principal.

N’hésite pas à consulter les liens ci-dessous qui t’apporteront des informations complémentaires.

Tout le code présenté a bien sûr été testé unitairement. Tous les tests unitaires sont disponibles sur Github.

Va + loin

  • Le challenge AVENGERS_1 est parfait pour mettre en pratique le pattern Factory.
  • Il est possible de créer une classe Singleton qui contiendra la logique du Singleton et permettra donc à d’autres classes d’en hériter. On peut alors avoir rapidement plusieurs Singleton dans une même application. Quelques explications ici.
  • Le pattern Factory en vidéo.
  • Notre Track sur les tests unitaires.

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.