Design patterns [2]

DESIGN PATTERNS (ou MOTIFS DE CONCEPTION). PART 2

La partie 1 est disponible ICI
Le volume 3 est paru ICI

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!

J'ai commencé dans la 1ère partie par les design patterns de type CREATION. Ce tuto va s'occuper des design patterns de type STRUCTUREL.

STRUCTUREL

COMPOSITE

Le composite permet de manipuler un groupe d'objets au fonctionnement similaire comme s'il s'agissait d'un seul objet.
Il comprend :

  • Le composant, une interface abstraite définissant le comportement, et déclare l'accès aux composants enfants (feuilles)
  • Le composite : stocke les feuilles et implémente leur gestion
  • La feuille : objet de la composition

L'exemple que je vais vous soumettre est une simulation d'application graphique : nous allons composer un graphique fait de plusieurs éléments graphiques, puis nous allons tous les dessiner en un seul appel.
Cet exemple utilise une classe interne PHP5 de la SPL : SplObjectStorage, objet très pratique, et je profite donc de l'occasion pour vous le présenter en action, même s'il n'est pas nécessaire au motif (mais son fonctionnement l'est, par contre).

Pour rappel, voici la doc de la SPL : http://www.php.net/~helly/php/ext/spl/

<?php
/**
* On redéfinit la classe SplObjectStorage au cas où elle n'existe pas, et SURTOUT pour que vous voyiez son fonctionnement.
*/
if (!class_exists ('SplObjectStorage')) {
    /**
     * Interface SplObjectStorage
     * @author: php
     *
     */
    class SplObjectStorage implements Iterator, Countable {
       
        /**
         * Array of objects
         *
         * @var array
         */
        private $storage = array();
        /**
         * Array of objects index
         *
         * @var int
         */
        private $index = 0;
    
        /**
         * public function rewind
         * Rewind array pointer
         *
         * @Return void
         */
        public function rewind() {
            rewind($this->storage);
        }
    
        /**
         * public function valid
         * Checks if current loop iteration is valid
         *
         * @Return boolean
         */
        public function valid() {
            return key($this->storage) !== false;
        }

        /**
         * public function key
         * Returns current key
         *
         * @Return int
         */
        public function key() {
            return $this->index;
        }
        
        /**
         * public function current
         * Returns current array entry
         *
         * @Return object
         */
        public function current() {
            return current($this->storage);
        }
        
        /**
         * public function next
         * Iterates to the next entry
         *
         * @Return void
         */
        public public function next() {
            next($this->storage);
            $this->index++;
        }
        
        /**
         * public function count
         * Returns the number of entries in the array of objects
         *
         * @Return int
         */
        public function count() {
            return count($this->storage);
        }
        
        /**
         * public function contains
         * Checks if an object is in the collector array
         *
         * @Param object $obj : object to check
         * @Return boolean
         */
        public function contains($obj) {
            if (is_object($obj)) {
                foreach($this->storage as $object) {
                    if ($object === $obj) {
                        return true;
                    }
                }
            }
            return false;
        }
        
        /**
         * public function attach
         * Attaches an object to the collector array
         *
         * @Param object $obj : object to attach
         * @Return void
         */
        public function attach($obj) {
            if (is_object($obj) && !$this->contains($obj)) {
                $this->storage[] = $obj;
            }
        }
        
        /**
         * public function detach
         * Detaches an object to the collector array
         *
         * @Param object $obj : object to detach
         * @Return void
         */
        public function detach($obj) {
            if (is_object($obj)) {
                foreach($this->storage as $idx => $object) {
                    if ($object === $obj) {
                        unset($this->storage[$idx]);
                        $this->rewind();
                        return;
                    }
                }
            }
        }
    }
}
/**
* Notre interface : le COMPOSANT
*
*/
interface graphic {
    public function draw();
}

/**
* Notre COMPOSITE!
*
*/
class compositeGraphic implements graphic {
    private $aGraphics;
    
    public function __construct() {
        $this ->aGraphics = new SplObjectStorage;
    }
    
    public function draw() {
        foreach ($this ->aGraphics as $oGraph) {
            $oGraph ->draw();
        }
    }
   
    public function attach(graphic $oGraph) {
        $this ->aGraphics ->attach($oGraph);
    }

    public function detach(graphic $oGraph) {
        $this ->aGraphics ->detach($oGraph);
    }
}

/**
* Notre FEUILLE
*
*/
class ellipse implements graphic {
    public function draw() {
        echo 'Ellipse'."rn";
    }
}

/**
* Une autre FEUILLE
*
*/
class triangle implements graphic {
    public function draw() {
        echo 'Triangle'."rn";
    }
}

try {
    /**
     * On crée plusieurs ellipses et triangles
     */
    $ellipse1 = new ellipse;
    $ellipse2 = new ellipse;
    $ellipse3 = new ellipse;
    
    $triangle1 = new triangle;
    $triangle2 = new triangle;    
    /**
     * On crée plusieurs compositeGraphic
     */
    $graf = new compositeGraphic;
    $graf1 = new compositeGraphic;
    $graf2 = new compositeGraphic;
    
    /**
     * On attache 2 ellipses et 1 triangle au compositeGraphic 1
     */
    $graf1 ->attach($ellipse1);
    $graf1 ->attach($ellipse2);
    $graf1 ->attach($triangle1);
    
    /**
     * On attache la 3ème ellipse et le deernier triangle au compositeGraphic 2
     */
    $graf2 ->attach($ellipse3);
    $graf2 ->attach($triangle2);
    
    /**
     * On attache les 2 compositeGraphic précédents au 3ème
     */
    $graf ->attach($graf1);
    $graf ->attach($graf2);
    
    /**
     * On trace notre grahique complet !
     */
    $graf ->draw();
} catch (Exception $e) {
    echo $e;
}
?>

PROXY

Le proxy se substitue à une autre classe. Il implémente la même interface (et une seule interface) que cette classe.
Il contrôle l'accès aux méthodes de la classe substituée. Il peut aussi être utilisée pour simplifier ou optimiser l'utilisation de la classe substituée.
Cette fois, 2 exemples. Le premier pour illustrer le contrôle d'accès, le second pour illustrer l'optimisation :
Le 1er exemple, donc, simule l'accès à un compte utilisateur. La classe user possède un numéro de compte auquel on ne doit pas pouvoir accéder sans le mot de passe adéquat. Le proxy vérifie que cette condition soit remplie, et autorise l'accès au numéro de compte uniquement dans ce cas-là :

<?php
/**
* L'interface
*
*/
interface iUser {
    public function getUserAccount();
}

/**
* La classe user qui va être substituée
* Possède un numéro de compte auquel on ne doit pas accéder sans le mot de passe!
*
*/
class user implements iUser {
    private $sAccount = '12345';
    
    public function __construct() {
        echo 'User initialisé<br />';
    }
    
    public function getUserAccount() {
        return $this ->sAccount;
    }
}

/**
* Le proxy se substituant à user.
* Le proxy va vérifier la validité du mot de passe avant de retourner le numéro de compte et l'utilisateur
*
*/
class userProxyProtection implements iUser {
    private $sPassword;
    private $oUser;
    
    public function __construct($sPwd) {
        $this ->sPassword = $sPwd;
        $this ->oUser = new user();
    }
    
    public function getUserAccount() {
        /**
         * Par commodité, je fixe ici le mot de passe demandé. En réalité, on passerait par une interrogation d'une base de données par exemple
         */
        if ($this ->sPassword !== 'motdepasse') {
            throw new Exception ('Mot de passe invalide');
        }
        return $this ->oUser ->getUserAccount();
    }
}

try {
    $oUser = new userProxyProtection('motdepasse');
    echo $oUser ->getUserAccount();
} catch (Exception $e) {
    echo $e;
}
?>

Le 2d exemple simule l'accès à de gros fichiers. On veut afficher leur contenu; il faut donc les charger.
Le proxy va s'occuper de s'assurer que l'on ne charge les fichiers que si c'est nécessaire (si on ne l'a pas déjà chargé et uniquement si on veut l'afficher) :

<?php
/**
* L'interface commune
*
*/
interface iBigFile {
    public function displayFile();
}

/**
* La classe bigFile qui charge et affiche un gros fichier
*
*/
class bigFile implements iBigFile {
    private $sFileName;
    
    public function __construct($sFileName) {
        $this ->sFileName = $sFileName;
        echo 'Charge ',$this ->sFileName, '<br />';
    }
    
    public function displayFile() {
        echo 'Affiche ', $this ->sFileName, '<br />';
    }
}

/**
* Le proxy qui va se substituer à bigFile et ne permettre le chargement d'un fichier QUE si c'est nécessaire (s'il n'a pas été déjà chargé et si l'on demande son affichage)
*
*/
class proxyBigFile implements iBigFile {
    private $sFileName;
    private $oFile;
    
    public function __construct($sFileName) {
        $this ->sFileName = $sFileName;
    }
    
    public function displayFile() {
        if (is_null ($this ->oFile)) {
            $this ->oFile = new bigFile($this ->sFileName);
        }
        $this ->oFile ->displayFile();
    }
}

$aFiles = array (new proxyBigFile('fichier1'), new proxyBigFile('fichier2'), new proxyBigFile('fichier3'));
$aFiles[0] ->displayFile();
$aFiles[1] ->displayFile();
$aFiles[0] ->displayFile(); // ce fichier (fichier1) ne sera pas rechargé puisqu'il l'a déjà été!
// A noter que le fichier3 n'a pas du tout été chargé puisqu'on ne demande pas son affichage!
?>

DELEGATION

La Delegation est une technique avec laquelle un objet possède un comportement mais délègue la responsabilité de l'implémentation de ce comportement à un autre objet.
A noter que cela résulte souvent en une perte de performances, là où l'on gagne en lisibilité.
Ce design pattern est très utile dans les cas où l'on a une multitude de classe héritées dont les comportements semblent très proches mais sont en réalité assez différents : il en ressort alors avec une programmation classique des classes très complexes héritant de-ci de-là et devenant incompréhensibles.

Voici un exemple simple simulant le comportement des hommes et des femmes :

<?php
/**
* Une interface pour décrire les comportements de notre package de classes
*
*/
interface iPeople {
    public function sayName();
    public function sayHello();
    public function drink();
    public function watchTv();
}

/**
* Une classe abstraite parente
* Cette classe va aller chercher les comportements de ses enfants
* Elle délègue...
*
*/
abstract class people implements iPeople {
    protected $sName;
    protected $drinkBehaviour;
    protected $watchTvBehaviour;
    
    public function __construct($sName) {
        $this ->sName = $sName;
    }
    
    public function sayName() {
        echo 'Mon nom est ', $this ->sName,'<br />';
    }
    
    public function sayHello() {
        echo 'Hello !<br />';
    }
    
    public function drink() {
        $this ->drinkBehaviour ->drink();
    }
   
    public function watchTv() {
        $this ->watchTvBehaviour ->watchTv();
    }

    public function setSayNameBehaviour($sayNameBehaviour) {
        $this ->sayNameBehaviour = $sayNameBehaviour;
    }
    
    public function setSayHelloBehaviour($sayHelloBehaviour) {
        $this ->sayHelloBehaviour = $sayHelloBehaviour;
    }
    
    public function setDrinkBehaviour($drinkBehaviour) {
        $this ->drinkBehaviour = $drinkBehaviour;
    }
    
    public function setWatchTvBehaviour($watchTvBehaviour) {
        $this ->watchTvBehaviour = $watchTvBehaviour;
    }
}

/**
* Une interface déclarant un comportement
*
*/
interface drinkBehaviour {
    public function drink();
}

/**
* Un comportement associé à une méthode drink()
* C'est un delegué
*
*/
class drinkBeer implements drinkBehaviour {
    public function drink() {
        echo 'Je bois de la bière'.'<br />';
    }
}

/**
* Un autre comportement, différent de celui ci-dessus, associé aussi à la méthode drink(), destiné à une autre classe
* C'est aussi un délégué
*/
class drinkCosmopolitan implements drinkBehaviour {
    public function drink() {
        echo 'Je bois des Cosmopolitains'.'<br />';
    }
}

/**
* Etc...
*
*/
interface watchTvBehaviour {
    public function watchTv();
}

class watchFootball implements watchTvBehaviour {
    public function watchTv() {
        echo 'Je regarde du foot à la télévision!'.'<br />';
    }
}

class watchDesperateHousewives implements watchTvBehaviour {
    public function watchTv() {
        echo 'Je regarde desperate Housewives à la télévision!'.'<br />';
    }
}

class man extends people implements iPeople {
    public function __construct($sName) {
        parent::__construct($sName);
        $this ->setDrinkBehaviour(new drinkBeer);
        $this ->setWatchTvBehaviour(new watchFootball);
    }
}

class woman extends people implements iPeople {
    public function __construct($sName) {
        parent::__construct($sName);
        $this ->setDrinkBehaviour(new drinkCosmopolitan);
        $this ->setWatchTvBehaviour(new watchDesperateHousewives);
    }
}

/**
* On teste les comportements :-)
*/
$oMan = new man('Paul');
$oWoman = new woman('Ingrid');
$oMan ->sayName();
$oWoman ->sayName();
$oMan ->sayHello();
$oWoman ->sayHello();
$oMan ->drink();
$oWoman ->drink();
$oMan ->watchTv();
$oWoman ->watchTv();
?>

FLYWEIGHT

Le flyweight (ou poids mouche) sert à limiter les ressources prises par une application demandant beaucoup de petites instances.
Il va se charger de sélectionner des objets déjà instanciés pouvant être réutilisés. Et avec une bonne méthodologie, on pourra tout de même obtenir avec un même objet, des comportements différents (en déportant des propriétés de l'objet vers des arguments de méthodes).

Petit exemple d'affichage html d'un texte :

<?php
/**
* Notre petite classe destinée à être instanciées de multiples fois sans l'intervention du flyweight
*
*/
class char {
    private $sChar;
    private $sFont;
    
    public function __construct($sChar, $sFont) {
        $this ->sChar = $sChar;
        $this ->sFont = $sFont;
    }
    
    public function printChar($iId) {
        echo '<span style=\"font-family:'.$this ->sFont.'\" id=\"'.$iId.'\">'.$this ->sChar.'</span>';
    }
}

/**
* Notre flyweight. Il stocke des char et ne va les instancier que s'il n'en a pas déjà stocké un identique
*
*/
class charFactory extends ArrayIterator {
    
    public function get($sChar, $sFont) {
        $sKey = $sChar.$sFont;
        if (!$this ->offsetExists($sKey)) {
            $this ->offsetSet($sKey, new char($sChar, $sFont));
        }
        return $this[$sKey];
    }
}
/**
* On teste
*/
$oText = new charFactory;
$aTab = array();
$aTab[]=$oText ->get('H', 'Arial');
$aTab[]=$oText ->get('E', 'Arial');
$aTab[]=$oText ->get('L', 'Verdana');
$aTab[]=$oText ->get('L', 'Verdana');
$aTab[]=$oText ->get('O', 'Arial');
$iCpt = 1;
foreach ($aTab as $oChar) {
    $oChar ->printChar('_'.$iCpt);
    $iCpt ++;
}
/**
* Ici, on se rend compte qu'on aurait instancié 5 objets si l'on n'avait pas utilisé le flyweight (1 pour chaque lettre du mot HELLO).
* Le flyweight n'en a instancié que 4, puisque les 2 L sont identiques (L VERDANA) : il a simplement réutilisé le même objet.
* Néanmoins, l'affichage est unique pour chacun puisqu'on pass un ID unique pour chaque, histoire de montrer que l'on peut, avec un même objet, si l'on déporte certaines variables qui pourraient être des propriétés vers des arguments des méthodes, obtenir des objets avec un comportement différent.
*/
echo $oText ->count();
?>

BRIDGE

Le BRIDGE permet de découpler une interface et son implémentation, de manière à ce qu'elles puissent évoluer séparément.
Il se compose de :

  • L'ABSTRACTION : définit l'interface abstraite et maintient la référence à l'IMPLEMENTOR
  • La REFINED ABSTRACTION : le BRIDGE. Etend l'ABSTRACTION.
  • L'ABSTRACT IMPLEMENTOR : définit l'interface pour l'implémentation des classes.
  • Le CONCRETE IMPLEMENTOR : les classes...

Un petit exemple où l'on va pouvoir choisir à moindre coût de générer du joli HTML tout neuf, ou du vieil HTML tout pourri... ;-) :

<?php
/**
* ABSTRACT IMPLEMENTOR, interface définissant les méthodes des CONCRETE IMPLEMENTORS
*
*/
interface writeHtml {
    public function writeBold($sWord);
    public function writeItalic($sWord);
}

/**
* CONCRETE IMPLEMENTOR pour le html récent
*
*/
class writeHtmlNew implements writeHtml {
    public function writeBold($sWord) {
        echo htmlspecialchars('<strong>'.$sWord.'</strong>');
    }
    
    public function writeItalic($sWord) {
        echo htmlspecialchars('<em>'.$sWord.'</em>');
    }
}

/**
* CONCRETE IMPLEMENTOR pour le vieux html moche
*
*/
class writeHtmlOld implements writeHtml {
    public function writeBold($sWord) {
        echo htmlspecialchars('<b>'.$sWord.'</b>');
    }
    
    public function writeItalic($sWord) {
        echo htmlspecialchars('<i>'.$sWord.'</i>');
    }
}

/**
* ABSTRACTION, qui définit les méthodes que la REFINDES ABSTRACTION utilisera
*
*/
interface htmlTpl {
    public function writeHtmlBold();
    public function writeHtmlItalic();
}

/**
* REFINED ABSTRACTION, qui joue le rôle de BRIDGE
*
*/
class htmlTplRefined {
    private $oHtmlType;
    private $sWord;
    
    public function __construct($sWord, writeHtml $htmlType) {
        $this ->oHtmlType = $htmlType;
        $this ->sWord = $sWord;
    }
    
    public function writeHtmlBold() {
        $this ->oHtmlType ->writeBold($this ->sWord);
        
    }
    public function writeHtmlItalic() {
        $this ->oHtmlType ->writeItalic($this ->sWord);
    }
}
$oHtmlNew = new htmlTplRefined('test', new writeHtmlNew);
$oHtmlOld = new htmlTplRefined('test', new writeHtmlOld);

$oHtmlNew ->writeHtmlBold();
echo'<br />';

$oHtmlNew ->writeHtmlItalic();
echo'<br />';

$oHtmlOld ->writeHtmlBold();
echo'<br />';

$oHtmlOld ->writeHtmlItalic();
?>

ADAPTER

L'ADAPTER permet d'adapter l'interface d'une classe à une autre interface.
Prenons un exemple de la vie courante : vous avez votre portable et sa recharge, prévu pour des prises françaises.
Les prises françaises fournissent de l'électricité. Mais vous êtes en Irlande. Les prises irlandaises fournissent aussi de l'électricité (si si), mais n'utilisent pas la même interface que les prises française. Vous utilisez donc un adaptateur pour brancher votre recharge sur la prise irlandaise.
Le petit exemple de code va se baser sur un contexte différent : vous avez trouvé une classe permettant de déplacer une voiture.
Vous avez écrit votre propre interface de déplacement, et avez besoin de créer une classe permettant de déplacer un homme.
Malheureusement, l'interface de la classe de déplacement de voiture n'est pas la même que la votre.
A noter que l'interface à adapter doit possèder au moins toutes les méthodes nécessaires à l'interface adaptée pour que l'utilisation de ce design pattern soit intéressante, et peut en possèder plus que nécessaire.

Donc, vous adaptez :-) :

<?php
/**
* L'interface que vous devez utiliser
*
*/
interface walk {
    public function walkLeft();
    public function walkRight();
    public function walkForward();
    public function walkBackward();
}

/**
* L'interface à adapter
*
*/
interface drive {
    public function driveLeft();
    public function driveRight();
    public function driveForward();
    public function driveBackward();
}

/**
* La classe implémentant l'interface à adapter
*
*/
class driveCar implements drive {
    protected $aPos = array('X' => 0, 'Y' => 0);
    
    public function driveLeft() {
        $this ->aPos['X'] -= 1;
    }

    public function driveRight() {
        $this ->aPos['X'] += 1;
    }
    
    public function driveForward() {
        $this ->aPos['Y'] += 1;
    }
    
    public function driveBackward() {
        $this ->aPos['Y'] -= 1;
    }
    
    public function getCarPos () {
        return $this ->aPos;
    }
}

/**
* Votre classe ADAPTER
*
*/
class walkGuy extends driveCar implements walk {
    
    public function walkLeft() {
        $this ->driveLeft();
    }
    
    public function walkRight() {
        $this ->driveRight();    
    }
    
    public function walkForward() {
        $this ->driveForward();
    }
    
    public function walkBackward() {
        $this ->driveBackward();
    }
    
    public function getPos() {
        return $this ->getCarPos();
    }
}
/**
* Test
*/
$oGuy = new walkGuy;
print_r ($oGuy ->getPos());
$oGuy ->walkRight();
$oGuy ->walkForward();
print_r ($oGuy ->getPos());
?>

DECORATOR

Le DECORATOR (ou décorateur) sert à ajouter dynamiquement de nouvelles fonctionnalités à un objet. C'est une technique plus souple que l'héritage, et qui dont peut parfois être très utile (pour simuler du multi-héritage par exemple).
Il se compose de :

  • Une INTERFACE pour la classe à décorer
  • La classe à décorer
  • Un ABSTRACT DECORATOR qui implémente l'INTERFACE de l'objet à décorer
  • X CONCRETE DECORATOR

Nous aurons ici 2 exemples. Pourquoi 2 ? Le 1er va vous montrer comment utiliser ce design pattern en PHP. Néanmoins, par rapport à d'autres langages, PHP ne permet pas aussi facilement d'appliquer cette technique comme elle l'est souvent : c'est à dire en imbriquant les CONCRETE DECORATOR afin d'hériter de chacunes de leurs spécificités.

Le 2d exemple va donc vous montrer comment on peut faire en PHP, en appliquant un autre design pattern que vous connaissez tous : la FACTORY.

1er exemple donc : on va dessiner un carré! La classe de base se contente de dessiner un carré dimensionné, sans plus de frioriture.
Les DECORATOR, au nombre de deux, vont, pour l'un, permettre de colorier le carre, et pour l'autre d'y ajouter une bordure.

Par contre, c'est l'un, ou l'autre ! :

<?php
/**
* L'INTERFACE de la classe à décorer
*
*/
interface box {
    public function dim($iWidth, $iHeight);
    public function draw();
}

/**
* La classe à décorer
*
*/
class simpleBox implements box {
    public $aStyle;
    
    public function dim($iHeight, $iWidth) {
        if (!is_int($iWidth) || !is_int($iHeight)) {
            throw new Exception('Parameters must be integers');
        }
        $this ->aStyle['width'] = 'width:'.$iWidth.'px';
        $this ->aStyle['height'] = 'height:'.$iHeight.'px';
    }
    
    public function draw() {
        if (is_null($this ->aStyle['width']) || is_null($this ->aStyle['height'])) {
            throw new Exception('You must dim the box first');
        }
        $sStyle = implode(';', $this ->aStyle);
        echo '<div style=\"'.$sStyle.';\"></div>';
    }
}

/**
* L'ABSTRACT DECORATOR qui implémente l'interface de la classe à décorer
*
*/
abstract class boxDecorator implements box {
    protected $oBox;
    
    public function __construct(box $o) {
        $this ->oBox = $o;
    }
}

/**
* Un premier CONCRETE DECORATOR
*
*/
class coloredBox extends boxDecorator {
    
    public function __construct(box $o, $sColor) {
        parent::__construct($o);
        $this ->oBox ->aStyle['color'] = 'background-color:'.$sColor;
    }
    
    public function dim($iHeight, $iWidth) {
        $this ->oBox ->dim($iHeight, $iWidth);
    }
    
    public function draw() {
        $this ->oBox ->draw();
    }
}
/**
* Et un second :-)
*
*/
class borderedBox extends boxDecorator {
    
    public function __construct(box $o, $iBorder, $sBorderStyle, $sBorderColor) {
        parent::__construct($o);
        if (!is_int($iBorder)) {
            throw new Exception('Border thickness must be an integer');
        }
        $this ->oBox ->aStyle['border'] = 'border:'.$iBorder.'px '.$sBorderStyle.' '.$sBorderColor;
    }
    
    public function dim($iHeight, $iWidth) {
        $this ->oBox ->dim($iHeight, $iWidth);
    }
    
    public function draw() {
        $this ->oBox ->draw();
    }
}

/**
* Les tests ! On va dessiner 2 jolis carrés :-)
*/
$oBox = new coloredBox (new simpleBox, 'blue');
$oBox ->dim(50,50);
$oBox ->draw();
$oBox = new borderedBox (new simpleBox, 1, 'dotted', 'red');
$oBox ->dim(50,50);
$oBox ->draw();
?>

Le 2d exemple donc qui va nous permettre de dessiner 1 seul carré qui aura hérité des spécificités de la classe de base ET des deux DECORATOR :

<?php
/**
* L'INTERFACE de la classe à décorer
*
*/
interface box {
    public function dim($iWidth, $iHeight);
    public function draw();
}

/**
* La classe à décorer
*
*/
class simpleBox implements box {
    public $aStyle;
    
    public function dim($iHeight, $iWidth) {
        if (!is_int($iWidth) || !is_int($iHeight)) {
            throw new Exception('Parameters must be integers');
        }
        $this ->aStyle['width'] = 'width:'.$iWidth.'px';
        $this ->aStyle['height'] = 'height:'.$iHeight.'px';
    }
    
    public function draw() {
        if (is_null($this ->aStyle['width']) || is_null($this ->aStyle['height'])) {
            throw new Exception('You must dim the box first');
        }
        $sStyle = implode(';', $this ->aStyle);
        echo '<div style=\"'.$sStyle.';\"></div>';
    }
}

/**
* L'ABSTRACT DECORATOR qui implémente l'interface de la classe à décorer
* Notez que cette fois, l'objet boxDecorator::oBox est une propriété statique, et que l'on a plus de constructeur
* A la place, on a une méthode d'usinage :-)
*
*/
abstract class boxDecorator implements box {
    protected static $oBox;
    
    public function getInstance(box $o) {
        self::$oBox = $o;
        return self::$oBox;
    }
}

/**
* Un premier CONCRETE DECORATOR
* Pas non plus de constructeur, mais une méthode d'usinage aussi!
*
*/
class coloredBox extends boxDecorator {
    
    public function getInstance(box $o, $sColor) {
        self::$oBox = parent::getInstance($o);
        self::$oBox ->aStyle['color'] = 'background-color:'.$sColor;
        return self::$oBox;
    }
    
    public function dim($iHeight, $iWidth) {
        self::$oBox ->dim($iHeight, $iWidth);
    }
    
    public function draw() {
        self::$oBox ->draw();
    }
}

/**
* Et un second :-)
*
*/
class borderedBox extends boxDecorator {
    
    public function getInstance(box $o, $iBorder, $sBorderStyle, $sBorderColor) {
        self::$oBox = parent::getInstance($o);
        if (!is_int($iBorder)) {
            throw new Exception('Border thickness must be an integer');
        }
        self::$oBox ->aStyle['border'] = 'border:'.$iBorder.'px '.$sBorderStyle.' '.$sBorderColor;
        return self::$oBox;
    }
    
    public function dim($iHeight, $iWidth) {
        self::$oBox ->dim($iHeight, $iWidth);
    }
    
    public function draw() {
        self::$oBox ->draw();
    }
}

/**
* Le test ! On va cette fois ne dessiner qu'un seul carré, utilisant les spécificités des deux DECORATOR! (et de la classe de base, évidemment)
*/
$oBox = borderedBox::getInstance(coloredBox::getInstance(new simpleBox, 'blue'), 1, 'dotted', 'red');
$oBox ->dim(50,50);
$oBox ->draw();
?>

La suite c'est par ICI...

Ce document intitulé « Design patterns [2] » 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