Design patterns [1]

DESIGN PATTERNS (ou MOTIFS DE CONCEPTION). PART 1

Ce tutoriel est composé de trois parties. Vous retrouverez les deux suivantes ici :
Les Design Patterns Volume 2
Les Design Patterns Volume 3

INTRODUCTION

J'ai décidé d'écrire ce tuto sur les design patterns car si l'on trouve de nombreuses références à ces méthodes de programmation sur le net,
la plupart du temps elles sont en anglais. Or, tout le monde n'est pas à l'aise avec la langue de Shakespeare ;-)
Je ne couvrirai pas l'exhaustivité des design patterns existants, mais je vais tâcher de me concentrer sur les plus fréquemment utilisés.
Les exemples de code seront écrits en PHP5, parce que...ben parce qu'on en est bientôt à PHP6 alors il serait temps d'abandonner PHP4 ! ;-)

UN DESIGN PATTERN, QU'EST-CE DONC ??

Depuis que le monde est monde (ou presque...), les programmeurs se sont heurtés à des problèmes récurrents. Ils ont donc réfléchi, et ont mis en place des méthodes pour résoudre ces problèmes. Les design patterns sont ces méthodes.
Vous avez probablement déjà utilisé des design patterns sans le savoir! Par vous-même, ou en ayant copié un bout de code. Mais il est bon de connaître ces méthodes et de les utiliser de manière académique : votre code en deviendra plus lisible pour les autres programmeur, au fait de ces méthodes. De plus, à quoi bon réinventer la roue ? Des programmeurs chevronnés ont eu, avant vous, les mêmes problèmes que vous, y ont réflêchi, et ont trouvé des parades : ces méthodes sont éprouvées, car elles sont été retournées dans tous les sens depuis bien des années.

LE VIF DU SUJET!

Je vais commencer par des design patterns que vous connaissez sans doute déjà, car certains sont expliqués dans la doc de PHP5, sur php.net. Ils font partie des design patterns de type création. Je continuerai les autres types de design patterns (de type structure et comportement) dans d'autres tutos.

DESIGN PATTERNS DE TYPE CREATION

SINGLETON

Le singleton sert à restreindre l'instanciation d'une classe à 1 seul objet. En clair, en utilisant le singleton, si vous tentez d'instancier 2 fois une classe, 1 seul objet vous sera renvoyé. Ce design pattern peut être très utile dans de nombreux cas, afin d'éviter des doublons inutiles, et donc des pertes de ressources.
Vous trouverez un exemple de singleton sur php.net : http://fr.php.net/manual/fr/language.oop5.patterns.php

Voici comment un singleton très simple peut se présenter :

<?php
class single {
    /**
     * Propriété privée stockant une instance de la classe si single::getInstance() est appelé
     *
     * @var single
     */
    private static $_instance;
     /**
     * Constructeur privé puisqu'on ne veut pas pouvoir instancier single de cette manière. Il faut passer par single::getInstance()
     *
     */
    private function __construct () {
        echo 'je suis une instance de single';
    }
    
    /**
     * Méthode implémentant le design pattern singleton
     *
     * @return single
     */
    public static function getInstance () {
        if (!isset (self::$_instance)) {
            self::$_instance = new single();
        }
        return self::$_instance;
    }
    
    /**
     * Méthode publique quelconque
     *
     */
    public function doSomething () {
        echo 'I am doing something!';
    }
    
    /**
     * Si l'on ne veut pas pouvoir instancier single de façon normale, on ne doit pas non plus pouvoir le cloner.
     *
     */
    public function __clone () {
        throw new Exception ('Le clonage de single n'est pas autorisé');
    }
}

//$o = new single (); // va échouer car le constructeur est privé
$o = single::getInstance ();
$o ->doSomething ();
$o2 = clone ($o); // va lancer une exception grâce à single::__clone()
?>

FACTORY

La factory, ou usine, est responsable de la "fabrication" d'objets. Il est très pratique dans le cadre d'un ensemble de classes abstraites et concrètes, souvent semblables, mais au fonctionnement légèrement différent et qui s'applique dans des cas précis. Par exemple, une classe d'abstraction de base de données pourrait implémenter une usine : l'usine instanciera la classe dédiée à mysql ou à mssql selon les cas.
Un exemple se trouve sur php.net : http://fr.php.net/manual/fr/language.oop5.patterns.php

Voici comment une factory très simple peut se présenter :

<?php
abstract class animalFactory {

    /**
     * Factory !
     *
     * @param string $sType
     * @return object
     */

    public static function factory ($sType) {
        if (!class_exists ($sType, true) ) {
            throw new Exception ($sType.' na pas été trouvé');
        }
        return new $sType;
    }
}

interface animal {
    public function say();    
}

class dog implements animal {
    
    public function say () {
        echo 'wouf';
    }
}

class cat implements animal {

    public function say () {
        echo 'miaou';
    }
}

try {
    $o = animalFactory::factory('cat');
    $o ->say();
} catch (Exception $e) {
    echo $e;
}
?>

SINGLETON FACTORY?

Et oui, pourquoi ne pas mixer les deux ? Les design patterns ne sont pas forcément destinés à être utilisés séparément!
Bien au contraire...
Voici un exemple un peu plus complexe :
Il se compose d'une classe parente abstraite qui va jouer le rôle à la fois d'usine, ET de contenant de singletons.
En fait, il s'agit d'un multiton : on peut stocker ici 1 et 1 seule référence à x classes.
Cet exemple utilise l'API de Réflexion afin de vérifier que les classes demandées soient valides (étendant la classe animal parente).

Vous trouverez plus d'infos sur cette API ici : http://fr2.php.net/manual/fr/language.oop5.reflection.php

<?php
abstract class animal {
 
    public static $_aInstance;
   
    protected function __construct () {
        // interdit!
    }
    
    /**
     * Factory !
     * Plus complexe, on vérifie cette fois que la classe demandée soit une classe valide (étendant animal) via la Réflexion
     *
     * @param string $sType
     * @return object
     */

    public static function singletonFactory ($sType) {
        $o = new ReflectionClass($sType);
        if (!$o ->isSubclassOf(__CLASS__)) {
            throw new Exception ($sType.' n'est pas une classe enfant de '.__CLASS__);
        }
        if (!isset (self::$_aInstance[$sType])) {
            self::$_aInstance[$sType] = new $sType;
        }
        return self::$_aInstance[$sType];
    }
    
    /**
     * Méthode pour vérifier notre tableau d'instance
     *
     * @return array
     */

    public static function getObject () {
        return self::$_aInstance;    
    }
    
    /**
     * Méthode abstraite affichant le cri de l'animal concerné
     *
     */

    abstract public function say();    
}

class dog extends animal {
  
    public function say () {
        echo 'wouf';
    }
}

class cat extends animal {
   
    public function say () {
        echo 'miaou';
    }
}

try {
    $o = animal::singletonFactory('dog');
    $o ->say();
    $o2 = animal::singletonFactory('cat');
    $o2 ->say();
    /**
    * Cet objet $o3 sera exactement le même que $o!
    */
    $o3 = animal::singletonFactory('dog');
    $o3 ->say();
    /**
    * On vérifie tout de suite cette affirmation...
    */
    echo '<pre>',print_r (animal::getObject()),'</pre>';
} catch (Exception $e) {
    echo $e;
}
?>

BUILDER

Le builder, ou monteur, est destiné à séparer la construction d'un objet de sa définition, de manière à toujours utiliser le même processus de construction.
Il se compose de 4 entités :

  • Un monteur abstrait, définissant les méthodes abstraites des monteurs concrets
  • Un (ou X) monteur concrêt implémentant le constructeur abstrait et construisant effectivement les produits
  • Un directeur qui se charge d'agencer correctement la construction des produits
  • Un produit, qui est l'objet à construire

Voici un exemple de builder :

<?php
/**
* Notre sandwich de base, le PRODUIT
*
*/

class sandwich {
    /**
     * Ses propriétés
     *
     * @var string
     */
    private $sPain;
    private $sContenu;
    private $sSauce;
    
    /**
     * Setter pour le pain
     *
     * @param string $sPain
     */
    public function setPain ($sPain) {
        $this ->sPain = $sPain;
    }
    
    /**
     * Setter pour le contenu
     *
     * @param string $sContenu
     */
    public function setContenu ($sContenu) {
        $this ->sContenu = $sContenu;
    }
    
    /**
     * Setter pour la sauce
     *
     * @param string $sSauce
     */
    public function setSauce ($sSauce) {
        $this ->sSauce = $sSauce;
    }
    
    /**
     * La méthode magique __toString qui va se charger d'afficher une description du sandwich créé
     *
     * @return string
     */
    public function __toString() {
        $sReturn = 'Votre sandwich : '.$this ->sContenu.' dans '.$this ->sPain.' avec '.$this ->sSauce;
        return $sReturn;
    }
    
}

/**
* Notre MONTEUR ABSTRAIT
*
*/
abstract class sandwichBuilder {
    /**
     * Le PRODUIT qu'il va créer
     *
     * @var sandwich
     */
    protected $oSandwich;
    
    /**
     * Getter pour récupérer le PRODUIT créé
     *
     * @return sandwich
     */
    public function getSandwich () {
        return $this ->oSandwich;
    }
    
    /**
     * Initialisation de la création du PRODUIT
     *
     */
    public function createNewSandwich() {
        $this ->oSandwich = new sandwich;
    }
    
    /**
     * Séquence de construction
     *
     */
    abstract public function addPain();
    abstract public function addContenu();
    abstract public function addSauce();
}

/**
* Un MONTEUR CONCRET!
*
*/
class jambonBeurre extends sandwichBuilder {
   
    public function addPain() {
        $this ->oSandwich ->setPain('baguette');
    }

    public function addContenu() {
        $this ->oSandwich ->setContenu('jambon');
    }
    
    public function addSauce() {
        $this ->oSandwich ->setSauce('beurre');
    }
}

/**
* Et un autre...
*
*/
class tomates extends sandwichBuilder {
    
    public function addPain() {
        $this ->oSandwich ->setPain('pain complet');
    }

     public function addContenu() {
        $this ->oSandwich ->setContenu('tomates');
    }
    
    public function addSauce() {
        $this ->oSandwich ->setSauce('huile d'olive');
    }
}

/**
* Notre DIRECTEUR :-) Il va se charger de gérer correctement les séquences nécessaires pour la construction
*
*/
class serveur {
    /**
     * Le MONTEUR CONCRET dont il va se servir
     *
     * @var sandwichBuiledr
     */
    private $oSandwichBuilder;
    
    /**
     * Setter qui va initialiser le MONTEUR CONCRET désiré
     *
     * @param sandwichBuilder $sandwichBuilder
     */
    public function setSandwichBuilder (sandwichBuilder $sandwichBuilder) {
        $this ->oSandwichBuilder = new $sandwichBuilder;
    }
    
    /**
     * Getter, retournant le PRODUIT créé par répercussion
     *
     * @return sandwich
     */
    public function getSandwich () {
        if (is_null ($this ->oSandwichBuilder)) {
            throw new Exception (__METHOD__.'() : aucun sandwichbuilder défini');
        }
        return $this ->oSandwichBuilder ->getSandwich();
    }
    
    /**
     * La séquence de construction du PRODUIT
     *
     */
    public function createSandwich() {
        if (is_null ($this ->oSandwichBuilder)) {
            throw new Exception (__METHOD__.'() : aucun sandwichbuilder défini');
        }
        $this ->oSandwichBuilder ->createNewSandwich();
        $this ->oSandwichBuilder ->addPain();
        $this ->oSandwichBuilder ->addContenu();
        $this ->oSandwichBuilder ->addSauce();
    }
}
try {
    $oJambonBeurre = new jambonBeurre;
    $oTomates = new tomates;
    $oServeur = new serveur;
    $oServeur ->setSandwichBuilder ($oJambonBeurre);
    $oServeur ->createSandwich();
    echo $oServeur ->getSandwich();
    echo '<br />';
    $oServeur ->setSandwichBuilder ($oTomates);
    $oServeur ->createSandwich();
    echo $oServeur ->getSandwich();
} catch (Exception $e) {
    echo $e;
}
?>

PROTOTYPE

Le prototype sert à optimiser la création d'instances complexes. Au lieu de créer une nouvelle instance de la classe, on va cloner la 1ère instance créée. Le clone possède toutes les propriétés de l'original.
On utilise en PHP5 la méthode magique __clone() pour cela : http://fr2.php.net/manual/fr/language.oop5.cloning.php

Voici un exemple très simple :

<?php
/**
* PROTOTYPE abstrait
*
*/
abstract class cookie {
    protected $iInstance;
    protected $sName;
    
    abstract public function setName($sName);
    abstract public function __clone ();
}

/**
* PROTOTYPE concret
*
*/
class chocolateCookie extends cookie {
    protected static $iInstances = 0;
    
    public function setName ($sName) {
        $this ->sName = $sName;
    }
    
    public function __clone () {
        $this ->iInstance = ++self::$iInstances;
    }
}

/**
* PROTOTYPE concret
*
*/
class macadamiaCookie extends cookie {
    protected static $iInstances = 0;

    public function setName ($sName) {
        $this ->sName = $sName;
    }
    
    public function __clone () {
        $this ->iInstance = ++self::$iInstances;
    }
}

/**
* La classe utilisatrice
*
*/
class cookieMachine {
    private $oCookie;
    public static $aCookies;
    
    public function setCookie(cookie $oCookie) {
        $this ->oCookie = $oCookie;
    }
    
    public function makeCookie () {
        self::$aCookies[] = clone $this ->oCookie;
    }
}

/**
* On crée une 1ère instance de chocolateCookie
*/
$oChocCookie = new chocolateCookie;
/**
* Et on lui attribue un nom!
*/
$oChocCookie ->setName ('Chocolate Cookie !');

/**
* Idem avec une instance de macadamiaCookie
*/
$oMacCookie = new macadamiaCookie;
$oMacCookie ->setName ('Macadamia Cookie !');

/**
* On crée une machine à faire les cookies (miam)
*/
$oMachine = new cookieMachine;
$oMachine ->setCookie($oChocCookie);
for ($i = 0; $i < 5; $i ++) {
    $oMachine ->makeCookie();
}

$oMachine ->setCookie($oMacCookie);
for ($i = 0; $i < 10; $i ++) {
    $oMachine ->makeCookie();
}

/**
* Et oui, tous les cookies clonés ont le même nom que le cookie original, alors que l'on n'est pas repassé par cookie::setName()! Par contre, nous avons explicitement modifié la propriété cookie::iInstance dans la méthode cookie::__clone(), donc celle-ci est différente à chaque fois.
*/
print_r (cookieMachine::$aCookies);
?>

La suite : Les Design Patterns Volume 2

Ce document intitulé « Design patterns [1] » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous