Mis à jour le lundi 8 janvier 2018
  • 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les design patterns

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Depuis les débuts de la programmation, tout un tas de développeurs ont rencontré différents problèmes de conception. La plupart de ces problèmes étaient récurrents. Pour éviter aux autres développeurs de buter sur le même souci, certains groupes de développeurs ont développé ce qu'on appelle des design patterns (ou masques de conceptions en français). Chaque design pattern répond à un problème précis. Comme nous le verrons dans ce chapitre, certains problèmes reviennent de façon récurrente et nous allons utiliser les moyens de conception déjà inventés pour les résoudre.

Ce chapitre est donc divisé en plusieurs sous-parties où chacune répond à un problème précis. Nous procèderons ainsi par étude de cas en posant le problème puis en le résolvant grâce aux moyens de conception connus.

Petit plus de ce chapitre : nous découvrirons les classes anonymes ! Il s'agit de classes ne possédant pas de nom, destinées à n'être utilisées qu'une seule fois dans un contexte précis. Comme vous le verrez, elles sont bien adaptées dans certaines situations !

Laisser une classe créant les objets : le pattern Factory

Le problème

Admettons que vous venez de créer une application relativement importante. Vous avez construit cette application en associant plus ou moins la plupart de vos classes entre elles. À présent, vous voudriez modifier un petit morceau de code afin d'ajouter une fonctionnalité à l'application. Problème : étant donné que la plupart de vos classes sont plus ou moins liées, il va falloir modifier un tas de chose ! Le pattern Factory pourra sûrement vous aider.

Ce motif est très simple à construire. En fait, si vous implémentez ce pattern, vous n'aurez plus denewà placer dans la partie globale du script afin d'instancier une classe. En effet, ce ne sera pas à vous de le faire mais à une classe usine. Cette classe aura pour rôle de charger les classes que vous lui passez en argument. Ainsi, quand vous modifierez votre code, vous n'aurez qu'à modifier le masque d'usine pour que la plupart des modifications prennent effet. En gros, vous ne vous soucierez plus de l'instanciation de vos classes, ce sera à l'usine de le faire !

Voici comment se présente une classe implémentant le pattern Factory :

<?php
class DBFactory
{
  public static function load($sgbdr)
  {
    $classe = 'SGBDR_' . $sgbdr;
    
    if (file_exists($chemin = $classe . '.class.php'))
    {
      require $chemin;
      return new $classe;
    }
    else
    {
      throw new RuntimeException('La classe <strong>' . $classe . '</strong> n\'a pu être trouvée !');
    }
  }
}

Dans votre script, vous pourrez donc faire quelque chose de ce genre :

<?php
try
{
  $mysql = DBFactory::load('MySQL');
}
catch (RuntimeException $e)
{
  echo $e->getMessage();
}

Exemple concret

Le but est de créer une classe qui nous distribuera les objets PDO plus facilement. Nous allons partir du principe que vous avez plusieurs SGBDR, ou plusieurs BDD qui utilisent des identifiants différents. Pour résumer, nous allons tout centraliser dans une classe.

<?php
class PDOFactory
{
  public static function getMysqlConnexion()
  {
    $db = new PDO('mysql:host=localhost;dbname=tests', 'root', '');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    return $db;
  }
  
  public static function getPgsqlConnexion()
  {
    $db = new PDO('pgsql:host=localhost;dbname=tests', 'root', '');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    return $db;
  }
}

Ceci vous simplifiera énormément la tâche. Si vous avez besoin de modifier vos identifiants de connexion, vous n'aurez pas à aller chercher dans tous vos scripts : tout sera placé dans notre factory !

Écouter ses objets : le pattern Observer

Le problème

Dans votre script est présente une classe qui s'occupe de la gestion d'un module. Lors d'une action précise, vous exécutez une ou plusieurs instructions. Celles-ci n'ont qu'une seule chose en commun : le fait qu'elles soient appelées car telle action s'est produite. Elles ont été placées dans la méthode « parce qu'il faut bien les appeler et qu'on sait pas où les mettre ». Il est intéressant dans ce cas-là de séparer les différentes actions effectuées lorsque telle action survient. Pour cela, nous allons regarder du côté du pattern Observer.

Le principe est simple : vous avez un objet observé et un ou plusieurs autre(s) objet(s) qui l'observe(nt). Lorsque telle action survient, vous allez prévenir tous les objets qui l'observent. Nous allons, pour une raison d'homogénéité, utiliser les interfaces prédéfinies de la SPL. Il s'agit d'une librairie standard qui est fournie d'office avec PHP. Elle contient différentes classes, fonctions, interfaces, etc. Vous vous en êtes déjà servi en utilisantspl_autoload_register() par exemple.

Attardons-nous plutôt sur ce qui nous intéresse, à savoir deux interfaces : SplSubject et SplObserver.

La première interface,SplSubject, est l'interface implémentée par la classe dont l'objet observé est issu. Elle contient trois méthodes :

  • attach(SplObserver $observer): méthode appelée pour ajouter un objet observateur à notre objet observé.

  • detach(SplObserver $observer): méthode appelée pour supprimer un objet observateur.

  • notify(): méthode appelée lorsque l'on aura besoin de prévenir tous les objets  observateurs que quelque chose s'est produit.

L'interfaceSplObserverest l'interface implémentée par les différents observateurs. Elle ne contient qu'une seule méthode qui est celle appelée par l'objet observé dans la méthodenotify(): il s'agit deupdate(SplSubject $subject).

Voici un diagramme mettant en œuvre ce design pattern (voir la figure suivante).

Diagramme modélisant une mise en place du design pattern Observer
Diagramme modélisant une mise en place du design pattern Observer

Nous allons maintenant imaginer le code correspondant au diagramme. Commençons par la classe dont seront issus les objets observés :

<?php
class Observee implements SplSubject
{
  // Ceci est le tableau qui va contenir tous les objets qui nous observent.
  protected $observers = [];
  
  // Dès que cet attribut changera on notifiera les classes observatrices.
  protected $nom;
  
  public function attach(SplObserver $observer)
  {
    $this->observers[] = $observer;
  }
  
  public function detach(SplObserver $observer)
  {
    if (is_int($key = array_search($observer, $this->observers, true)))
    {
      unset($this->observers[$key]);
    }
  }
  
  public function notify()
  {
    foreach ($this->observers as $observer)
    {
      $observer->update($this);
    }
  }
  
  public function getNom()
  {
    return $this->nom;
  }
  
  public function setNom($nom)
  {
    $this->nom = $nom;
    $this->notify();
  }
}

Voici deux classes dont les objets issus seront observateurs :

<?php
class Observer1 implements SplObserver
{
  public function update(SplSubject $obj)
  {
    echo 'Observer1 a été notifié ! Nouvelle valeur de l\'attribut <strong>nom</strong> : ', $obj->getNom();
  }
}

class Observer2 implements SplObserver
{
  public function update(SplSubject $obj)
  {
    echo 'Observer2 a été notifié ! Nouvelle valeur de l\'attribut <strong>nom</strong> : ', $obj->getNom();
  }
}

Ces deux classes font exactement la même chose, ce n'était qu'à titre d'exemple basique que je vous ai donné ces exemples, afin que vous puissiez vous rendre compte de la syntaxe de base lors de l'utilisation du pattern Observer.

Pour tester nos classes, vous pouvez utiliser ce bout de code :

<?php
$o = new Observee;
$o->attach(new Observer1); // Ajout d'un observateur.
$o->attach(new Observer2); // Ajout d'un autre observateur.
$o->setNom('Victor'); // On modifie le nom pour voir si les classes observatrices ont bien été notifiées.

Vous pouvez constater qu'ajouter des objets observateurs de cette façon peut être assez long si on en a cinq ou six. Il y a une petite technique qui consiste à pouvoir obtenir ce genre de code :

<?php
$o = new Observee;

$o->attach(new Observer1)
  ->attach(new Observer2)
  ->attach(new Observer3)
  ->attach(new Observer4)
  ->attach(new Observer5);

$o->setNom('Victor'); // On modifie le nom pour voir si les classes observatrices ont bien été notifiées.

Pour effectuer ce genre de manœuvres, la méthodeattach()doit retourner l'instance qui l'a appelée (en d'autres termes, elle doit retourner$this).

Exemple concret

Regardons un exemple concret à présent. Nous allons imaginer que vous avez, dans votre script, une classe gérant les erreurs générées par PHP. Lorsqu'une erreur est générée, vous aimeriez qu'il se passe deux choses :

  • Que l'erreur soit enregistrée en BDD.

  • Que l'erreur vous soit envoyée par mail.

Pour cela, vous pensez donc coder une classe comportant une méthode chargée d'attraper l'erreur et d'effectuer les deux opérations ci-dessus. Grave erreur ! Ceci est surtout à éviter : le rôle de votre classe est d'intercepter les erreurs, et non de les gérer ! Ce sera à d'autres classes de s'en occuper : ces classes donneront naissance à des objets qui vont observer l'objet gérant l'erreur et une fois notifiés, ils vont effectuer l'action pour laquelle ils ont été conçus. Vous voyez un peu la tête qu'aura le script ?

Vous êtes capables de le faire tout seul. Voici la correction.

ErrorHandler : classe gérant les erreurs
<?php
class ErrorHandler implements SplSubject
{
  // Ceci est le tableau qui va contenir tous les objets qui nous observent.
  protected $observers = [];
  
  // Attribut qui va contenir notre erreur formatée.
  protected $formatedError;
  
  public function attach(SplObserver $observer)
  {
    $this->observers[] = $observer;
    return $this;
  }
  
  public function detach(SplObserver $observer)
  {
    if (is_int($key = array_search($observer, $this->observers, true)))
    {
      unset($this->observers[$key]);
    }
  }
  
  public function getFormatedError()
  {
    return $this->formatedError;
  }
  
  public function notify()
  {
    foreach ($this->observers as $observer)
    {
      $observer->update($this);
    }
  }
  
  public function error($errno, $errstr, $errfile, $errline)
  {
    $this->formatedError = '[' . $errno . '] ' . $errstr . "\n" . 'Fichier : ' . $errfile . ' (ligne ' . $errline . ')';
    $this->notify();
  }
}
MailSender : classe s'occupant d'envoyer les mails
<?php
class MailSender implements SplObserver
{
  protected $mail;
  
  public function __construct($mail)
  {
    if (preg_match('`^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$`', $mail))
    {
      $this->mail = $mail;
    }
  }
  
  public function update(SplSubject $obj)
  {
    mail($this->mail, 'Erreur détectée !', 'Une erreur a été détectée sur le site. Voici les informations de celle-ci : ' . "\n" . $obj->getFormatedError());
  }
}
BDDWriter : classe s'occupant de l'enregistrement en BDD
<?php
class BDDWriter implements SplObserver
{
  protected $db;
  
  public function __construct(PDO $db)
  {
    $this->db = $db;
  }
  
  public function update(SplSubject $obj)
  {
    $q = $this->db->prepare('INSERT INTO erreurs SET erreur = :erreur');
    $q->bindValue(':erreur', $obj->getFormatedError());
    $q->execute();
  }
}
Testons notre code !
<?php
$o = new ErrorHandler; // Nous créons un nouveau gestionnaire d'erreur.
$db = PDOFactory::getMysqlConnexion();

$o->attach(new MailSender('login@fai.tld'))
  ->attach(new BDDWriter($db));

set_error_handler([$o, 'error']); // Ce sera par la méthode error() de la classe ErrorHandler que les erreurs doivent être traitées.

5 / 0; // Générons une erreur

D'accord, cela en fait du code ! Je ne sais pas si vous vous en rendez compte, mais ce que nous venons de créer là est une excellente manière de coder. Nous venons de séparer notre code comme il se doit et nous pourrons le modifier aisément car les différentes actions ont été séparées avec logique.

Des classes anonymes pour nos observateurs

Depuis la version 7, PHP nous offre une fonctionnalité intéressante : les classes anonymes. Une classe anonyme est une classe ne possédant pas de nom. Vous serez amenés à en utiliser lorsque la classe que vous écrivez n'est clairement destinée qu'à une seule utilisation précise ou qu'elle n'a pas besoin d'être documentée. Dans ces cas-là, il n'est pas utile de déclarer cette classe dans un fichier dédié (ça en vient même à alourdir inutilement le code). Voici un exemple très simple d'une classe anonyme :

<?php
$monObjet = new class
{
  public function sayHello()
  {
    echo 'Hello world!';
  }
};

$monObjet->sayHello();

Une classe anonyme suit les mêmes règles que les classes normales : il est possible de procéder à des héritages, d'implémenter des interfaces, d'utiliser des traits, etc.

Nous allons ici remplacer nos 2 observateurs par 2 classes anonymes. Voici une solution possible :

<?php
$mailSender = new class('login@fai.tld') implements SplObserver
{
  protected $mail;
  
  public function __construct($mail)
  {
    if (preg_match('`^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$`', $mail))
    {
      $this->mail = $mail;
    }
  }
  
  public function update(SplSubject $obj)
  {
    mail($this->mail, 'Erreur détectée !', 'Une erreur a été détectée sur le site. Voici les informations de celle-ci : ' . "\n" . $obj->getFormatedError());
  }
};

$db = PDOFactory::getMysqlConnexion();
$dbWriter = new class($db) implements SplObserver
{
  protected $db;
  
  public function __construct(PDO $db)
  {
    $this->db = $db;
  }
  
  public function update(SplSubject $obj)
  {
    $q = $this->db->prepare('INSERT INTO erreurs SET erreur = :erreur');
    $q->bindValue(':erreur', $obj->getFormatedError());
    $q->execute();
  }
};

$o = new ErrorHandler; // Nous créons un nouveau gestionnaire d'erreur.

$o->attach($mailSender)
  ->attach($dbWriter);

set_error_handler([$o, 'error']); // Ce sera par la méthode error() de la classe ErrorHandler que les erreurs doivent être traitées.

5 / 0; // Générons une erreur

Comme vous pouvez le constater, si un argument doit être passé au constructeur, cela se fera juste après le mot-cléclass.

Il n'y a aucune difficulté dans cette notion, seul un peu de syntaxe est à apprendre !

Séparer ses algorithmes : le pattern Strategy

Le problème

Vous avez une classe dédiée à une tâche spécifique. Dans un premier temps, celle-ci effectue une opération suivant un algorithme bien précis. Cependant, avec le temps, cette classe sera amenée à évoluer, et elle suivra plusieurs algorithmes, tout en effectuant la même tâche de base. Par exemple, vous avez une classeFileWriterqui a pour rôle d'écrire dans un fichier ainsi qu'une classeDBWriter. Dans un premier temps, ces classes ne contiennent qu'une méthodewrite()qui n'écrira que le texte passé en paramètre dans le fichier ou dans la BDD.
Au fil du temps, vous vous rendez compte que c'est dommage qu'elles ne fassent que ça et vous aimeriez bien qu'elles puissent écrire en différents formats (HTML, XML, etc.) : les classes doivent donc formater puis écrire. C'est à ce moment qu'il est intéressant de se tourner vers le pattern Strategy. En effet, sans ce design pattern, vous seriez obligés de créer deux classes différentes pour écrire au format HTML par exemple :HTMLFileWriteretHTMLDBWriter. Pourtant, ces deux classes devront formater le texte de la même façon : nous assisterons à une duplication du code, la pire chose à faire dans un script ! Imaginez que vous voulez modifier l'algorithme dupliqué une dizaine de fois... Pas très pratique n'est-ce pas ?

Exemple concret

Passons directement à l'exemple concret. Nous allons suivre l'idée que nous avons évoquée à l'instant : l'action d'écrire dans un fichier ou dans une BDD. Il y aura pas mal de classes à créer donc au lieu de vous faire un grand discours, je vais détailler le diagramme représentant l'application (voir la figure suivante).

Diagramme modélisant une mise en place du design pattern Strategy
Diagramme modélisant une mise en place du design pattern Strategy

Ça en fait des classes ! Pourtant (je vous assure) le principe est très simple à comprendre. La classeWriterest abstraite (ça n'aurait aucun sens de l'instancier : on veut écrire, d'accord, mais sur quel support ?) et implémente un constructeur qui acceptera un argument : il s'agit du formateur que l'on souhaite utiliser. Nous allons aussi placer une méthode abstraitewrite(), ce qui forcera toutes les classes filles deWriterà implémenter cette méthode qui appellera la méthodeformat()du formateur associé (instance contenue dans l'attribut$formater) afin de récupérer le texte formaté. Allez, au boulot ! :)

Commençons par l'interface. Rien de bien compliqué, elle ne contient qu'une seule méthode :

<?php
interface Formater
{
  public function format($text);
}

Ensuite vient la classe abstraiteWriterque voici :

<?php
abstract class Writer
{
  // Attribut contenant l'instance du formateur que l'on veut utiliser.
  protected $formater;
  
  abstract public function write($text);
  
  // Nous voulons une instance d'une classe implémentant Formater en paramètre.
  public function __construct(Formater $formater)
  {
    $this->formater = $formater;
  }
}

Nous allons maintenant créer deux classes héritant deWriter:FileWriteretDBWriter.

<?php
class DBWriter extends Writer
{
  protected $db;
  
  public function __construct(Formater $formater, PDO $db)
  {
    parent::__construct($formater);
    $this->db = $db;
  }
  
  public function write ($text)
  {
    $q = $this->db->prepare('INSERT INTO lorem_ipsum SET text = :text');
    $q->bindValue(':text', $this->formater->format($text));
    $q->execute();
  }
}
<?php
class FileWriter extends Writer
{
  // Attribut stockant le chemin du fichier.
  protected $file;
  
  public function __construct(Formater $formater, $file)
  {
    parent::__construct($formater);
    $this->file = $file;
  }
  
  public function write($text)
  {
    $f = fopen($this->file, 'w');
    fwrite($f, $this->formater->format($text));
    fclose($f);
  }
}

Enfin, nous avons nos trois formateurs. L'un ne fait rien de particulier (TextFormater), et les deux autres formatent le texte en deux langages différents (HTMLFormateretXMLFormater). J'ai décidé d'ajouter le timestamp dans le formatage du texte histoire que le code ne soit pas complètement inutile (surtout pour la classe qui ne fait pas de formatage particulier).

<?php
class TextFormater implements Formater
{
  public function format($text)
  {
    return 'Date : ' . time() . "\n" . 'Texte : ' . $text;
  }
}
<?php
class HTMLFormater implements Formater
{
  public function format($text)
  {
    return '<p>Date : ' . time() . '<br />' ."\n". 'Texte : ' . $text . '</p>';
  }
}
<?php
class XMLFormater implements Formater
{
  public function format($text)
  {
    return '<?xml version="1.0" encoding="ISO-8859-1"?>' ."\n".
           '<message>' ."\n".
           "\t". '<date>' . time() . '</date>' ."\n".
           "\t". '<texte>' . $text . '</texte>' ."\n".
           '</message>';
  }
}

Et testons enfin notre code :

<?php
function autoload($class)
{
  if (file_exists($path = $class . '.php'))
  {
    require $path;
  }
}

spl_autoload_register('autoload');

$writer = new FileWriter(new HTMLFormater, 'file.html');
$writer->write('Hello world !');

Ce code de base a l'avantage d'être très flexible. Il peut paraître un peu imposant pour notre utilisation, mais si l'application est amenée à obtenir beaucoup de fonctionnalités supplémentaires, nous aurons déjà préparé le terrain ! :)

Allégeons notre code avec les classes anonymes

Eh oui, comme pour le pattern Observer, nous pouvons utiliser les classes anonymes pour rendre le tout plus léger. Cette fois-ci, j'aimerais que vous réfléchissiez vous-mêmes aux classes qui peuvent être rendues anonymes, puis que vous implémentiez lesdites classes. N'hésitez pas à vous appuyer sur le code du pattern Observer pour vous aider !

Une fois que vous avez réalisé ce petit travail, nous pouvons le corriger ensemble. Ici, ce sont les formateurs (c'est-à-dire les classes TextFormater, HTMLFormater et XMLFormater) qui ne méritent pas une classe dédiée. Anonymisons cela !

<?php
$textFormater = new class implements Formater
{
  public function format($text)
  {
    return 'Date : ' . time() . "\n" . 'Texte : ' . $text;
  }
};

$htmlFormater = new class implements Formater
{
  public function format($text)
  {
    return '<p>Date : ' . time() . '<br />' ."\n". 'Texte : ' . $text . '</p>';
  }
};

$xmlFormater = new class implements Formater
{
  public function format($text)
  {
    return '<?xml version="1.0" encoding="ISO-8859-1"?>' ."\n".
           '<message>' ."\n".
           "\t". '<date>' . time() . '</date>' ."\n".
           "\t". '<texte>' . $text . '</texte>' ."\n".
           '</message>';
  }
};

function autoload($class)
{
  if (file_exists($path = $class . '.php'))
  {
    require $path;
  }
}

spl_autoload_register('autoload');

$writer = new FileWriter($htmlFormater, 'file.html');
$writer->write('Hello world !');

N'est-ce pas bien plus pratique ? :)

Une classe, une instance : le pattern Singleton

Nous allons terminer par un pattern qui est en général le premier qu'on vous présente. Si je ne vous l'ai pas présenté au début c'est parce que je veux que vous fassiez attention en l'employant car il peut être très mal utilisé et se transformer en mauvaise pratique. On considérera alors le pattern comme un « anti-pattern ». Cependant, il est très connu et par conséquent il est essentiel de le connaître et de savoir pourquoi il ne faut pas l'utiliser dans certains contextes.

Le problème

Nous avons une classe qui ne doit être instanciée qu'une seule fois. À première vue, ça vous semble impossible et c'est normal. Jusqu'à présent, nous pouvions faire de multiples$obj=new Classe; jusqu'à l'infini, et nous nous retrouvions avec une infinité d'instances deClasse. Il va donc falloir empêcher ceci.

Pour empêcher la création d'une instance de cette façon, c'est très simple : il suffit de mettre le constructeur de la classe en privé ou en protégé !

T'es marrant toi, on ne pourra jamais créer d'instance avec cette technique !

Bien sûr que si ! Nous allons créer une instance de notre classe à l'intérieur d'elle-même ! De cette façon nous aurons accès au constructeur.

Oui mais voilà, il ne va falloir créer qu'une seule instance... Nous allons donc créer un attribut statique dans notre classe qui contiendra... l'instance de cette classe ! Nous aurons aussi une méthode statique qui aura pour rôle de renvoyer cette instance. Si on l'appelle pour la première fois, alors il faut instancier la classe puis retourner l'objet, sinon on se contente de le retourner.

Il reste un petit détail à régler. Nous voulons vraiment une seule instance, et là, il est encore possible d'en avoir plusieurs. En effet, rien n'empêche l'utilisateur de cloner l'instance ! Il faut donc bien penser à interdire l'accès à la méthode__clone().

Ainsi, une classe implémentant le pattern Singleton ressemblerait à ceci :

<?php
class MonSingleton
{
  protected static $instance; // Contiendra l'instance de notre classe.
  
  protected function __construct() { } // Constructeur en privé.
  protected function __clone() { } // Méthode de clonage en privé aussi.
  
  public static function getInstance()
  {
    if (!isset(self::$instance)) // Si on n'a pas encore instancié notre classe.
    {
      self::$instance = new self; // On s'instancie nous-mêmes. :)
    }
    
    return self::$instance;
  }
}

Ceci est le strict minimum. À vous d'implémenter de nouvelles méthodes comme vous l'auriez fait dans votre classe normale !

Voici donc une utilisation de la classe :

<?php
$obj = MonSingleton::getInstance(); // Premier appel : instance créée.
$obj->methode1();

Exemple concret

Un exemple concret pour le pattern Singleton ? Non, désolé, nous allons devoir nous en passer. :-°

Quoi ? Tu te moques de nous ? Alors il sert à rien ce design pattern ? o_O

Selon moi, non. Je n'ai encore jamais eu besoin de l'utiliser. Ce pattern doit être employé uniquement si plusieurs instanciations de la classe provoquaient un dysfonctionnement. Si le script peut continuer normalement alors que plusieurs instances sont créées, le pattern Singleton ne doit pas être utilisé.

Donc ce que nous avons appris là, ça ne sert à rien ?

Non. Il est important de connaître ce design pattern, non pas pour l'utiliser, mais au contraire pour ne pas y avoir recours, et surtout savoir pourquoi. Cependant, avant de vous répondre, je vais vous présenter un autre pattern très important : l'injection de dépendances.

L'injection de dépendances

Comme tout pattern, celui-ci est né à cause d'un problème souvent rencontré par les développeurs : le fait d'avoir de nombreuses classes dépendantes les unes des autres. L'injection de dépendances consiste à découpler nos classes. Le pattern singleton que nous venons de voir favorise les dépendances, et l'injection de dépendances palliant ce problème, il est intéressant d'étudier ce nouveau pattern avec celui que nous venons de voir.

Soit le code suivant :

<?php
class NewsManager
{
  public function get($id)
  {
    // On admet que MyPDO étend PDO et qu'il implémente un singleton.
    $q = MyPDO::getInstance()->query('SELECT id, auteur, titre, contenu FROM news WHERE id = '.(int)$id);
    
    return $q->fetch(PDO::FETCH_ASSOC);
  }
}

Vous vous apercevez qu'ici, le singleton a introduit une dépendance entre deux classes n'appartenant pas au même module. Deux modules ne doivent jamais être liés de cette façon, ce qui est le cas dans cet exemple. Deux modules doivent être indépendants l'un de l'autre. D'ailleurs, en y regardant de plus près, cela ressemble fortement à une variable globale. En effet, un singleton n'est rien d'autre qu'une variable globale déguisée (il y a juste une étape en plus pour accéder à la variable) :

<?php
class NewsManager
{
  public function get($id)
  {
    global $db;
    // Revient EXACTEMENT au même que :
    $db = MyPDO::getInstance();
    
    // Suite des opérations.
  }
}

Vous ne voyez pas où est le problème ? Souvenez-vous de l'un des points forts de la POO : le fait de pouvoir redistribuer sa classe ou la réutiliser. Dans le cas présent, c'est impossible, car notre classeNewsManagerdépend deMyPDO. Qu'est-ce qui vous dit que la personne qui utiliseraNewsManageraura cette dernière ? Rien du tout, et c'est normal. Nous sommes ici face à une dépendance créée par le singleton. De plus, la classe dépend aussi de PDO : il y avait donc déjà une dépendance au début, et le pattern Singleton en a créé une autre. Il faut donc supprimer ces deux dépendances.

Comment faire alors ?

Ce qu'il faut, c'est passer notre DAO au constructeur, sauf que notre classe ne doit pas être dépendante d'une quelconque bibliothèque. Ainsi, notre objet peut très bien utiliser PDO, MySQLi ou que sais-je encore, la classe se servant de lui doit fonctionner de la même manière. Alors comment procéder ? Il faut imposer un comportement spécifique à notre objet en l'obligeant à implémenter certaines méthodes. Je ne vous fais pas attendre : les interfaces sont là pour ça. Nous allons donc créer une interfaceiDBcontenant (pour faire simple) qu'une seule méthode :query().

<?php
interface iDB
{
  public function query($query);
}

Pour que l'exemple soit parlant, nous allons créer deux classes utilisant cette structure, l'une utilisant PDO et l'autre MySQLi. Cependant, un problème se pose : le résultat retourné par la méthodequery()des classes PDO et MySQLi sont des instances de deux classes différentes, les méthodes disponibles ne sont, par conséquent, pas les mêmes. Il faut donc créer d'autres classes pour gérer les résultats qui suivent eux aussi une structure définie par une interface (admettonsiResult).

<?php
interface iResult
{
  public function fetchAssoc();
}

Nous pouvons donc à présent écrire nos quatre classes :MyPDO,MyMySQLi,MyPDOStatementetMyMySQLiResult.

<?php
class MyPDO extends PDO implements iDB
{
  public function query($query)
  {
    return new MyPDOStatement(parent::query($query));
  }
}
<?php
class MyPDOStatement implements iResult
{
  protected $st;
  
  public function __construct(PDOStatement $st)
  {
    $this->st = $st;
  }
  
  public function fetchAssoc()
  {
    return $this->st->fetch(PDO::FETCH_ASSOC);
  }
}
<?php
class MyMySQLi extends MySQLi implements iDB
{
  public function query($query)
  {
    return new MyMySQLiResult(parent::query($query));
  }
}
<?php
class MyMySQLiResult implements iResult
{
  protected $st;
  
  public function __construct(MySQLi_Result $st)
  {
    $this->st = $st;
  }
  
  public function fetchAssoc()
  {
    return $this->st->fetch_assoc();
  }
}

Nous pouvons donc maintenant écrire notre classeNewsManager. N'oubliez pas de vérifier que les objets sont bien des instances de classes implémentant les interfaces désirées !

<?php
class NewsManager
{
  protected $dao;
  
  // On souhaite un objet instanciant une classe qui implémente iDB.
  public function __construct(iDB $dao)
  {
    $this->dao = $dao;
  }
  
  public function get($id)
  {
    $q = $this->dao->query('SELECT id, auteur, titre, contenu FROM news WHERE id = '.(int)$id);
    
    // On vérifie que le résultat implémente bien iResult.
    if (!$q instanceof iResult)
    {
      throw new Exception('Le résultat d\'une requête doit être un objet implémentant iResult');
    }
    
    return $q->fetchAssoc();
  }
}

Testons maintenant notre code.

<?php
$dao = new MyPDO('mysql:host=localhost;dbname=news', 'root', '');
// $dao = new MyMySQLi('localhost', 'root', '', 'news');

$manager = new NewsManager($dao);
print_r($manager->get(2));

Je vous laisse commenter et décommenter les deux premières lignes pour vérifier que les deux fonctionnent. Après quelques tests, vous vous rendrez compte que nous avons bel et bien découplé nos classes ! Il n'y a ainsi plus aucune dépendance entre notre classeNewsManageret une quelconque autre classe.

Pour conclure

Le principal problème du singleton est de favoriser les dépendances entre deux classes. Il faut donc être très méfiant de ce côté-là, car votre application deviendra difficilement modifiable et l'on perd alors les avantages de la POO. Je vous recommande donc d'utiliser le singleton en dernier recours : si vous décidez d'implémenter ce pattern, c'est pour garantir que cette classe ne doit être instanciée qu'une seule fois. Si vous vous rendez compte que deux instances ou plus ne causent pas de problème à l'application, alors n'implémentez pas le singleton. Et par pitié : n'implémentez pas un singleton pour l'utiliser comme une variable globale ! C'est la pire des choses à faire car cela favorise les dépendances entre classes comme nous l'avons vu.

Si vous voulez en savoir plus sur l'injection de dépendances (notamment sur l'utilisation de conteneurs), je vous invite à lire cet excellent cours « Introduction à l'injection de dépendances en PHP ».

En résumé

  • Un design pattern est un moyen de conception répondant à un problème récurrent.

  • Le pattern factory a pour but de laisser des classes usine créer les instances à votre place.

  • Le pattern observer permet de lier certains objets à des « écouteurs » eux-mêmes chargés de notifier les objets auxquels ils sont rattachés.

  • Le pattern strategy sert à délocaliser la partie algorithmique d'une méthode afin de le permettre réutilisable, évitant ainsi la duplication de cet algorithme.

  • Le pattern singleton permet de pouvoir instancier une classe une seule et unique fois, ce qui présente quelques soucis au niveau des dépendances entre classes.

  • Le pattern injection de dépendances a pour but de rendre le plus indépendantes possible les classes.

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