• 6 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 16/02/2024

Utilisez les design patterns créationnels

Maîtrisez le design pattern Factory Method

Imaginez que l’entreprise pour laquelle vous travaillez assure essentiellement du transport de marchandises par la route.

Dans votre application, vous aurez probablement développé la logique business du transport directement dans la classe Truck ou Car.

Mais voilà, l’entreprise souhaite maintenant livrer en Australie, par bateau.

Le transport par bateau va obéir à d’autres contraintes (taxes, coût, etc.) que le transport par la route. On peut s’en sortir en ajoutant quelques conditions dans le code, mais que se passera-t-il le jour où l’entreprise voudra également assurer du transport par avion ou par drone ?

Appliquez le design pattern Factory Method

Le design pattern Factory Method consiste à remplacer les appels à la construction d’objets à l’aide de l’opérateur  new  par l’appel d’une méthode spécifique d'une classe. La classe chargée de cette responsabilité est appelée Factory, et les objets créés sont souvent appelés products.

Le design pattern Factory Method repose sur une classe responsable de la création d'objets (l'usine) qui va créer les objets (dont on se réfère en tant que
Le design pattern Factory Method
  1.  Le type de  transport (le Product) dépend d’une interface.

  2. Chaque type de transport est donc une implémentation concrète et différente de l’interface.

  3. La Factory dépend elle aussi d’une interface qui définit la méthode en charge de la création du produit. Souvent, les développeurs l’appellent factory ou create.

Mettons en pratique le design pattern en mettant en place l’interface pour définir les différents types de transport :

<?php

interface Transport
{
    public function deliverOrder();
}
<?php

class Truck implements Transport
{
    /**
     * Chaque type de transport a ses spécificités de livraison
     */
    public function deliverOrder()
    {
        // ...
    }

    // et toutes les autres méthodes utiles à cet objet!
}
<?php

class Ship implements Transport
{
    public function deliverOrder()
    {
        // ...
    }
    
    // et toutes les autres méthodes utiles à cet objet!
}

Ensuite, l'interface qui va définir le comportement de la  Factory :

<?php

interface TransportFactory
{
    public function createTransport() : Transport;
    
    public function deliver();
}

La création d’un objet n’est généralement pas la responsabilité principale de ce type de classe. Ici, c’est la fonction deliver qui aura le plus de valeur "métier", et elle doit être commune à toutes nos implémentations concrètes.

Pour modéliser cela, nous pouvons définir une classe abstraite :

<?php

abstract class AbstractTransportFactory implements TransportFactory
{
    abstract public function createTransport() : Transport;
    
    /**
     * cette fonction ne doit pas être surchargée
     */ 
    final public function deliver()
    {
        return $this->createTransport()->deliverOrder();
    }
}

Nous aurons donc une  Factory  par type de transport, qui retournera le mode de transport :

<?php

class TruckTransportFactory extends AbstractTransportFactory
{
    public function createTransport()
    {
        return new Truck();
    }
}

class ShipTransportFactory extends AbstractTransportFactory
{
    public function createTransport()
    {
        return new Ship();
    }
}

Quels changements avons-nous faits, finalement ? :-°

Nous avons rendu notre système de livraison complètement indépendant de chaque mode de transport, et rendu possible l’ajout d’un nombre infini de modes de livraison. Pour cela, il nous suffit de créer deux nouvelles classes : le type de transport et son "transport handler", dont il faudra seulement implémenter la méthode createTransport.

Maîtrisez le design pattern Prototype

La construction d’un objet peut prendre énormément de temps et de ressources, par exemple si elle fait appel à des fichiers, à des appels réseau ou encore à des informations que l’on retrouve en base de données. Par souci d’optimisation de performance, on peut préférer copier un objet existant et le modifier plutôt qu’en recréer un en partant de zéro.

Le problème est que ce n’est pas si simple de copier un objet à l’identique !

Appliquez le design pattern Prototype

Le design pattern Prototype (aussi appelé "Clone") consiste à permettre la copie d’un objet sans créer de dépendances fortes entre l’original et sa copie.

Pour cela, nous allons déléguer la création du clone d’un objet... à l’objet lui-même. :ange:

Le design pattern Prototype ajoute un nouveau contrat sur un objet: une fonction qui définira comment créer la copie ou le clone.
Le design pattern Prototype
  1. Tout d’abord, une interface nommée Prototype  contient l’unique contrat sur la fonction de copie : ici la fonction clone().

  2. Chaque objet implémentant cette interface sera donc copiable.

  3. Enfin, la responsabilité d’exécuter l’opération de la copie est déléguée à un Client.

De cette façon, même si l’objet est capable de "se" cloner, nous avons respecté le principe de séparation des responsabilités (SRP) que nous avions abordé dans une partie précédente de ce cours.

L’opération de copie est plus rapide que l’opération d’instanciation pour les "gros" objets.

En PHP, l’implémentation de ce design pattern est native, notamment grâce à l’opérateur  clone  et la fonction "magique" __clone(). La fonction magique  __clone()  permet au développeur de modifier l’objet copié lors de l’appel à l’opérateur clone.

Par exemple, imaginons que nous souhaitions créer une boutique en ligne complète, et seulement en changer le thème graphique.

Cela dit, l’opérateur  clone  a une limitation : toutes les propriétés de l’objet qui sont également des objets ne seront pas clonées, mais seulement assignées en tant que référence. Il faudra donc "compléter" l’opération de copie dans la fonction __clone()  :(.

À l’aide du design pattern Prototype, nous pouvons limiter les risques d’erreur lors de la copie, tout en faisant directement l’opération de changement de thème.

<?php

class Shop
{
    private $theme;

    public function getTheme() 
    {
        return $this->theme;
    }
    
    public function setTheme(Theme $theme)
    {
        $this->theme = $theme;
    }
    
    public function __clone()
    {
        // maintenant qu'il est différent
        // on pourra mettre à jour le theme seulement sur la copie
        $this->theme = clone $this->theme;
    }
}

Puis l'interface qui définit le contrat de "copie" de la boutique (le "Client") :

<?php

use Shop;
use Theme;

interface ShopClonable
{
    public function cloneWithTheme(Shop $shop, Theme $theme) : Shop;
}

Et maintenant, un cas d’utilisation dans une classe dont ce serait la responsabilité (un Client) :

<?php

class ShopCloner implements ShopClonable
{
    public function cloneWithTheme(Shop $shop, Theme $theme)
    {
        $newShop = clone $shop;
        $newShop->setTheme($theme);
        
        return $newShop;
    }
}

Et voici une mise en pratique :

<?php

$shop = new ShopFactory->createShop(/* ... */); // beaucoup d'opérations
$shopCloner = new ShopCloner();
$theme = new Theme('modern-theme'); // par exemple

$newShop = $shopCloner->cloneWithTheme($shop, $theme);

var_dump($shop->getTheme() === $newShop->getTheme()); // false, les thèmes des deux magasins ne sont pas les mêmes

Maîtrisez le design pattern Builder

Le design pattern Builder vous permet de construire des objets complexes étape par étape. Cette stratégie vous permet donc de produire différents types d’objets à l’aide du même plan de construction.

Le design pattern Builder va déléguer la responsabilité de la construction d'un objet à un ensemble de classes appelées
Le design pattern Builder
  1. Le builder  est une interface qui contiendra une fonction pour chaque fonctionnalité de l’objet à construire.

  2. L’implémentation de chaque  Builder doit être capable de retourner un objet.

  3. Une classe appelée Director  est capable de piloter un builder pour créer un objet de façon spécifique : nous y reviendrons plus tard.

Imaginez par exemple que vous deviez développer un site web pour un grand constructeur automobile.

Il vous demande de permettre aux utilisateurs de pouvoir configurer et visualiser les différents types de véhicules qu’il propose. Lors de la configuration d’une voiture, on peut changer la couleur, l’intérieur, la motorisation et de nombreuses autres options.

Appliquez le design pattern Builder

Si l’on représente cela sous forme d’objet Car, pour disons une  Renolt 12  (toute similitude avec un véhicule existant est fortuite :ange:) :

<?php

class Renolt12 extends Car
{
    private $color;
    
    private $motorization;
    
    // la clim
    private $airConditionner;
    
    private $someOtherOption;
    
    // ...
    
    public function __construct($color, $motorization, $airConditionner, $someOtherOption, /* ... */)
    {
        //
    }
}

$myCar = new Renolt12('blue', '75cc', true, false, /* ... */);

Ce code est valide, pourtant, il n’est pas très facile à maintenir et à faire évoluer :

  • D’abord, plus on aura d’options, plus le constructeur va grossir, et il faudra changer toutes les instanciations de Renolt12 dans le logiciel.

  • Ensuite, même si nous configurons des valeurs par défaut et que nous souhaitons modifier la toute dernière option, il faudra remettre toutes les valeurs précédentes en constructeur.

  • Et entre nous, le septième paramètre qui vaut  true , il correspond à quoi, déjà ? :o

  • Enfin, sachant que la Renolt13 va bientôt sortir et possède les mêmes options, va-t-on devoir dupliquer tout ce code dans une nouvelle classe ? :(

Le but est d’organiser la construction d’un objet selon une série d’étapes (monter le moteur choisi, appliquer la peinture, installer la climatisation...).

Voici à quoi pourrait ressembler l’interface CarBuilder :

<?php

interface CarBuilder
{
    public function paintCar(string $color) : CarBuilder;
    
    public function mountEngine(string $motorization) : CarBuilder;
    
    public function installAirConditionner() : CarBuilder;
    
    public function getCar() : Car;
}

Et la classe Renolt12Builder : 

<?php

class Renolt12Builder implements CarBuilder
{
    private $car;
    
    public function __construct()
    {
        $this->car = new Renolt12();
    }
    
    public function paintCar(string $color) : CarBuilder
    {
        $this->car->setColor($color);
    }
    
    public function mountEngine(string $motorization) : CarBuilder
    {
        $this->car->setMotorization($motorization);
    }
    
    // ...
    
    public function getCar() : Car
    {
        return $this->car;
    }
}

Et son utilisation pour créer la  Renolt12  de nos rêves : :D

<?php

$myRenolt12 = (new Renolt12Builder())
    ->paintCar('yellow')
    ->mountEngine('250cc')
    ->installAirConditionner()
    ->getCar()
;

Et voilà ! C’est plus facile à lire, à maintenir et à faire évoluer. Le fait qu'une méthode retourne l'instance modifiée permet d'enchaîner l'appel aux méthodes. Il s'agit également d'un design pattern appelé  Façade  .

Pensez-vous que nous avons terminé ?

"Passez-moi le directeur !"

En complément des Builders, le design pattern suggère de créer des classes dites  Director lorsque l’on souhaite appliquer à un builder un ensemble de fonctions dans un ordre précis.

Nous aurions pu utiliser le design pattern Builder pour construire un objet Pizza, et dans ce cas, les étapes doivent être faites dans un ordre précis, n’est-ce pas ? :)

Revenons à la classe Director. Si la voiture  Renolt12  est proposée en éditionOpenClassroomsSwag  et Rich, nous pourrions imaginer ce type d’implémentation :

<?php

class Director
{
    private $builder;

    public function __construct(Builder $builder)
    {
        $this->builder = $builder;        
    }

    public function createOpenClassroomsEdition()
    {
        return $this->builder->paintCar('purple')
            ->installAirConditionner()
            ->getCar()
        ;
    }

    public function createSwagEdition()
    {
        return $this->builder->paintCar('gray')
            ->installAirConditionner()
            ->mountEngine('60cc')
            ->getCar()
        ;
    }

    public function createRichEdition()
    {
        return $this->builder->paintCar('gold')
            ->installAirConditionner()
            ->mountEngine('600cc')
            ->getCar()
        ;
    }
}

Nous déléguons donc la création au  Directeur, qui se chargera d’appeler le bon "constructeur" pour créer l’objet de nos rêves :

<?php

$director = new Director(new Renolt12Builder());
// Je suis riche !!!

$myCar = $director->createRichEdition();

// Je veux la renolt13 édition Swag, s'il vous plaît!

$otherDirector = new Director(new Renolt13Builder());

$swagCar = $otherDirector->createSwagEdition();

Et puisque c’est le directeur qui décide, si une option n’est pas disponible pour la Renolt13, il ne sera juste pas possible de la demander. :magicien:

Singleton, design ou anti-pattern ?

Jusque-là, nous avons parlé des design patterns comme de solutions à des problèmes communs de conception logicielle.

Pourtant, le design pattern Singleton est souvent décrié et reconnu comme étant plutôt un "anti-pattern" (que vous pourriez aussi appeler une "fausse bonne idée" :D).

Le design pattern Singleton est un design pattern créationnel qui applique une contrainte sur une classe : il ne pourra y avoir qu’une et une seule instance possible de cette classe tout en fournissant un accès global à cette instance de classe.

Le meilleur exemple d’utilisation d’un Singleton en PHP, c’est lorsque l’on souhaite créer une  connexion à une base de données.

En effet, lorsque les informations d’authentification à la base de données ont été fournies, quel est l’intérêt de repasser les informations d’authentification et/ou de recréer la connexion à chaque fois que nous aurons besoin de manipuler des informations ? :o

Le design pattern Singleton consiste à rendre innaccessible le constructeur et à passer par une fonction publique qui, une fois construit, redonnera toujours le même objet.
Le design pattern Singleton

Implémentez un Singleton

Comme nous pouvons le voir dans le schéma UML précédent, deux conditions sont requises pour créer un Singleton :

  • Il faut rendre le constructeur inaccessible. En PHP, il faut que la fonction __construct  soit déclarée privée, et l’on ne pourra donc plus instancier l’objet à l’aide de l’opérateur new .

  • Enfin, il faut pouvoir récupérer une instance de l’objet à l’aide d’une fonction publique statique. Cette fonction appellera le constructeur privé et conservera l’objet créé dans une propriété privée statique. Une fois appelée, la fonction privée de construction ne sera jamais rappelée !

Reprenons l’exemple de la base de données en créant une classe Database à l’aide d’un Singleton :

<?php

use PDO;

class Database
{
    private static $instance = null;

    // permet de faire des requêtes SQL à l'aide de PDO
    // @doc https://www.php.net/manual/intro.pdo.php
    protected $connection;

    private function __construct()
    {
        $this->connection = new PDO(PDO_DSN, USER, PASSWD);
    }

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

        return self::$instance;
    }

    public function getConnection()
    {
        return $this->connection;
    }
}

Et un exemple d’utilisation :

<?php

PDO_DSN = 'mysql:host=localhost;dbname=test';
USER = 'root';
PASSWD = 'password';

$connection = Database::getInstance()->getConnection();
$stmt = $connection->prepare('SELECT * FROM users');
$users = $stmt->execute();

Que se passe-t-il si l’on essaie de cloner l’objet ? Nous avons vu que c’était possible avec le design pattern Prototype. :euh:

En effet, il va falloir aussi rendre privée cette fonction particulière ! Complétons la classeDatabase :

<?php

use PDO;

class Database
{
    private static $instance = null;
    protected $connection;

    private function __construct()
    {
        $this->connection = new PDO(PDO_DSN, USER, PASSWD);
    }


    // INTERDIT l'appel à l'opérateur "clone" 
    private function __clone() {}


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

        return self::$instance;
    }

    public function getConnection()
    {
        return $this->connection;
    }
}

Comme nous pouvons le voir, un Singleton a quelques avantages :

  • Il assure qu’il n’y aura qu’une seule instance de classe.

  • Puisque la création passe par une fonction publique statique, il est possible de récupérer l’objet partout dans le projet.

  • La récupération de l’objet est peu coûteuse, car on le ne construit qu’une seule fois.

Mais il a de sérieux défauts :

  • Il viole le principe SOLID SRP : une classe ne devrait avoir qu’une et une seule responsabilité.

  • Passer par un Singleton peut cacher des problèmes de conception : pour la classeDatabase, si les constantes PDO_DSNUSER   et  PASSWD n’ont pas été définies "avant" l’appel à la fonction getInstance(), l’objet sera invalide :waw:.

  • Il est difficile de tester un Singleton, puisqu’une fois qu’il est construit, il ne peut plus être changé ! Compliqué alors de vérifier le comportement de l’objet en fonction de différentes valeurs des constantes PDO_DSNUSER  et PASSWD.

Exercez-vous !

Dans ce mini-projet, vous allez mettre en place le système de conception de pizzas d'une pizzeria en implémentant le design pattern Builder !

Une pizza, c'est un type de pâte, une base (tomate, crème) et une série d'ingrédients. Et à partir de cela, on peut faire toutes les pizzas connues : la calzone, la reine, la 4 fromages...

Mais dans les faits, les clients ont souvent des demandes d'ajout ou de taille des ingrédients des pizzas, un peu comme la customisation des voitures que nous venons de voir précédemment...;)

Vous n'allez devoir gérer dans cet exercice que le cas où une pizza Margherita peut être demandée en formats M et XL.

Pour chacune des deux versions, un supplément "œuf" est possible.

  1. Passer au format XL coûte 2 €.

  2. Le supplément "œuf" coûte 1,5 €.

Téléchargez l'archive de l'exercice et  vous complèterez le projet existant selon les commentaires à disposition.

D'abord, regardons le projet de plus près ensemble :

Maintenant, à vos claviers ! Corrigeons l'exercice dans le screencast suivant :

En résumé

Les design patterns créationnels délèguent la création d’objets à une fonction ou classe spécifique.

  • Si les objets partagent les mêmes fonctions, mais sont de types différents, il faut privilégier le design pattern Factory Method.

  • Si vous souhaitez économiser l’instanciation d’un objet coûteux pour le modifier légèrement, vous implémenterez le design patten Prototype.

  • Si les objets à manipuler sont complexes, ou leur construction dynamique (elle dépend de nombreux paramètres passés par l’utilisateur), nous pourrons utiliser le design pattern Builder. Et si certains objets sont toujours construits ou doivent être reconstruits dans un ordre spécifique, nous pourrons utiliser des Directors  .

  • Enfin, le design pattern Singleton est particulier et présente des défauts majeurs de conception. Son usage est justifié dans certains cas très particuliers (gestion de la base de données), mais devrait être évité.

Enfin, sachez qu’il existe bien d’autres design patterns créationnels !

Dans le prochain chapitre, nous découvrirons les design patterns structuraux. Vous êtes prêt ?

Exemple de certificat de réussite
Exemple de certificat de réussite