Archives par étiquette : flyweight

PHP – Le design pattern Flyweight

Soyons honnêtes, si vous êtes un développeur web, les chances pour que vous ayez à utiliser le design pattern Flyweight en PHP sont assez minces, en raison du fait que Flyweight est surtout utile quand vous avez un très grand nombre d’objets en RAM et qu’il vous est important d’en économiser l’instanciation, ce qui arrive rarement lorsque vous développez (bien !) une application web. Vous le trouverez davantage dans des domaines comme les traitements de texte ou les jeux vidéo qui utilisent des environnements multi-tâches (multithreaded). En PHP, le multithreading est à ce jour utilisable seulement en ligne de commande, pas dans un environnement de serveur Web.

Cela ne signifie pas pour autant que ce design pattern n’est JAMAIS utilisé, il est par exemple implémenté dans l’ORM Doctrine, pour la gestion des types.

Le principe de fonctionnement du design pattern

Flyweight signifie « poids mouche » en bon français, mais pourquoi donc ? Parce que l’idée est de garder les objets dont nous allons nous servir les plus petits possible, comme la catégorie de boxeurs à laquelle ce nom fait référence…petits certes, mais efficaces !

Ces objets vont avoir deux états:

  • un état intrinsèque, qui ne dépend pas du contexte de l’objet
  • un état extrinsèque

Le premier réside dans la classe même tandis que le second lui sera fourni lors de l’appel à une de ses méthodes.

Prenons comme objet poids mouche un produit culturel donc l’état intrinsèque, immuable, sera uniquement composé de son nom. La classe ProduitCulturel se conformera à une interface assez simple, dont l’unique méthode afficher prend en paramètre l’état extrinsèque, c’est à dire tout ce qui est susceptible de venir enrichir l’état intrinsèque de l’objet. Le contexte est passé sous forme de tableau pour garder l’exemple le plus simple possible mais un objet dédié à ces contextes serait largement préférable.

interface ProduitCulturelInterface
{
    public function affiche(array $contexte): void;
}

class ProduitCulturel implements ProduitCulturelInterface
{
    protected string $nom;

    public function __construct(string $nom)
    {
        $this->nom = $nom;
    }

    public function affiche(array $contexte): void
    {
        echo 'Nom: ' . $this->nom . PHP_EOL;

        foreach ($contexte as $cle => $valeur) {
            echo ucfirst($cle) . ': ' . $valeur . PHP_EOL;
        }
    }
}

Pour gérer nos poids mouches, nous allons devoir utiliser une fabrique, car c’est à elle que s’adressera le code client pour obtenir ces objets. Il ne doit en aucun cas réaliser lui-même les instanciations ! Voilà ladite fabrique, assez rudimentaire: elle possède une méthode getProduitCulturel qui vérifie si une instance est déjà disponible avec le nom donné. Si c’est le cas, elle la renvoie, évitant une nouvelle instanciation, sinon elle instancie la classe ciblée et la range sagement dans son tableau interne, qui sert de « parc à poids mouches » comme le nomme si joliment le livre du GoF.

class FabriqueDeProduitsCulturels
{
    protected array $produits = [];

    public function getProduitCulturel(string $nom): ProduitCulturel
    {
        if (array_key_exists($nom, $this->produits)) {
            $produit = $this->produits[$nom];
        } else {
            $produit = new ProduitCulturel($nom);
            $this->produits[$nom] = $produit;
        }

        return $produit;
    }
}

Libre à vous d’en faire une version statique, pour ma part je ne suis pas tellement en faveur des appels statiques car ils complexifient les tests unitaires.

Notre code client va utiliser une classe Commande (aucun rapport avec le design pattern du même nom !) qui proposera dans son interface publique une méthode permettant l’ajout de produits culturels. Ces produits liés à la commande seront stockés dans un tableau du même nom tandis que leur contexte ira dans un tableau dédié, au même indice. Ainsi nous avons d’un côté les poids mouches et de l’autre le contexte auquel ils sont liés. Le constructeur de notre classe Commande se verra injecter son unique dépendance, la fabrique !

Afficher les produits d’une commande consistera tout simplement à boucler sur les produits et à invoquer leur méthode d’affichage, qui requiert le contexte en paramètre.

class Commande
{
    protected FabriqueDeProduitsCulturels $fabrique;

    protected array $produits = [];

    protected array $contextes = [];

    public function __construct(FabriqueDeProduitsCulturels $fabrique)
    {
        $this->fabrique = $fabrique;
    }

    public function ajouteProduit(string $nom, array $contexte)
    {
        $produit = $this->fabrique->getProduitCulturel($nom);
        $this->produits[] = $produit;
        $this->contextes[] = $contexte;
    }

    public function afficheProduits()
    {
        foreach ($this->produits as $index => $produit) {
            $produit->affiche($this->contextes[$index]);
        }
    }
}

Voici comment tout cela sera utilisé:

$commande = new Commande(new FabriqueDeProduitsCulturels());

$commande->ajouteProduit('Livre', ['prix' => 10.99, 'titre' => '1984', 'auteur' => 'George Orwell']);
$commande->ajouteProduit('Disque', ['prix' => 19.99, 'titre' => '1984', 'auteur' => 'Van Halen']);

$commande->afficheProduits();

Pour résumer…

Flyweight n’est utile qui si vous avez un nombre trèèèèès important d’objets de type semblable (si c’est le cas, demandez-vous déjà si c’est bien normal !) car son but est d’économiser la mémoire durant le temps d’exécution d’un processus (généralement multitâches)

Il met en jeu une fabrique qui sera chargée d’instancier le cas échéant les objets poids mouches et des classes concrètes contenant des états intrinsèques. Une fois l’objet obtenu par l’utilisation de la fabrique, il ne pourra plus être modifié (il est immuable) mais le client pourra lui transmettre son état extrinsèque par l’appel d’une de ses méthodes. Ici nous avons utilisé une méthode terriblement triviale qui réalise un simple affichage mais nous aurions pu tout aussi bien invoquer un service web pour commander les produits sur une API externe.

Ce billet a été réalisé en s’inspirant librement du livre du GoF et d’un ouvrage paru chez ENI Éditions.