Aller au menu - Aller au contenu

Icône Les design patterns

Avatar
Mise à jour : 25/05/2012
Difficulté : Difficile Difficile Creative Commons BY-NC-SA
24 952 visites depuis 7 jours, dont 473 sur ce chapitre classé 13/786
Nous allons découvrir dans ce chapitre ce que sont les design patterns (ou motifs de conception). Les design patterns sont donc des façons de concevoir des classes afin de répondre à un problème que nous sommes susceptibles de rencontrer. Ce sont des bonnes pratiques, mais il faut faire attention à ne pas trop en abuser sous prétexte que ça fait classe. ;)
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

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

Le problème


Admettons que vous venez de créer une assez grosse application. 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 de new à 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 :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?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 :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?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. Bref, nous allons tout centraliser dans une classe.

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?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ésent une classe s'occupant 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 chose en commun : le fait qu'elles soient appelées car telle action s'est produite. Elles n'ont rien d'autre en commun, elles sont un peu foutues 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 une classe observée et une ou plusieurs autre(s) classe(s) qui l'observe(nt). Lorsque telle action survient, vous allez prévenir toutes les classes 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 utilisant spl_autoload_register(). ;)

Bref, regardons plutôt ce qui nous intéresse, à savoir deux interfaces : SplSubject et SplObserver.

La première interface, SplSubject, est l'interface implémentée par l'objet observé. Elle contient trois méthodes :
  • attach (SplObserver $observer) : méthode appelée pour ajouter une classe observatrice à notre classe observée ;
  • detach (SplObserver $observer) : méthode appelée pour supprimer une classe observatrice ;
  • notify() : méthode appelée lorsqu'on aura besoin de prévenir toutes les classes observatrices que quelque chose s'est produit.
L'interface SplObserver est l'interface implémentée par les différents observateurs. Elle ne contient qu'une seule méthode qui est celle appelée par la classe observée dans la méthode notify() : il s'agit de update ( (SplSubject $subject)).

Voici un diagramme mettant en œuvre ce design pattern :

Image utilisateur


On va maintenant imaginer le code correspondant au diagramme. Commençons par la classe observée :

Code : PHP - Classe observée
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
    class Observee implements SplSubject
    {
        // Ceci est le tableau qui va contenir tous les objets qui nous observent
        protected $observers = array();
        
        // 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();
        }
    }
?>


Vous pouvez constater la présence du nom des interfaces en guise d'argument. Cela veut dire que cet argument doit implémenter l'interface spécifiée.


Voici les deux classes observatrices :

Code : PHP - Classes observatrices
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
    class Observer1 implements SplObserver
    {
        public function update (SplSubject $obj)
        {
            echo __CLASS__, ' a été notifié ! Nouvelle valeur de l\'attribut <strong>nom</strong> : ', $obj->getNom();
        }
    }
    
    class Observer2 implements SplObserver
    {
        public function update (SplSubject $obj)
        {
            echo __CLASS__, ' 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é ça, histoire que vous voyez la syntaxe de base lors de l'utilisation du pattern Observer.

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

Code : PHP
1
2
3
4
5
6
<?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 voir qu'ajouter des classes observatrices de cette façon peut être assez long si on en a 5 ou 6. Il y a une petite technique qui consiste à pouvoir obtenir ce genre de code :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?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éthode attach() doit retourner l'instance qui l'a appelé (en d'autres termes, elle doit retourner $this).

Exemple concret



Regardons un exemple concret à présent. Nous allons imaginer que vous ayez, 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 attrapant l'erreur et effectuant les deux opérations ci-dessus. Grave erreur ! Ceci est surtout à ne pas faire : votre classe est chargée d'intercepter les erreurs, et non de les gérer ! Ce sera à d'autres classes de s'en occuper : ces classes vont observer la classe gérant l'erreur et une fois notifiée, elles vont effectuer l'action pour laquelle elles ont été conçues. Vous voyez un peu la tête qu'aura le script ?

Rappel : pour intercepter les erreurs, il vous faut utiliser set_error_handler(). Pour faire en sorte que la fonction de callback appelée lors de la génération d'une erreur soit une méthode d'une classe, passez un tableau à deux entrées en premier argument. La première entrée est l'objet sur lequel vous allez appeler la méthode, et la seconde est le nom de la méthode.


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

Secret (cliquez pour afficher)

ErrorHandler : classe gérant les erreurs


Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
    class ErrorHandler implements SplSubject
    {
        // Ceci est le tableau qui va contenir tous les objets qui nous observent
        protected $observers = array();
        
        // 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


Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?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


Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?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 !


Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?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 (array ($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
?>


Pfiou, ça en fait du code ! Je ne sais pas si vous vous en rendez compte, mais ce qu'on vient 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.

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 tache de base. Par exemple, vous avez une classe EcrireFichier qui a pour rôle d'écrire dans un fichier ainsi qu'une classe EcrireBDD. Dans un premier temps, ces classes ne contiennent qu'une méthode ecrire() 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 : EcrireFichierHTML et EcrireBDDHTML. Pourtant, ces deux classes devront formater le texte de la même façon : nous assisterons à une duplication du code, et c'est 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 vous montrer le diagramme représentant l'application :

Image utilisateur


Ça en fait des classes ! Pourtant (je vous assure) le principe est très simple à comprendre. La classe Ecrire est abstraite (ça n'aurait aucun sens de l'instancier : on veut écrire, ok, 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 abstraite ecrire(), ce qui forcera toutes les classes filles de Ecrire à implémenter cette méthode qui appellera la méthode formater() du formateur associé (instance contenue dans l'attribut $formateur) 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 :

Code : PHP - iFormateur.interface.php
1
2
3
4
5
6
<?php
    interface iFormateur
    {
        public function formater ($texte);
    }
?>


Ensuite vient la classe abstraite Ecrire que voici :

Code : PHP - Ecrire.class.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
    abstract class Ecrire
    {
        // Attribut contenant l'instance du formateur que l'on veut utiliser
        protected $formateur;
        
        abstract public function ecrire ($texte);
        
        // Nous voulons une instance d'une classe implémentant iFormateur en paramètre
        public function __construct (iFormateur $formateur)
        {
            $this->formateur = $formateur;
        }
    }
?>


Nous allons maintenant créer deux classes héritant de Ecrire : EcrireFichier et EcrireBDD.

Code : PHP - EcrireBDD.class.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
    class EcrireBDD extends Ecrire
    {
        protected $db;
        
        public function __construct (iFormateur $formateur, PDO $db)
        {
            parent::__construct($formateur);
            $this->db = $db;
        }
        
        public function ecrire ($texte)
        {
            $q = $this->db->prepare('INSERT INTO lorem_ipsum SET texte = :texte');
            $q->bindValue(':texte', $this->formateur->formater($texte));
            $q->execute();
        }
    }
?>

Code : PHP - EcrireFichier.class.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
    class EcrireFichier extends Ecrire
    {
        // Attribut stockant le chemin du fichier
        protected $fichier;
        
        public function __construct (iFormateur $formateur, $fichier)
        {
            parent::__construct($formateur);
            $this->fichier = $fichier;
        }
        
        public function ecrire ($texte)
        {
            $f = fopen ($this->fichier, 'w');
            fwrite ($f, $this->formateur->formater($texte));
            fclose ($f);
        }
    }
?>


Et enfin, nous avons nos trois formateurs. L'un ne fait rien de particulier (FormaterTexte), et les deux autres formatent le texte en deux langages différents (FormaterHTML et FormaterXML). 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). ;)

Code : PHP - FormaterTexte.class.php
1
2
3
4
5
6
7
8
9
<?php
    class FormateurTexte implements iFormateur
    {
        public function formater ($texte)
        {
            return 'Date : ' . time() . "\n" . 'Texte : ' . $texte;
        }
    }
?>

Code : PHP - FormaterHTML.class.php
1
2
3
4
5
6
7
8
9
<?php
    class FormateurHTML implements iFormateur
    {
        public function formater ($texte)
        {
            return '<p>Date : ' . time() . '<br />' ."\n". 'Texte : ' . $texte . '</p>';
        }
    }
?>

Code : PHP - FormaterXML.class.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
    class FormateurXML implements iFormateur
    {
        public function formater ($texte)
        {
            return '<?xml version="1.0" encoding="ISO-8859-1"?>' ."\n".
                   '<message>' ."\n".
                   "\t". '<date>' . time() . '</date>' ."\n".
                   "\t". '<texte>' . $texte . '</texte>' ."\n".
                   '</message>';
        }
    }
?>


Et testons enfin notre code :

Code : PHP - index.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
    function autoload ($classe)
    {
        if (file_exists ($chemin = $classe . '.class.php') OR file_exists ($chemin = $classe . '.interface.php'))
            require $chemin;
    }
    
    spl_autoload_register ('autoload');
    
    $ecritureFichier = new EcrireFichier (new FormateurHTML, 'fichier.html');
    $ecritureFichier->ecrire('Hello world !');
?>


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

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 avec 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 très important de savoir ce que c'est mais surtout : 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 de Classe. Il va donc falloir empêcher ceci.

Pour empêcher de créer 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 sur 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... On va 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 on instancie la classe puis on retourne l'objet, sinon on se contente de le retourner. ;)

Il y a aussi 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 :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?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 :

Code : PHP
1
2
3
4
<?php
    $obj = MonSingleton::getInstance(); // Premier appel : instance créée
    $obj->methode1();
?>


Exemple concret


Un exemple concret pour le pattern Singleton ? Non, désolé, on va devoir s'en passer. :-°

Hein ? Quoi ? Tu te moques de moi ? 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 utilisé uniquement si plusieurs instanciations de la classe provoqueraient un dysfonctionnement. Si le script peut continuer normalement alors que plusieurs instances sont créées, le pattern Singleton ne doit pas être utilisé.

Donc en gros, ce qu'on a appris là, c'est du vent ?

Non. Il est important de connaitre ce design pattern, non pas pour l'utiliser, mais pour ne pas l'utiliser, 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 : celui qui fait qu'on a plein de classes dépendantes les unes des autres. L'injection de dépendances consiste à découpler nos classes. Le pattern singleton qu'on vient 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 qu'on vient de voir.

Soit le code suivant :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?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 ici. Deux modules doivent être indépendants les uns des autres. D'ailleurs, en y regardant de plus près, ça 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) :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?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. Là, on ne peut pas, car notre classe NewsManager dépend de MyPDO. Qu'est-ce qui vous dit que la personne qui utilisera NewsManager aura 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. On va donc créer une interface iDB contenant (pour faire simple) qu'une seule méthode : query().

Code : PHP
1
2
3
4
5
<?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éthode query() des classes PDO et MySQLi sont des instances de deux classes différentes, et 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 elles aussi une structure définie par une interface (admettons iResult).

Code : PHP
1
2
3
4
5
<?php
    interface iResult
    {
        public function fetchAssoc();
    }

Nous pouvons donc à présent écrire nos 4 classes : MyPDO, MyMySQLi, MyPDOStatement et MyMySQLiResult.

Code : PHP - MyPDO
1
2
3
4
5
6
7
8
<?php
    class MyPDO extends PDO implements iDB
    {
        public function query($query)
        {
            return new MyPDOStatement(parent::query($query));
        }
    }

Code : PHP - MyPDOStatement
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?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);
        }
    }

Code : PHP - MyMySQLi
1
2
3
4
5
6
7
8
<?php
    class MyMySQLi extends MySQLi implements iDB
    {
        public function query($query)
        {
            return new MyMySQLiResult(parent::query($query));
        }
    }

Code : PHP - MyMySQLiResult
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
    class MyMySQLiResult implements iResult
    {
        protected $st;
        
        public function __construct(MySQLi_Result $st)
        {
            $this->st = $st;
        }
        
        public function fetchAssoc()
        {
            return $this->st->fetch_assoc();
        }
    }

On peut donc maintenant écrire notre classe NewsManager. N'oubliez pas de vérifier que les objets sont bien des instances de classes implémentant les interfaces désirées. ;)

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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.

Code : PHP
1
2
3
4
5
6
<?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 classe NewsManager et une quelconque autre classe.

Le problème dans notre cas, c'est qu'il est difficile de faire de l'injection de dépendances pour qu'une classe supporte toutes les bibliothèques d'accès aux BDD (PDO, MySQLi, etc.) à cause des résultats des requêtes. De son côté, PDO a la classe PDOStatement, tandis que MySQLi a MySQLi_STMT pour les requêtes préparées et MySQLi_Result pour les résultats de requêtes classiques. Cela est donc difficile de les conformer au même modèle. On va donc, dans le TP qui va venir, utiliser une autre technique pour découpler nos classes.


En résumé


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 on perd alors les avantages de la POO. En bref, je vous recommande 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 on l'a 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 tutoriel de vincent1870.
Ce chapitre est très important. Je ne vous demande pas de retenir tous ces design patterns non plus, mais sachez que ce sont les principaux et qu'ils peuvent vous être utile à plusieurs reprises. Cependant, n'en abusez pas trop : ne les utilisez que quand vous en avez réellement besoin ! ;)
Chapitre précédent Sommaire Chapitre suivant

Partager

15 commentaires pour "Les design patterns"
Note moyenne : 3.69 / 4 (342 votes)
Pseudo Commentaire
Hors ligne aslo # Posté le 24/12/2011 à 17:29:14

Avis : Décevant

Pour répondre à Rurik, une erreur est déclenchée parce qu'il est interdit de modifier la portée d'une méthode dans une classe fille. Maman a crée une méthode publique, il n'y a pas de raison que sa fille en fasse une protégée.

Problème résolu ? Passez en Résolu (Image utilisateur)!
Problème résolu sans aucune intervention ? Indiquez néanmoins la réponse !
 
Hors ligne glorymack # Posté le 28/12/2011 à 11:50:47

Avis : Bon

Pour être réaliste, j'ai l'impression d'être précipité dans un monde qui n'est pas mien tout d'un coup, qu'est ce qui se passe ? j'ai loupé des épisode ou...aurai- je omis d'autre chapitres, et pourtant non, j'ai lu à la loupe chacun de chapitre jusqu'en arriver là,
autant dire que j'ai donné bien plus d'effort qu'un Zero puisse donner pour en arriver jusqu'à ce chapitre ! Et ma grande satisfaction je comprends mieux le concept OO, et là je t'adresse mes sincères remerciements et un coût de chapeau pour le travaille battu!

Je résume, j'ai jamais fais du POO, j'ai lu ce tuto et tout semble limpide jusque là, sauf que tout d'un coup apparût " Les design patterns ", à qui s'adresse ce chapitre ? aux experts et techniciens de OO en php ou au simple zero désirant apprendre l'OO, autant dire que ce chapitre m'a coupé les ailles ! T'as mis la barre top haute pour un zero, et là on est pas sur la même longueur d'onde...

De part l'introduction du chapitre tu parles (si vous implémentez ce pattern, vous n'aurez plus de new à placer dans la partie globale du script afin d'instancier une classe.... ce ne sera pas à vous de le faire mais à une classe usine) qu'est-ce qu'on est censé comprendre là dessus ? et tout d'un coup la class DBFactory, d'où elle sort celle-là ? et après le bout de code qu'on est censé déchiffrer nous mêmes !

A mon avis c'est l'approche qui n'est pas très claire et ce qui implique que le reste du chapitre devienne subitement une ambiguïté ! Il est évident qu'un bon programmeur en OO trouve son compte dans ce chapitre, mais honnêtement pour un zero, c'est un peu du suicide à l'abandon !

Tu as réussi à faire un bon tuto sur l'OO à tel enseigne de pénétrer le niveau d'un zero à appréhender le concept Objet. C'est très bien et surtout encourageant. Mais daigne comprendre qu'il y a bien de chapitre comme celui - ci, comme les exceptions semblent échappé à la compréhension d'un apprenti.
Hors ligne Marco_105 # Posté le 19/01/2012 à 11:40:57
Avatar

Effectivement, les design pattern sont un tutoriel très intéressant et valorisant pour la POO, toutefois je reste dubitatif des possibilités de PHP en la matière... réagit-il vraiment comme un langage objet ?

A la fin de l'exercice pattern Strategy j'envoi ça :

Code : PHP
1
2
$ecriturefichier = new EcrireBDD(new FormaterTexte(), PDOFactory::getMysqlConnexion());
$ecriturefichier->ecrire('Hello world');


et la réponse sur le navigateur :
Fatal error: Cannot call abstract method Ecrire::ecrire() o_O

Il me semble avoir correctement suivi le tuto et mal comprendre pourquoi au lieu d'aller chercher la méthode ecrire() de la classe EcrireBDD (EcrireFichier donne une réponse identique) c'est la méthode de la classe abstraite Ecrire qui a été appelée ?

Code des méthodes utilisées, respectivement classe mère, classe fille :
Code : PHP
1
public abstract function ecrire($texte);


Code : PHP
1
2
3
4
5
6
public function ecrire($texte)
    {
        $sql = $this->db->prepare('INSERT INTO lorem_ipsum SET texte = :texte');
        $sql->bindValue(':texte', $this->formateur->formater($texte));
        $sql->execute;
    }


:euh: Cela suffit-il pour expliquer ce problème ?
Hors ligne armelo # Posté le 12/05/2012 à 03:13:35

Bonjour à tous,
Juste pour mettre un accent sur la remarque de "glorymack" concernant les Design pattern. Normalement c'est un chapitre qui devrais etre en annexe du tuto!
Pour pratiquer les DP il faut suffisamment connaitre le langage à utiliser (PHP, C++, Java, VB.Net ... bref un langage OO). Il est quasiement impossible pour un Zero en POO de s'ensortir apres avoir lu les premiers chapitres du tuto. Il faut un minimum de pratique pour y arriver avec un minimum de difficulté.

Pour ceux que ça intéresserait, le Pattern "Singleton" est très souvent utilisé dans la gestion de la connexion (...à une BDD par exemple).
Nous savons que pour interagir avec une base de donnée, une et une seul connexion est nécessaire, et à la fin de nos opérations sur la BDD on dois se déconnecter de la BDD(fermer la connexion). L'idéal dans ce cas de figure est de créer une classe dediée à cette tache:

Exemple:

class connexionBDD {
/*tous les attributs de la connexion (nomBDD, user,
*password, port) ne doivent pas etre implementer par
*crainte que l'utilisateur essaye de les setter(changer
*de user ou de BDD sans avoir fermer l'ancienne
*connexion */

/*Declaration de l'attribut de type ConnexionBDD*/

/*Constructeur privée (pour empêcher toute
*instanciation)*/

/* Implémenter la metode static getConnexion() qui se charge de
*créer la connexion si elle n'existe pas deja avec tous
*les param nécessaire. (condition avec isset par
*exemple)
*PS: Ne pas oublier d'attraper les éventuelles
*exceptions pouvant survenir*/

/* Implémenter la méthode static déconnexion() qui se
*chargera de déconnecter l'apli de la BDD.
*PS: Ne pas oublier d'attraper les éventuelles
*exceptions pouvant survenir*/
}

vous pourrez ainsi balancer vos connexion partou dans le code global sans crainte de bourrer la BDD de connexion inutile.

Un autre petit problème peut survenir malgré cette petite sécurité: L'oublie de la déconnexion après une opération nécessitant une connexion à la BDD.
Pour être sure que ce pb ne vienne pas vous gâcher la fête, Il suffit de se déconnecter automatiquement lorsqu'on veut créer une nouvelle connexion en appelant la méthode déconnexion en premier instruction dans le script de la méthode getConnexion. de cette façon qu'on soit connecter ou pas la deConnexion est automatisé dans la connexion.
Connecté devil may cry # Posté hier à 19:45:21
Avatar

Études : IUP MIAGE Aix-Marseille

Citation : armelo
Bonjour à tous,
Juste pour mettre un accent sur la remarque de "glorymack" concernant les Design pattern. Normalement c'est un chapitre qui devrais etre en annexe du tuto!
Pour pratiquer les DP il faut suffisamment connaitre le langage à utiliser (PHP, C++, Java, VB.Net ... bref un langage OO). Il est quasiement impossible pour un Zero en POO de s'ensortir apres avoir lu les premiers chapitres du tuto. Il faut un minimum de pratique pour y arriver avec un minimum de difficulté.

Pour ceux que ça intéresserait, le Pattern "Singleton" est très souvent utilisé dans la gestion de la connexion (...à une BDD par exemple)...

Il ne faut pas seulement connaitre le langage, il faut aussi s'entrainer sur de la conception pour comprendre les pendants et les aboutissants des design patterns. Ce n'est pas destiné pour le zéro qui apprend l'objet mais qui souhaite aller plus loin (en ayant déjà une grande part de recul). Mais vu le survol des patterns présentés, ça risque d'être difficile à apprendre pour le zéro lambda.

Je vais le répéter mais attention à ne pas abuser du singleton, beaucoup de personnes l'utilise mais à tord. La connexion en singleton frôle cet abus, c'est juste une variable globale camouflée (et autant utiliser une variable globale). Si tu veux faire quelque chose de vraiment propre, il faudrait faire de l’injection de dépendance mais c'est un peu fastidieux.

Voir tous les commentaires