T: / Corrigés des challenges / PHP
Découverte de 2 design pattern en PHP : Singleton et Factory
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.
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 :
Avec un peu d’habitude, on peut déceler 2 design pattern, que l’on va prendre le temps de présenter :
Mettons tout ça en pratique !
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 :
Les autres propriétés et méthodes :
Ces méthodes sont utilisées juste après et vont prendre tout leur sens.
On commence par créer la classe parente Pizzaiolo, celle-ci contient à la fois :
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é :
Cette méthode va fonctionner pour nos 4 classes de Pizzaiolos.
Voyons voir maintenant comment mettre tout ça en musique !
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 :
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 :
En raccourcissant un peu, celui-ci tient en 2 lignes :
IngredientsPrices::getInstance()->setPrices($ingredients);
$total = (new Addition($pizzas, $pizzaiolos))->getTotal();
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.
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.
Other content to discover