1. Prototype

1.1. Introduction

Le Prototype est un design pattern très simplement accessible en PHP puisque l'usage de la méthode __clone est une application de celui-ci.

Le Prototype est la capacité d'un Object de se cloner en un nouvel Object qui hérite de l'état de l'Object cloné.

A noter que le __clone crée une nouvelle référence pour l'Object copié.

Dans le cas où un Object fait référence à d'autres Objects (par exemple un Attribute "Color $color" ), cette référence est copiée, ce qui peut être problématique si l'on modifie la valeur de l'Object référencé.

1.2. Le problème

Supposons qu'on a une Class Pencil qui aurait un Attribute Color :

Pencil.php
<?php
class Pencil implements PencilInterface
{
    protected ColorInterface $color;

    public function __construct(ColorInterface $color) 
    {
        $this->color = $color;
    }

    public function getColor(): ColorInterface 
    {
        return $this->color;
    }
}

PencilInterface.php
<?php
interface PencilInterface 
{
    public function getColor(): ColorInterface;
}

ColorInterface.php
<?php
interface ColorInterface 
{
    public function getValue(): string;
}

Color.php
<?php
class Color implements ColorInterface 
{
    public function __construct($value)
    {
        $this->value = $value;
    }

    public function setValue(string $value)
    {
        $this->value = $value;
    }

    public function getValue(): string
    {
        return $this->value;
    }
}


Voici un comportement problématique :

    $color = new Color('red');
    $firstPencil = new Pencil($color);
    $clonedPencil = clone $firstPencil;
    $color->setValue('green');
    echo $clonedPencil->getColor()->getValue(); // -> 'green'
    echo $firstPencil->getColor()->getValue(); // -> 'green'


Ici, les deux instances de Pencil se retrouvent avec une couleur ayant la valeur verte. Ce que l'on ne souhaitait pas forcément : on souhaite avoir un Pencil ayant une Color 'green' et l'autre ayant une Color 'red'.
La seule façon de résoudre ce problème est de surcharger la méthode __clone afin de cloner également l'instance de Color (et donc obtenir une nouvelle référence différente de la première).

1.3. Solution

Il faut réécrire la méthode __clone de cette façon :

Pencil.php
<?php
class Pencil implements PencilInterface
{
    protected ColorInterface $color;

    public function __construct(ColorInterface $color) 
    {
        $this->color = $color;
    }

    public function getColor(): ColorInterface 
    {
        return $this->color;
    }

    public function __clone() 
    {
        $this->color = clone $this->color;
    }
}


Se faisant, si l'on réexecute le bout de code précédent, on obtient ceci

    $color = new Color('red');
    $firstPencil = new Pencil($color);
    $clonedPencil = clone $firstPencil;
    $color->setValue('green');
    echo $clonedPencil->getColor()->getValue(); // -> 'red'
    echo $firstPencil->getColor()->getValue(); // -> 'green'

Le premier crayon a bien hérité d'une couleur verte, tandis que le second a bien une couleur qui a été modifiée en rouge.

1.4. Les ValueObjects

Le ValueObject est un Object immutable, c'est à dire que ces propriétés sont initialisés qu'à un seul moment, la grande majorité du temps dans le constructeur ; il est aussi possible de faire des setters qui ne settent qu'une seule fois la valeur, ce qui peut potentiellement produire un effet de bord selon le test effectué.

1.5. Setter d'un immutable

Voici à quoi pourrait ressembler un tel setter. Reprenons la Class Pencil :
<?php
class Pencil implements PencilInterface
{
protected ?ColorInterface $color = null;

public function __construct(ColorInterface $color)
{
$this->color = $color;
}


public function setColor(?ColorInterface $color = null): PencilInterface
{
if (\is_null($this->color)) {
$this->color = $color;
}

return $this;
}

public function getColor(): ColorInterface
{
return $this->color;
}
}


Ici, on sette la couleur une seule et unique fois, mais étant donné que l'Attribute color peut également être null, il est tout à fait possible de passer une première fois dans la méthode, setter $color à null, puis repasser une autre fois et setter $color avec une instance implémentant ColorInterface.
Le principe d'un Object immutable est de ne plus être modifié après sa création, ce qui potentiellement peut toujours être le cas ici.
Eventuellement, on pourrait rajouter un Attribute indiquant si l'état est fixe ou non, mais cela deviendrait compliqué à gérer s'il fallait identifier pour chaque Attribute si son setter a été ou non appelé. En outre, il faudrait également garantir que cet Attribute n'est setté qu'une seule fois.

Donc la solution de passer par des setters plutôt que par le constructeur n'est pas une bonne idée.

1.6. Comment cloner un Object possédant un Attribute ValueObject ?

Il est difficile de lier les concepts de Prototype et de ValueObject car il faudrait créer une méthode clone ayant des paramètres similaire à ceux du __construct.
Dans le cas de l'Attribute Color, si l'on considère que c'est un ValueObject, il ne serait plus possible de modifier la Color du Pencil cloné, ce qui serait déroutant.

Donc si l'on souhaite cloner un Object possédant un Attribute ValueObject, il faut accepter que cet Attribute sera immutable d'un clone à l'autre.

1.7. Factory+Pool

Pour créer un Pencil avec une Color immutable, on peut passer par une Factory, voire mélanger le pattern design Pool et Factory afin de conserver les différents Pencil créés.

On peut envisager de n'avoir qu'une seule Factory nommé PencilFactory (on pourrait éventuellement avoir une deuxième Factory nommée ColorFactory qui stockerait les différents instances de Color).

Ici, la PencilPoolFactory garde en mémoire les différentes instance de Color créées puis fabrique et retourne des instances de Pencil. Etant donné qu'il change d'état, il n'est donc pas possible de passer par une méthode statique.

<?php
class PencilPoolFactory
{
    /**
    * ColorInterface[]
    */
    protected array $colors = array();

    public function has(string $key)
    {
        return isset($this->colors[$key]);
    }

    public function get(string $colorValue): ColorInterface
    {
        return $this->colors[$colorValue];
    }
  
     public function add(ColorInterface $color, string $colorValue)
     {
         $this->colors[$colorValue] = $color;
     }

    public function create(string $colorValue)
    {
        if (!$this->has($colorValue)) {  
            $this->add(new Color($colorValue), $colorValue);
        }

        return new Pencil($this->get($colorValue));
    }
}


Voici comment l'utiliser :

    $pencilPoolFactory = new PencilPoolFactory();
    $redPencil = $pencilPoolFactory->create('red');
    $greenPencil = $pencilPoolFactory->create('green');

    echo $redPencil->getColor()->getValue(); // 'red'
    echo $greenPencil->getColor()->getValue(); // 'red'


🧙‍♂️️Le PencilPoolFactory mélange deux design patterns mais cela produit un mauvais pattern (le mot antipattern est un peu trop populaire et désigne aujourd'hui des choses eux-mêmes populaires).
D'une première raison : ce PencilPoolFactory fabrique des instances de Color ET des instances de Pencil, il a donc deux responsabilités, ce n'est pas la faute de la combinaison des deux design patterns.
D'une deuxième raison : le PencilPoolFactory garde en mémoire les instances de Color, ce qui lui donne une nouvelle responsabilité car l'utilisateur sera tenté de faire également un $pencilPoolFactory->get('color').


1.7.1 Résolution du problème

Il faut découper le PencilPoolFactory en trois : PencilFactory, ColorFactory et PencilPool. Chacune de ces Classs aura une seule responsabilité.

1.7.1.1 PencilPool avec un Attribute $colorFactory

* Le PencilFactory aura un Attribute Singleton : $pencilPool.
* Le PencilPool aura un Attribute Singleton : $colorFactory.

PencilFactory.php
<?php
class PencilFactory
{
    PencilPoolInterface $pencilPool;

    public function __construct(
        PencilPoolInterface $pencilPool
    ) 
    {
        $this->pencilPool = $pencilPool
    }

    public function create(string $colorValue): PencilInterface
    {
        $color = $this->pencilPool->get($colorValue);

        return new Pencil($color);
    }

    protected function getColor(string $colorValue): ColorInterface
    {
        return $this->pencilPool->get($colorValue);
    }
}



Cette solution est dérangeante. Le PencilPool dispose d'un Attribute autre que celui pour lequel il est dédié, ce qui est déroutant car il stocke des références de ColorInterface ET de PencilFactoryInterface.

La solution inverse semble plus pertinente : le PencilFactory possède un Attribute $pencilPool qui ferait office de stockage des nouvelles instances.

1.7.1.2 ColorFactory avec un Attribute $pencilPool

Le PencilPool se réduit ici à son strict nécessaire : il stocke des instances implémentant ColorInterface

PencilPool.php
<?php
class PencilPool implements PencilPoolInterface
{
    /**
    * ColorInterface[]
    */
    protected array $colors = array();

    public function has(string $key)
    {
        return isset($this->colors[$key]);
    }

    public function get(string $colorValue): ColorInterface
    {
        return $this->colors[$colorValue];
    }
  
    public function add(ColorInterface $color, string $colorValue)
    {
        $this->colors[$colorValue] = $color;
    }
}

Et le PencilFactory possède un Attribute $pencilPool.

PencilFactory.php
<?php
class PencilFactory implements PencilFactoryInterface
{
    protected PencilPool $pencilPool;
    
    public function create(string $colorValue)
    {
        if (!($pencilPool->has($colorValue)) {
            $color = new Color($colorValue);
            $pencilPool->set($color, $colorValue);
        } else {
            $color = $pencilPool->get($colorValue);
        }

        return $color;
    }
}

La PencilFactory demande à sa $pencilPool si la Color existe, auquel cas elle le retourne. Si ce n'est pas le cas, elle la crée et le transmet au pencilPool pour que ce dernier le stocke.

Eventuellement, on pourrait imaginer une nouvelle Class plus complexe, avec un nom un peu ambigue comme PencilManager.

Cette Class ferait le lien entre le PencilFactory, le ColorFactory et le PencilPool.
Cela garantirait que les trois Class seraient simples et immutables (on ne les modifierait plus une fois conçues).

Sauf qu'on imagine bien que le PencilManager finirait par avoir plusieurs responsabilités, même si elles sont déléguées à d'autres Class, ce qui n'est en soi pas un problème si on fait en sorte que ce Manager ne représente que la fusion des trois autres.
Ce ne serait qu'une Facade, qui ne devrait faire qu'une chose : retourner un Pencil en fonction d'une valeur de couleur.




2. Le design pattern Singleton et l'implémentation des services

Avec les frameworks modernes, on passe notre temps à manipuler des Singletons car tout ce qu'on nomme service en est en fait un.

Un singleton n'est rien d'autre qu'une classe que l'on ne peut instancier qu'une et une seule fois par exécution. Si l'on désire créer une nouvelle instance et qu'il en existe déjà une, c'est celle-ci qui sera utilisée, magique donc !

2.1. La classe Singleton

2.1.1 Schema DML


2.1.2 Exemple de la classe Singleton

Voici une façon d'écrire une classe qui implémente le design pattern Singleton :

<?php
class Singleton
{
    private static $mySelf = null;

    private function __construct(){} // Empêche toute création d'instance.
    private function __clone(){} // Empêche toute copie d'instance.

    public static function getInstance()
    {
        if (self::$mySelf === null) {
            self::$mySelf = new Singleton();
        }
        
        return self::$mySelf;
    }
}

2.1.3 Exemple d'utilisation (attention, ça se complique !)

A l'usage, lorsque l'on cherche à créer deux instances, voici ce qu'il se passe:

<?php
$firstSingleton = Singleton::getInstance();
$secondSingleton = Singleton::getInstance();

2.1.4 Pourquoi l'usage de self et du static ?

Puisque que la classe n'est pas instanciée au moment de l'appel à getInstance, $this n'existe pas encore. Et comme $this n'existe pas, le seul moyen de faire appel à l'attribut mySelf est de passer par du static ::.

2.2. La classe SingletonMaker

2.2.1 Introduction

C'est tout de même très étrange comme façon de créer une instance, c'est un peu trop fantastique ce qu'il se passe, non ? On dirait que l'on hack la façon dont on doit utiliser les classes, notamment, il me semble vraiment étrange pour une classe de s'autocréer.
On pourrait éventuellement passer par un créateur de Singleton pour gérer tout ceci de façon plus claire, je vous propose donc de créer le SingletonMaker.

2.2.2 Exemple de la classe SingletonMaker

A noter que la classe aurait pu s'appeler SingletonFactory, mais elle s'appelle SingletonMaker pour éviter l'ambiguité avec le design pattern Factory, dont le code ci dessous n'est pas représentatif.

class Singleton
{

}

class SingletonMaker
{
    private static $singleton = null;

    public static function getSingleton()
    {
        if (self::$singleton === null) {
            self::$singleton = new Singleton();
        }

        return self::$singleton;
    }
}

C'est déjà un peu plus normal ce qu'il se passe ici, on passe par une classe qui nous retourne le Singleton en le créant si besoin.

2.2.3 Exemple d'utilisation

Rien de très différent de la fois d'avant.

<?php
$firstSingleton = SingletonMaker::getSingleton();
$secondSingleton = SingletonMaker::getSingleton();

2.3. Notre gestionnaire de services - la classe ServiceManager

Quelque part, nous venons de créer un gestionnaire de services, si on renomme un peu les choses, voici ce qu'on pourrait obtenir :

2.3.1 Proposition d'implémentation du ServiceManager

En renommant Singleton en MyFirstService, en créant une seconde classe MySecondService et en renommant le SingletonMaker par ServiceManager, on pourrait écrire ceci :

class MyFirstService
{

}

class MySecondService
{

}

class ServiceManager
{
    private static $services = [];

    public static function get(string $serviceClassName)
    {
        if (!isset(self::$services[$serviceClassName])) {
            self::$services[$serviceClassName] = new $serviceClassName();
        }

        return self::$services[serviceClassName];
    }
}


A l'usage, voici comment on récupérerait nos services :

$myFirstService = ServiceManager::get('MyFirstService');
$mySecondService = ServiceManager::get('MySecondService');

2.3.2 Appelation de services plus abstraites

Et eventuellement faire une appelation de services différente que l'on recalculerait à la volée.

Ex : 'myFirstService' comme nom du service et new ucfirst("myFirstService")() pour créer l'instance (pour rappel le ucfirst met en majuscule la première lettre d'une chaine de caractères).

class MyFirstService
{

}

class ServiceManager
{
    private static $services = [];

    public static function get(string $serviceName)
    {
        $serviceClassName = ucfirst($serviceName);

        if (!isset(self::$services[$serviceName])) {
            self::$services[$serviceName] = new $serviceClassName();
        }

        return self::$services[$serviceName];
    }
}


A l'usage, on pourrait l'appeler comme ceci :

$myService = ServiceManager::get('myFirstService');

2.3.2.1 Apparté

Derrière, on peut appeler ça un containeur de services, puis déclarer dans un fichier de configuration yml, xml ou ce que l'on souhaite le nom des services publics (haha).

3. Static Factory

3.1. Introduction

FactoryMethod/AbstractFactory/StaticFactory/SimpleFactory : peu importe le nom qu'on donne à ces designs patterns, il faut surtout comprendre que le but d'une classe suffixée Factory est celui-ci : construire et retourner des objets.
Que cette classe Factory étende une AbstractFactory, qu'elle contienne une ou plusieurs méthodes de fabrication, qu'elle soit elle même générée par une autre Factory, l'essentiel est qu'elle crée des instances.
En outre, si le Factory a ce rôle de constructeur, il paraitrait logique que tous les 'new' ne soient présents que dans de telles classes dans tout le code source du projet.

Les méthodes de création des classes suffixées Factory sont justement nommées FactoryMethod. "(create(...)/factory(...)/make(...)")


3.2. Intention

Fournir une interface permettant de créer des familles d'objets liés ou dépendants sans spécifier leurs classes concrètes. A la différence de l'AbstractFactory qui définit des Factory ayant plusieurs méthodes de fabrication, la StaticFactory définit des Factory qui n'ont qu'une seule méthode statique de fabrication pour un ensemble d'objets.

🧙‍♂️️Ce que je comprends plutôt de ce qu'est le StaticFactory :
A la différence de l'AbstractFactory, on ne définit ici qu'une seule classe Factory ; étant donné qu'il n'y a qu'une seule classe Factory, il n'est donc pas nécessaire de mettre du code dans une classe abstraite (donc le fichier 'AbstractSomethingFactory.php' n'existerait pas). Le design pattern StaticFactory est un cas particulier de l'AbstractFactory.


3.3. Exemple concret

Supposons que l'on souhaite afficher à l'écran le nom du repas à prendre en fonction du moment de la journée (par exemple pour une personne qui n'aurait plus toute sa tête). Pour simplifier, on ne considère que trois types de repas : le petit déjeuner, le déjeuner et le diner.

3.3.1 Code procédural

En procédural, voici ce que l'on pourrait écrire :

index.php
<?php
$currentHour = (int) date('H');

if ($currentHour < 12) {
    echo 'breakfeast';
} elseif ($currentHour < 16) {
    echo 'lunch';
} else {
    echo 'dinner';
}

Ici, il y a un problème fondamental : la logique heure/type de repas n'est pas récupérable en l'état dans les autres fichiers de l'application, il faudra alors dupliquer ce code. Si un nouveau repas est introduit (comme le petit déjeuner), il faudra chercher dans l'ensemble du code cette logique pour la changer.

Une fonction aurait tout sa place ici.
Par exemple :

<?php
    function getMealName($currentHour): string
    {
        if ($currentHour < 12) {
            $mealName = 'breakfeast';
        } elseif ($currentHour < 16) {
            $mealName = 'lunch';
        } else {
            $mealName = 'dinner';
        }

        return $mealName;
    }
    echo getMealName((int) date('H'));

Cette fonction résout le problème principal : la logique heure/type de repas est à un seul endroit.
Là où cela se complique, c'est si l'on souhaite retourner autre chose que le nom du repas, par exemple les horaires de celui-ci, on pourrait éventuellement faire une deuxième fonction comme celle-ci :

    function getMealTime($currentHour): string
    {
        if ($currentHour < 12) {
            $mealTime = '0h - 12h';
        } elseif ($currentHour < 16) {
            $mealTime = '12h - 16h';
        } else {
            $mealTime = '> 16h';
        }

        return $mealTime;
    }
    echo getMealTime((int) date('H'));

Mais cette fonction génère à nouveau un problème de duplication.
Non seulement on duplique à nouveau la logique heure/type de repas mais s'il faut rajouter un type de repas, il faudra modifier les deux fonctions.

La solution ici est de renvoyer une donnée plus complexe qui contiendrait plusieurs informations. Cette donnée pourrait contenir à la fois le nom du repas mais également les horaires de celui-ci. Il existe des solutions pour cela : on peut retourner un tableau, une structure particulière (ex: json/xml/yaml) ou bien un Objet. Le tableau ou la structure pose une difficulté : le client de la fonction doit connaitre le nom des clefs d'accès aux différentes informations. Dans le cas d'un Objet, le client a accès aux méthodes publiques de celui-ci pour accéder à une représentation des différentes informations.

3.3.2 En POO

3.3.3 Création des modèles Meal

En POO, on pourrait représenter chaque type de repas de la façon suivante :

MealInterface.php
<?php 

namespace App\Model\Meal;

interface MealInterface
{
    public function getName();
}

AbstractMeal.php
<?php
namespace App\Model\Meal;

abstract class AbstractMeal implements MealInterface
{
    protected string $name;

    public function getName(): string
    {
        return $this->name;
    }
}

Breakfeast.php
<?php
namespace App\Model\Meal;

class Breakfast extends AbstractMeal implements MealInterface
{
    public function __construct()
    {
        $this->name = 'breakfast';
    }
}

Lunch.php
<?php
namespace App\Model\Meal;

class Lunch extends AbstractMeal implements MealInterface
{
    public function __construct()
    {
        $this->name = 'lunch';
    }
}

Dinner.php
<?php
namespace App\Model\Meal;

class Dinner extends AbstractMeal implements MealInterface
{
    public function __construct()
    {
        $this->name = 'dinner';
    }
}

3.3.4 Mise en application sans Factory

Maintenant que les différentes classes sont prêtes, on peut les utiliser :

MealController.php
<?php 
    namespace App\Meal\Controller;

    use App\Meal\Model\Breakfast;
    use App\Meal\Model\Lunch;
    use App\Meal\Model\Dinner;

    class MealController 
    {
        public function index()
        {
            $currentHour = (int) date('H');
            
            if ($currentHour < 12) {
                $meal = new Breakfast();
            } elseif ($currentHour < 16) {
                $meal = new Lunch();
            } else {
                $meal = new Dinner();
            }

            echo $meal->getName();
        }
    }


Ici, le Controller doit avoir connaissance des différentes classes pour faire son affichage. Ce qui pose un problème dans le cas d'une modification des types de repas : il faut modifier l'ensemble des fichiers qui manipulent les classes concrètes (là où il y a des 'use').

Le but de la Factory est d'éviter cela afin que le Controller n'ait plus à gérer les différents modèles. Tout ce qu'il doit savoir est qu'il manipule une instance qui implémente l'interface MealInterface. Cette interface a une méthode getName(), commune à toutes les classes qui l'implémentent.

3.3.5 Avec l'Abstract Factory

Voici une implémentation possible de la classe MealFactory, elle reprend la logique du MealController :

MealFactory.php
<?php
    namespace App\Meal\Factory;

    use App\Meal\Model\MealInterface;
    use App\Meal\Model\Breakfast;
    use App\Meal\Model\Lunch;
    use App\Meal\Model\Dinner;

    class MealFactory implements MealFactoryInterface
    {
        public static function create(int $hour): MealInterface
        {
            if ($hour < 12) {
                $meal = new Breakfast();
            } elseif ($hour < 16) {
                $meal = new Lunch();
            } else {
                $meal = new Dinner();
            }

            return $meal;
        }
    }


Elle implémente l'interface MealFactoryInterface. Cette interface permettra dans le cas où l'on souhaite créer une deuxième MealFactory (par exemple pour un pays où les repas seraient différents) de mettre en place le mécanisme d'AbstractFactory (ie. plusieurs classes de type MealFactory (FrenchMealFactory/AfricanMealFactory)).

MealFactory.php
<?php
    namespace App\Meal\Factory;

    use App\Meal\Model\MealInterface;

    interface MealFactoryInterface
    {
        public function create(int $hour): MealInterface;
    }


Voici désormais ce que fera le MealController :

MealController.php
<?php 
    namespace App\Meal\Controller;

    use App\Meal\Factory\MealFactory;

    class MealController
    {
        public function index()
        {
            $mealFactory = MealFactory::create((int) date('H'));
            echo $meal->getName();
        }
    }


🧙‍♂️️L'usage des méthodes static est assez déconseillée car le Controller a ici une dépendance avec la classe concrète de la Factory. Si on souhaite utiliser une autre MealFactory, il faudra modifier dans l'ensemble du code cet usage. Mieux vaut que MealFactory implémente l'interface MealFactoryInterface. On injectera ensuite la Factory dans le constructeur des classes qui en dépendent.



3.4. Sources

📖️️https://designpatternsphp.readthedocs.io/en/latest/Creational/StaticFactory/README.html

📖️️https://waytolearnx.com/2020/02/design-patterns-static-factory-en-php.html