Vous êtes sur la page 1sur 10

Design patterns de création

Alexandre Brabant*
2024

Tout de suite quelques ressources pour compléter ce cours :

ˆ les "Objects Calisthenics", qui sont de petits exercices et règles pour améliorer la qualité de
votre code. Vous pouvez regarder cette vidéo en français.

ˆ les design patterns au complet, sur un site appelé refactoring.guru en anglais, certes, mais
en accès libre et gratuit ;

ˆ des ouvrages payants, cette fois, mais qui sont des bibles pour le développement logiciel :
Clean Code (dès maintenant !) et Clean Architecture (si vous avez déjà un peu d'expé-
rience).

Enn, on ne peut pas ignorer le livre fondateur du "gang of four" : Design Patterns : Elements
of Reusable Object-Oriented Software.

1 Qu'est-ce qu'un design pattern ?

En développement logiciel, la majorité des fonctionnalités que l'on vous demandera de coder ré-
pondent à des problèmes très communs et forcément déjà résolus.

Dans ce cas, vous pouvez être tenté de réutiliser votre code, ou alors le code d'une librairie open
source partagée et maintenue par la communauté.

En vous professionnalisant, vous utiliserez souvent des frameworks logiciels qui fournissent un en-
semble d'outils répondant à des problèmes communs : Symfony pour le langage PHP, ou encore
Django pour le langage Python.

Les frameworks sont très puissants, mais ils nécessitent en contrepartie un temps d'apprentissage
assez long. D'autre part, si vous intégrez une équipe qui n'utilise pas le même langage ou le même
framework que le vôtre, vous aurez quelques dicultés d'adaptation.

Les design patterns sont des solutions typiques à des problèmes communs en développement logi-
ciel : ils ne sont pas une implémentation concrète d'une solution à un problème, mais plutôt une
* Adaptation d'un cours de Mickaël Andrieu (mis à jour le 16/02/2024)
stratégie à appliquer pour le résoudre de façon élégante et maintenable.

Ils sont souvent confondus avec les algorithmes, qui eux aussi fournissent une solution à des pro-
blèmes communs. Mais là où l'algorithme est à appliquer exactement sur le modèle d'une recette
de cuisine, les design patterns sont à adapter à votre code : un design pattern peut être implémenté
de multiples façons.

Les designs patterns, répondent à trois grands types de problèmes :


ˆ la création d'objets ;
ˆ la gestion des relations entre objets ;
ˆ et enn, la gestion des responsabilités et comportements d'un objet.

2 Utilisez les design patterns de créations

En développement logiciel, un anti-pattern est une erreur courante de conception. Les Anglais
parlent parfois de "code smell" (en français, "du code qui pue" !).

2.1 Le design pattern Factory Method

Imaginez que l'entreprise pour laquelle vous travaillez assure essentiellement du transport de mar-
chandises par la route.

Dans votre application, vous aurez probablement développé la logique business du transport di-
rectement 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 ?

2.1.1 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écique d'une classe. La classe chargée de
cette responsabilité est appelée Factory, et les objets créés sont souvent appelés products.

Page 2
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 diérente de l'interface.
3. La Factory dépend elle aussi d'une interface qui dénit la méthode en charge de la création
du produit. Souvent, les développeurs l'appellent factory ou create.

Bien que l'on nomme cette classe Factory, la création de produits n'est généralement pas la
responsabilité principale de cette classe, qui contient d'autres fonctions plus utiles au com-
portement attendu de ces objets.

Mettons en pratique le design pattern en mettant en place l'interface pour dénir les dié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()
{
// ...
}

Page 3
// 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énir 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énir 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 :

Page 4
<?php

class TruckTransportFactory extends AbstractTransportFactory


{
public function createTransport()
{
return new Truck();
}
}

class ShipTransportFactory extends AbstractTransportFactory


{
public function createTransport()
{
return new Ship();
}
}
Nous avons ainsi rendu notre système de livraison complètement indépendant de chaque mode de
transport, et rendu possible l'ajout d'un nombre inni de modes de livraison. Pour cela, il nous
sut de créer deux nouvelles classes : le type de transport et son "transport handler", dont il
faudra seulement implémenter la méthode createTransport.

2.2 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 chiers, à 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 modier 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 !

2.2.1 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.

Page 5
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. Enn, 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).
L'opération de copie est plus rapide que l'opération d'instanciation pour les "gros" objets.

Nous créons un objet sans passer par une instanciation, il n'y aura donc pas d'opérateur new uti-
lisé : c'est un bon indice pour identier ce design pattern.

En PHP, l'implémentation de ce design pattern est native, notamment grâce à l'opérateur


clone et la fonction "magique" __clone(). Cette dernière permet au développeur de modier
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.

C'est un besoin réel, et cette fonctionnalité s'appelle même le multi-boutique.

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

Page 6
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énit 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;
}
}

Page 7
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());


// renvoie false car les thèmes des deux magasins ne sont pas les mêmes

2.3 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").

Le design pattern Singleton est un design pattern de création 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.

Petit rappel : un objet est une 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 eet, lorsque les informations d'authentication à la base de données ont été fournies, quel est
l'intérêt de repasser les informations d'authentication et/ou de recréer la connexion à chaque fois
que nous aurons besoin de manipuler des informations ?

Page 8
2.3.1 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.
ˆ 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);
}

// Ne pas oublier d'indterdire l'appel à l'opérateur "clone"


// pour s'assurer qu'il n'y aura qu'une seule instance :
private function __clone() {}

public static function getInstance()


{
if (self::$instance === null) {
self::$instance = new self();
}

return self::$instance;
}

public function getConnection()


{
return $this->connection;
}

Page 9
}
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();

Avantages d'un Singleton :


ˆ 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.

Inconvénients :
ˆ Passer par un Singleton peut cacher des problèmes de conception : pour la classe Database,
si les constantes PDO_DSN, USER et PASSWD n'ont pas été dénies "avant" l'appel à la fonction
getInstance(), l'objet sera invalide.
ˆ Il est dicile de tester un Singleton, puisqu'une fois qu'il est construit, il ne peut plus être
changé ! Compliqué alors de vérier le comportement de l'objet en fonction de diérentes
valeurs des constantes PDO_DSN, USER et PASSWD.

À moins que vous n'ayez réellement besoin d'avoir une seule instance d'un objet dans votre appli-
cation, il n'est pas conseillé d'implémenter un Singleton. Il existe une autre méthode permettant
d'accéder à un objet de façon globale. . .

Page 10

Vous aimerez peut-être aussi