POO en PHP : design pattern Factory

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

→ Corrigé du Challenge : L’armée de Daenerys

Dans le challenge GOT_1, l’objectif est de déterminer la composition de l’armée de Daenerys en fonction du nombre d’ennemis à affronter. Pour cela, elle a 3 types de troupes à disposition : des Dragons, des Immaculés, et des Dothrakis (tu ne connais pas ces termes, t’inquiète pas c’est pas grave, promis, on en parle plus après 😉 ) Pour chaque troupe, les mécaniques sont semblables.

L’idée ici va donc être de structurer correctement les troupes avec un ensemble de classes pour pouvoir les générer à la volée et appliquer ces mécaniques. Pour cela on va utiliser le design pattern Factory.

Au programme de ce corrigé :

Qu’est ce que le design pattern Factory ?

Le design pattern Factory permet de créer des objets sans spécifier la classe à instancier. C’est à dire qu’on ne fait pas un « new MaClasse ». Au lieu de ça, on passe par une méthode de fabrication qui prend en charge l’instanciation de la classe et son renvoi. Cette méthode prendra généralement des paramètres en entrée pour déterminer quelle classe instancier. L’exemple le plus basique : « Factory::build(‘MaClasse’) ».

Ce pattern permet notamment de découper le code en séparant la logique de création d’objets du programme principal, rendant le système plus modulaire, plus facile à étendre et donc à maintenir. Ce challenge GOT_1 est le parfait candidat pour illustrer tout ça !

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 fichiers sont organisés :

Diagramme UML pour résoudre le challenge.

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

  • A droite, on retrouve la classe Commandant, c’est elle qui sera chargée du programme principal.
  • A gauche, on a les classes liées aux troupes.
  • Les 3 classes de nos troupes hériteront toutes d’une classe parent « Troops ».
  • Cette classe parent a des propriétés « protected », c’est à dire que les enfants peuvent y accéder mais elles ne sont pas accessibles en dehors des classes pour autant.
  • Cette classe parent a une méthode publique, un getteur classique. Et elle a surtout 2 méthodes publiques abstraites. C’est à dire que les classes enfant devront obligatoirement les déclarer. Ce sont ces 2 méthodes qui porteront les mécaniques propres à chaque troupe.
  • Enfin, la classe TroopsFactory, qui a une seule méthode publique statique. On voit dans son typage qu’elle retournera bien une instance de « Troops ». En fait, elle ne renverra jamais une instance de « Troops », mais une instance d’une classe enfant de « Troops ». « Troops » étant abstraite, il n’est de toute façon pas possible de l’instancier directement.

Les classes dédiées aux troupes et l’héritage en PHP

Voici le contenu de la classe Troops :

namespace Challenges\GOT_1;

abstract class Troops
{
    protected int $nbTroops;

    public function __construct(
        protected int $army
    ) {}

    abstract public function calculNumberOfTroops(): void;
    abstract public function getRealNumberOfTroops(): int;

    public function getNbTroops(): int
    {
        return $this->nbTroops;
    }
}

Un peu d’explications :

  • La classe est abstraite, on utilise le mot clé « abstract » dans la déclaration
  • Une propriété est déclarée en dehors du constructeur, l’autre dedans directement avec le principe de promotion de propriété dans le constructeur.
  • Les 2 déclarations des méthodes abstraites
  • Et le getter

Voici le contenu de la classe Dragons (les 2 autres seront très similaires) :

namespace Challenges\GOT_1\Troops;

use Challenges\GOT_1\Troops;

class Dragons extends Troops
{
    public function calculNumberOfTroops(): void
    {
        $troops = floor($this->army / 3);

        $troops = floor($troops / 1000);

        $this->nbTroops = (int) min([$troops, 3]);
    }

    public function getRealNumberOfTroops(): int
    {
        return $this->nbTroops * 1000;
    }
}

Un peu d’explications :

  • Le namespace est différent, nos classes enfant seront rangées dans un dossier dédié « /Troops ».
  • L’héritage se met en place grâce à « extends Troops ».
  • Les 2 méthodes abstraites dans le parent sont ici déclarées et complétées. On y retrouve les coefficients définis dans l’énoncé du challenge pour les calculs à réaliser.
  • On peut manipuler $this->army et $this->nbTroops qui sont déclarées dans le parent.
  • A noter qu’il n’y a pas de constructeur. Et dans ce cas là, c’est le constructeur du parent qui sera utilisé, avec donc l’initialisation de la propriété « army ».

Le design pattern Factory en PHP

Si on reprend le schéma UML, on a décidé de créer une méthode statique qui prend 2 paramètres :

  • Le nom de la classe, c’est à dire le nom de la troupe à créer
  • Un entier army, qui est l’entier passé dans le constructeur (cf. section précédente)

Voici le code :

class TroopsFactory
{
    public static function recruit(string $className, int $army): Troops
    {
        if (! class_exists('Challenges\\GOT_1\\Troops\\' . $className)) {
            throw new \Exception('La classe ' . $className . ' n\'existe pas');
        }

        $class = 'Challenges\\GOT_1\\Troops\\' . $className;
        $instance = new $class($army);

        // Pour PHPStan
        assert($instance instanceof Troops);
    
        return $instance;
    }
}

Un peu d’explications :

  • La fonction statique permettra d’appeler la méthode sans faire de « new TroopsFactory ».
  • On vérifie que la classe souhaitée existe bien, si ce n’est pas le cas, on lance une exception. C’est une mécanique classique dans une Factory, pour contrôler que la Factory n’instancie pas n’importe quelle classe de notre application. C’est pour ça aussi qu’on a bien rangé les classes concernées dans un dossier dédié « /Troops »
  • On met le nom de la classe (avec son chemin absolu) dans une variable
  • On instancie la classe grâce à la variable précédente. A la manière d’une « variable variable », on peut, en PHP, instancier une classe à partir d’une variable qui contient son nom.
  • $army est passé en paramètre lors de l’instanciation de la classe
  • Un assert dédié surtout à PHPStan (un analyseur statique de code pour PHP), sinon il n’arrive pas à être « sûr » que la classe retournée est bien une instance de Troops et émet toujours une erreur liée au non respect du type de retour.
  • Et on retourne l’instance nouvellement créée !

Cette méthode peut donc être utilisée pour instancier indifféremment les classes « Dragons », « Immacules » ou « Dothrakis ».

Utilisation de la Factory dans le programme principal

Voici le contenu de la classe Commandant :

class Commandant
{
    /**
     * @var int[] $composition
     */
    private array $composition = [];

    public function __construct(
        private int $army
    ) {}

    /**
     * @param string[] $troops
     */
    public function determineTroops(array $troops): void
    {
        foreach ($troops as $troopClass) {
            $troop = TroopsFactory::recruit($troopClass, $this->army);

            $troop->calculNumberOfTroops();

            $this->composition[] = $troop->getNbTroops();
            $this->army -= $troop->getRealNumberOfTroops();
        }
    }

    public function getComposition(): string
    {
        return implode('_', $this->composition);
    }
}

Un peu d’explications :

  • Une propriété « composition » servira pour stocker les informations du résultat final du challenge
  • L’utilisation de la Factory se trouve dans la méthode « detemineTroops » qui prend en paramètre un tableau de « troops » qui ne sont autres que les noms des classes à utiliser.
  • Un foreach permet d’exécuter le même code sur chaque type de troupe. Comme les troupes sont toutes constituées de la même façon grâce à l’héritage, ce code fonctionne peu importe le type de troupe traité.
  • Enfin une méthode getComposition renvoie la composition au format attendu du challenge, par exemple 2_27_1459

Et enfin, l’utilisation de cette classe Commandant :

$daenerys = new Commandant($armee);
// La liste des troupes à "recruter" pour la bataille !
$daenerys->determineTroops(['Dragons', 'Immacules', 'Dothrakis']);
echo $daenerys->getComposition();

Conclusion

Ce qui est intéressant de noter ici c’est qu’il sera très facile de créer un nouveau type de troupe. Pour cela il faudrait :

  • Créer une nouvelle classe qui hérite de « Troops ». Les méthodes abstraites dans « Troops » vont contraindre la structure de cette nouvelle classe pour que tout fonctionne correctement.
  • Bien ranger le fichier de cette nouvelle classe dans le dossier /Troops
  • Et c’est tout !
  • Grâce à la Factory, je n’ai aucun changement à réaliser dans Commandant, ni même dans TroopsFactory. Pas besoin de if imbriqués ou d’un switch dans Commandant pour gérer la bonne classe à appeler.
  • La méthode detemineTroops continuera de fonctionner si je lui passe 4 types de troupes (ou même 5, ou même 2, ou même 1, etc.).

On a ici une bonne illustration du découpage de code entre les objets spécifiques, la création de ces objets et notre programme principal.

Le design pattern Factory sera particulièrement utile dans ce genre de situation, où un objet doit être créé en fonction de configurations dynamiques, d’options ou de types qui ne sont déterminés qu’au moment de l’exécution.

Pour aller + loin :

Des challenges qui peuvent te permettre de mettre en œuvre ce pattern :

Le code complet de ce corrigé (avec notamment les 2 autres classes) peut être retrouvé sur Github.

Bonus : les tests unitaires avec Pest PHP

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

Ces tests permettent notamment de mettre en avant le concept de dataset, qui permet de passer plusieurs jeux de données à un même test. Je teste ainsi de façon très efficace les différents cas possibles et limites pour la composition de l’armée. Et je m’appuie aussi sur les jeux de données proposés par Tainix pour construire les tests !

Résultat en image :

Résultats de tests unitaires avec Pest PHP.

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.