Design patterns [3]

DESIGN PATTERNS (ou MOTIFS DE CONCEPTION) - PART 3

Ce tutoriel est composé de trois parties. Vous retrouverez les deux premières ici :
Les design patterns Volume 1
Les Design Patterns Volume 2

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, puis dans la 2de, les design patterns de type STRUCTUREL. Ce tuto va s'occuper des design patterns de type COMPORTEMENT.
Cette dernière partie est un peu plus longue que les précédentes, et les exemples plus complexes. J'estime, si vous avez lu les précédents, que vous êtes maintenant suffisamment aguerris :-) Mais il y a ici des design patterns particulièrement sympas!

COMPORTEMENT

TEMPLATE METHOD

Le TEMPLATE METHOD (patron de méthode) est un design pattern très simple, que tous ceux programmant en POO PHP5 connaissent : il s'agit de définir un template pour un ensemble de sous-classes. Ce template va se charger de déclarer et/ou d'implémenter des méthodes communes nécessaires à toutes ses sous-classes. Une méthode implémentée sera réutilisée telle quelle par les sous-classes; les méthodes simplement déclarées devront être implémentées dans les sous-classes en suivant la déclaration.
Pour ce faire, on utilisera les classes abstraites.
La classe abstraite (le TEMPLATE) définira comme FINAL les méthodes qu'il implémentera : il s'agit ici de fixer le template. Elles ne pourront donc pas être surchargées par les sous-classes.
Un petit exemple simple avec 2 jeux de dés où l'on a qu'une seule chance de jeter les dés : un 421, et un ou l'on doit faire 3 fois six :

<?php
/**
* Le TEMPLATE ! La classe abstraite parente
*
*/
abstract class threeDiceGames {
    protected $aThrowResults = array();
    protected $iNumberOfDice = 3;
    final protected function throwDie() {
        return mt_rand(1,6);
    }
    final protected function throwDice() {
        for ($iCpt = 0; $iCpt < $this ->iNumberOfDice; $iCpt++) {
            $this ->aThrowResults[] = $this ->throwDie();
        }
    }
    final public function play() {
        $this ->aThrowResults = array();
        $this ->throwDice();
        $this ->showResult();
        if (true === $this ->checkHasWon()) {
            $this ->won();
        } else {
            $this ->lost();
        }
    }
    final protected function showResult() {
        echo 'Résultats:<br />';
        foreach ($this ->aThrowResults as $iResult) {
            echo $iResult.'<br />';
        }
    }
    abstract protected function checkHasWon();
    abstract protected function lost();
    abstract protected function won();
}

/**
* Une sous-classe héritant du TEMPLATE, et implémentant uniquement les méthodes abstraites. Les méthodes implémentées
* dans le TEMPLATE seront utilisées telles quelles, et ne peuvent être redéfinies
*
*/
class forTwoOne extends threeDiceGames {
    protected function checkHasWon() {
        if (in_array(4, $this ->aThrowResults) && in_array(2, $this ->aThrowResults) && in_array(1, $this ->aThrowResults)) {
            return true;
        }
        return false;
    }
    protected function won() {
        echo 'Bravo, vous avez gagné, vous avez fait 421!<br />';
    }
    protected function lost() {
        echo 'Dommage, vous avez perdu, vous n'avez pas fait 421...<br />';
    }
}

/**
* Une autre sous-classe héritant du TEMPLATE
*
**/
class sixSixSix extends threeDiceGames {
    protected function checkHasWon() {
        if (18 === array_sum($this ->aThrowResults)) {
            return true;
        }
        return false;
    }
    protected function won() {
    echo 'Bravo, vous avez gagné, vous avez fait 3*6!<br />';
    }
    protected function lost() {
        echo 'Dommage, vous avez perdu, vous n'avez pas fait 3*6...<br />';
    }
}
$forTwoOne = new forTwoOne;
$sixSixSix = new sixSixSix;
$forTwoOne ->play();
$sixSixSix ->play();
?>

STRATEGIE

La STRATEGIE permet de sélectionner un algorithme en fonction du contexte.
Elle se compose de :

  • Une STRATEGIE, qui est l'interface déclarant les actions des STRATEGIES CONCRETES
  • Des STRATEGIES CONCRETES
  • Un CONTEXTE qui va sélectionner la STRATEGIE adaptée au contexte courant.

J'ai, il y a quelques temps, publié un package de validation des données utilisateurs basé sur ce principe, bien plus complet que l'exemple ci-dessous. Il se trouve ICI.

Et maintenant, l'exemple simple :

<?php
/**
* L'interface de STRATEGIE déclarant les actions des STRATEGIES CONCRETES
*
*/
interface strategie {
    public function validate($mData);
}

/**
* Une STRATEGIE CONCRETE validant les emails
*
*/
class validateEmail implements strategie {
    public function validate($mData) {
        if (!preg_match (''^[[:alnum:]]([-_.]?[[:alnum:]])*@[[:alnum:]]([-.]?[[:alnum:]])*.([a-z]{2,4})$'', $mData)) {
            return false;
        }
        return true;
    }
}

/**
* Une STRATEGIE CONCRETE validant les url
*
*/
class validateUrl implements strategie {
    public function validate($mData) {
        if (!preg_match (''((?:https?|ftp)://S+[[:alnum:]]/?)'si', $mData) && !preg_match (''((?<!//)(www.S+[[:alnum:]]/?))'si', $mData)) {
            return false;
        }
        return true;
    }
}

/**
* Une STRATEGIE CONCRETE validant les entiers
*
*/
class validateInt implements strategie {
    public function validate($mData) {
        if (!preg_match(''[0-9]+'', $mData)) {
            return false;
        }
        return true;
    }
}

/**
* Une STRATEGIE CONCRETE validant les chaines alphanumériques
*
*/
class validateAlphaNum implements strategie {
    public function validate($mData) {
        if (!preg_match(''[1-9a-zA-Z]+'', $mData)) {
            return false;
        }
        return true;
    }
}

/**
* La classe de CONTEXTE choisissant la stratégie à employer selon le contexte
*
*/
class dataValidator {
    private $oStrategie;
    public function setStrategie(strategie $strategie) {
        $this ->oStrategie = $strategie;
    }
    public function validate($mData) {
        if (is_string($mData) && trim($mData) === '') {
            return false;
        }
        return $this ->oStrategie ->validate($mData);
    }
}

/**
* On teste tout ça avec différentes données
*/
try {
    $dataValidator = new dataValidator;
    $mData = 'moi@home.net';
    $dataValidator ->setStrategie(new validateEmail);
    if (true === $dataValidator ->validate ($mData)) {
        echo 'Valide';
    } else {
        echo 'Invalide';
    }
    echo '<br />';
    $mData = 12345;
    $dataValidator ->setStrategie (new validateInt);
    if (true === $dataValidator ->validate ($mData)) {
        echo 'Valide';
    } else {
        echo 'Invalide';
    }
    echo '<br />';
    $mData = '';
    $dataValidator ->setStrategie (new validateAlphaNum);
    if (true === $dataValidator ->validate ($mData)) {
        echo 'Valide';
    } else {
        echo 'Invalide';
    }
} catch (Exception $e) {
    echo $e;
}
?>

OBSERVATEUR

L'Observateur (observer en anglais) est un design pattern permettant d'écouter un objet et de réagir à certains évènements.
Tout l'intérêt est de pouvoir, sur 1 évènement donné, provoquer toute une série de réactions en cascade, un peu comme des triggers en SQL.
Il se compose de :

  • Un SUJET ABSTRAIT, l'interface qui déclare 3 méthodes nécessaires au SUJET CONCRET: attach(), detach() et notify()
  • Un SUJET CONCRET qui implémente le SUJET ABSTRAIT. Il s'agit de l'objet observé. On y intègre la gestion d'un état sur lequel vont se baser les OBSERVATEURS
  • Un OBSERVATEUR ABSTRAIT, l'interface qui déclare la méthode notify() nécessaire aux OBSERVATEURS
  • Un OBSERVATEUR CONCRET qui implémente l'OBSERVATEUR ABSTRAIT et réagit aux évènements qui le concerne.

Je vais reprendre un exemple que j'ai déjà publié dans les sources ICI.
Il s'agit ici d'observer la vitesse d'une voiture... On y retrouve une classe déjà utilisée dans la partie 1 de ce tuto, SplObjectStorage. J'ai aussi réécrit toutes les interfaces internes à PHP5 au cas où elles n'existent pas sur votre version de PHP, et surtout pour que vous puissiez examiner leur code. Elles proviennent toutes de la SPL (http://www.php.net/~helly/php/ext/spl/)

De même, cet exemple utilise aussi un design pattern vu dans le 2de partie de ce tuto, le COMPOSITE. Ici, il sert à stocker et manipuler tous les OBSERVATEURS liés au SUJET.

Voici l'exemple :

<?php
/**
* defining abstract implementations
*/
if (!interface_exists ('Countable')) {
    /**
    * Interface Countable
    * @author: php
    *
    */
    interface Countable {
        public function count();
    }
}
if (!interface_exists ('SplObserver')) {
    /**
    * OBSERVATEUR ABSTRAIT
    * Interface SplObserver
    * @author: php
    *
    */
    interface SplObserver {
        public function update(SplSubject $subject);
    }
}
if (!interface_exists ('SplSubject')) {
    /**
    * SUJET ABSTRAIT
    * Interface SplSubject
    * @author: php
    *
    */
    interface SplSubject {
        public function attach(SplObserver $observer);
        public function detach(SplObserver $observer);
        public function notify();
    }
}
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;
                    }
                }
            }
        }
    }
}
/**
* abstract class SplSubjectComposite implements SplSubject
* @author: Johan Barbier <johan.barbier@gmail.com>
*
*/
abstract class SplSubjectComposite implements SplSubject {
    /**
    * state of the subject
    *
    * @var mixed
    */
    protected $state;
    /**
    * collector array of observers
    *
    * @var SplObjectStorage
    */
    protected $aObservers;
    /**
    * public function __construct
    * constructor
    *
    */
    protected function __construct () {
        $this ->aObservers = new SplObjectStorage;
    }
    /**
    * final protected function setState
    * sets the object's state
    *
    * @param mixed $state
    */
    final protected function setState ($state) {
        $this ->state = $state;
    }
    /**
    * final protected function getState
    * gets the object's state
    *
    * @return mixed
    */
    final protected function getState () {
        return $this ->state;
    }
    /**
    * public function attach
    * Attaches an observer
    *
    * @Param SplObserver $observer : the observer to attach
    * @Return boolean
    */
    final public function attach(SplObserver $observer) {
        if(!$this->aObservers->contains($observer)) {
            $this->aObservers->attach($observer);
        }
        return true;
    }
    /**
    * public function detach
    * Detaches an observer
    *
    * @Param SplObserver $observer : the observer to detach
    * @Return boolean
    */
    final public function detach(SplObserver $observer) {
        if(!$this->aObservers->contains($observer)) {
            return false;
        }
        $this->aObservers->detach($observer);
        return true;
    }
    /**
    * public function notify
    * Notify all the observers
    *
    * @Return void
    */
    final public function notify() {
        foreach($this->aObservers as $observer) {
            $observer->update($this);
        }
    }
}
/**
* starting concrete implementations
*/
/**
* SUJET CONCRET
* class car extends SplSubjectComposite
* @author: Johan Barbier <johan.barbier@gmail.com>
*
*/
class car extends SplSubjectComposite {
    /**
    * Name of the car
    *
    * @var string
    */
    private $sName;
    /**
    * State of the car
    *
    * @var int
    */
    private $iSpeed = 0;

    /**
    * States constants
    *
    */
    const _STOPPED = 0;
    const _STARTED = 1;
    /**
    * public function __construct
    * constructor
    * Initializes car's nname
    *
    * @param string $sName
    */
    public function __construct ($sName) {
        parent::__construct ();
        $this ->sName = $sName;
    }
    /**
    * public function start
    * Starts the car and notify observers
    *
    * @Return void
    */
    public function start () {
        $this ->setState(self::_STARTED);
        $this ->notify();
    }
    /**
    * public function stop
    * Stops the car and notify observers
    *
    * @Return void
    */
    public function stop () {
        $this ->setState(self::_STOPPED);
        $this ->iSpeed = 0;
        $this ->notify();
    }
    /**
    * public function accelerate
    * Accelerates the car and notify the observers
    *
    * @Param int $iAcceleration : how many Kmh you want to accelerate
    * @Return void
    */
    public function accelerate ($iAcceleration) {
        if (self::_STOPPED === $this ->getState()) {
            throw new Exception ('You must start the car before accelerating...');
        }
        if (!is_int ($iAcceleration) || $iAcceleration < 0) {
            throw new Exception ('Wrong value for car::accelerate()');
        }
        $this ->iSpeed += $iAcceleration;
        $this ->notify();
    }
    /**
    * public function __get
    * Get properties value
    *
    * @Param string $sProp : property's name
    * @Return mixed
    */
    public function __get ($sProp) {
        switch ($sProp) {
            case 'STATE' :
                return $this ->getState();
                break;

            case 'SPEED' :
                return $this ->iSpeed;
                break;

            case 'NAME' :
                return $this ->sName;
                break;

            default :
                throw new Exception ($sProp.' cannot be read');
        }
    }
    /**
    * public function __set
    * Set properties value
    *
    * @Param string $sProp : property's name
    * @Param mixed $mVal : property's value
    * @Return void
    */
    public function __set ($sProp, $mVal) {
        throw new Exception ($sProp.' cannot be set');
    }
}
/**
* OBSERVATEUR CONCRET
* class carStateObserver implements SplObserve
* @author Johan Barbier <johan.barbier@gmail.com>
*
*/
class carStateObserver implements SplObserver {
    /**
    * Stored subject's state
    *
    * @var int
    */
    private $iSubjectState;
    /**
    * public function update
    * updater. Echoes subject's state (if the car is stopped or started)
    *
    * @param SplSubject $subject
    * @return void
    */
    public function update(SplSubject $subject) {
        switch ($subject ->STATE) {
            case car::_STOPPED :
                if (is_null ($this ->iSubjectState)) {
                    echo $subject ->NAME.' has not been started<br />';
                } else {
                    echo $subject ->NAME.' has been stopped<br />';
                }
                $this ->iSubjectState = 0;
                break;

            case car::_STARTED :
                if (1 !== $this ->iSubjectState) {
                    echo $subject ->NAME.' has been started<br />';
                    $this ->iSubjectState = 1;
                }
                break;

            default :
                throw new Exception ('Unexpected error in carStateObserver::update()');
        }
    }
}
/**
* OBSERVATEUR CONCRET
* class carSpeedObserver implements SplObserver
* @author Johan Barbier <johan.barbier@gmail.com>
*
*/
class carSpeedObserver implements SplObserver {
    /**
    * public function update
    * updater. Echoes subject's speed
    *
    * @param SplSubject $subject
    * @return void
    */
    public function update(SplSubject $subject) {
        if (car::_STOPPED !== $subject ->STATE) {
            echo $subject ->NAME.' speed is '.$subject ->SPEED.'Kmh<br />';
        }
    }
}
/**
* OBSERVATEUR CONCRET
* class carOverspeedObserver implements SplObserver
* @author Johan Barbier <johan.barbier@gmail.com>
*
*/
class carOverspeedObserver implements SplObserver {
    /**
    * public function update
    * updater. Catches the subject if it is overspeeding!
    *
    * @param SplSubject $subject
    * @return void
    */
    public function update(SplSubject $subject) {
        if ($subject ->SPEED > 130) {
            throw new Exception ('Speed limit is 130! You lost your driver license !');
        }
    }
}
/**
* Et maintenant, on teste tout ça :-)
*
*/
try {
    $oCar = new car ('AUDI A4');
    $oObs = new carStateObserver;
    $oObs2 = new carSpeedObserver;
    $oObs3 = new carOverspeedObserver;
    $oCar ->attach($oObs);
    $oCar ->attach($oObs2);
    $oCar ->attach($oObs3);
    $oCar ->start();
    $oCar ->accelerate(20);
    $oCar ->accelerate(30);
    $oCar ->stop();
    $oCar ->start();
    $oCar ->accelerate(50);
    $oCar ->accelerate(70);
    $oCar ->accelerate(100);
    $oCar ->accelerate(150);
} catch (Exception $e) {
    echo $e ->getMessage ();
}
?>

MEMENTO

Le memento permet d'effectuer la sauvegarde de l'état d'une classe, et de restaurer cet état (un peu comme un rollback en SQL).
Il est composé de :

  • L'ORIGINATOR, qui est la classe pouvant être restaurée. Cette classe possède un état.
  • Le MEMENTO qui est chargé de la sauvegarde et de la restauration.
  • Le CARETAKER qui est chargé d'utiliser le MEMENTO.

Un petit exemple abstrait, avec la restauration de l'état de l'objet :

<?php
/**
* L'ORIGINATOR, la classe qui va être sauvegardée et restaurée
*
*/
class originator {
    private $iState;
    public function set($iState) {
        if (!is_int($iState)) {
            throw new Exception('State must be an integer');
        }
        $this ->iState = $iState;
        echo 'Etat initialisée à '.$this ->iState.'<br />';
    }
    public function saveToMemento() {
        echo 'Sauvegarde!<br />';
        return new memento($this ->iState);
    }
    public function restoreFromMemento($oMemento) {
        if (!$oMemento instanceof memento) {
            throw new Exception(__METHOD__.'() : parameter given must be a memento object');
        }
        $this ->iState = $oMemento ->getSavedState();
        echo 'Etat restauré via le memento : '.$this ->iState.'<br />';
    }
}

/**
* Le MEMENTO qui se charge de sauvegarder l'état et de le retourner si demandé
*
*/
class memento{
    private $iState;
    public function __construct($iState) {
        $this ->iState = $iState;
    }
    public function getSavedState() {
        return $this ->iState;
    }
}
/**
* Le CARETAKER qui fait les demandes au MEMENTO
*
*/
class caretaker extends ArrayIterator {
    public function addMemento($oMemento) {
        $this[] = $oMemento;
    }
    public function getMemento($iOffset) {
        if(!$this ->offsetExists($iOffset)) {
            throw new Exception('No saved memento at position '.$iOffset);
        }
        return $this[$iOffset];
    }
}

try {
    $oCaretaker = new caretaker;
    $oOriginator = new originator;
    $oOriginator ->set(1);
    $oOriginator ->set(2);
    $oCaretaker ->addMemento($oOriginator ->saveToMemento());
    $oOriginator ->set(3);
    $oCaretaker ->addMemento($oOriginator ->saveToMemento());
    $oOriginator ->set(4);
    $oOriginator ->restoreFromMemento($oCaretaker ->getMemento(1));
} catch(Exception $e) {
    echo $e;
}
?>

Et un exemple concret à peine différent, au fond :

<?php
/**
* L'ORIGINATOR, la classe qui va être sauvegardée et restaurée
*
*/
class button {
    private $sColor = 'white';
    private $sText = 'Hello';
    private $sBorder = 'black';
    private $sState;
    private $aUserFriendlyProps = array('COLOR', 'TEXT', 'BORDERCOLOR');
    public function __set($sProp, $sVal) {
        if (!in_array($sProp, $this ->aUserFriendlyProps)) {
            throw new Exception ('Property '.$sProp.' cannot be set');
        }
        switch ($sProp) {
            case 'COLOR':
                $this ->sColor = $sVal;
                break;

            case 'TEXT':
                $this ->sText = $sVal;
                break;

            case 'BORDERCOLOR':
                $this ->sBorder = $sVal;
                break;
        }
    }
    public function __toString() {
        $sHtml = '<span style="background-color:'.$this ->sColor.';border: 1px solid '.$this ->sBorder.';">'.$this ->sText.'</span><br />';
        return $sHtml;
    }
    public function saveToMemento() {
        echo 'Sauvegarde!<br />';
        $this ->sState = serialize(array('sColor' => $this ->sColor, 'sText' => $this ->sText, 'sBorder' => $this ->sBorder));
        return new memento($this ->sState);
    }
    public function restoreFromMemento($oMemento) {
        if (!$oMemento instanceof memento) {
            throw new Exception(__METHOD__.'() : parameter given must be a memento object');
        }
        $this ->sState = $oMemento ->getSavedState();
        $aProps = unserialize($this ->sState);
        foreach ($aProps as $sProp => $sVal) {
        $this -> $sProp = $sVal;
    }
    echo 'Etat restauré via le memento<br />';
    }
}
/**
* Le MEMENTO qui se charge de sauvegarder l'état et de le retourner si demandé
*
*/
class memento{
    private $sState;
    public function __construct($sState) {
        $this ->sState = $sState;
    }
    public function getSavedState() {
        return $this ->sState;
    }
}
/**
* Le CARETAKER qui fait les demandes au MEMENTO
*
*/
class caretaker extends ArrayIterator {
    public function addMemento($oMemento) {
        $this ->append($oMemento);
        return $this ->count() - 1;
    }
    public function getMemento($iOffset) {
        if(!$this ->offsetExists($iOffset)) {
            throw new Exception('No saved memento at position '.$iOffset);
        }
        return $this ->offsetGet($iOffset);
    }
}

try {
    $oCaretaker = new caretaker;
    $oButton = new button;
    $oButton ->COLOR = 'orange';
    $oButton ->TEXT = 'Mon bouton';
    echo $oButton;
    $iSavedPosition1 = $oCaretaker ->addMemento($oButton ->saveToMemento());
    $oButton ->COLOR = 'green';
    $oButton ->BORDERCOLOR = 'blue';
    echo $oButton;
    $iSavedPosition2 = $oCaretaker ->addMemento($oButton ->saveToMemento());
    $oButton ->COLOR = 'red';
    $oButton ->BORDERCOLOR = 'gray';
    echo $oButton;
    $oButton ->restoreFromMemento($oCaretaker ->getMemento($iSavedPosition1));
    echo $oButton;
} catch(Exception $e) {
    echo $e;
}
?>

STATE

Le state (ou Etat) représente l'état d'un objet. Il permet d'agir en conséquence en fonction de l'état d'un objet, et donc de modifier son comportement. Les méthodes d'état auront des actions différentes en fonction de l'état courant de l'objet.
De plus, ce design pattern peut permettre d'appliquer à l'objet le fonctionnement d'un interrupteur : la méthode d'état X peut modifier l'objet vers l'état Y, et la méthode d'état Y vers l'état X.
L'état est composé de :

  • une classe abstraite d'état
  • X classes concrètes d'état
  • la classe dont l'état change

Deux petits exemples ici. Le premier montre le fonctionnement du design pattern de manière simple, et le second montrera la notion "d'interrupteur".

Premier exemple :

<?php
/**
* STATE abstrait définissant les méthodes d'état
*
*/
abstract class abstractVendeur {
    protected $sName;
    public function __construct($sName) {
        $this ->sName = $sName;
    }
    abstract public function welcome();
    abstract public function advice();
    abstract public function bye();
}
/**
* Un STATE concret
*
*/
class vendeurSympa extends abstractVendeur {
    public function welcome() {
        echo $this ->sName.' : Bonjour, que puis-je faire pour vous ?<br />';
    }
    public function advice() {
        echo $this ->sName.' : Ah, je trouve que le bleu vous va beaucoup mieux!<br />';
    }
    public function bye() {
        echo $this ->sName.' : Je vous en prie, bonne journée!<br />';
    }
}
/**
* Un autre STATE concret
*
*/
class vendeurPasSympa extends abstractVendeur {
    public function welcome() {
        echo $this ->sName.' : Ouais, vous voulez quoi?<br />';
    }
    public function advice() {
        echo $this ->sName.' : Chais pas, ce sont vos goûts.<br />';
    }
    public function bye() {
        echo $this ->sName.' : C'est ça.<br />';
    }
}
/**
* L'objet dont l'état change
*
*/
class vendeur {
    private $oEtat;
    public function __construct() {
        $this ->oEtat = new vendeurSympa(__CLASS__);
    }
    public function process() {
        $this ->oEtat ->welcome();
        $this ->oEtat ->advice();
        $this ->oEtat ->bye();
    }
    public function setSympa() {
        $this ->oEtat = new vendeurSympa(__CLASS__);
    }
    public function setPasSympa() {
        $this ->oEtat = new vendeurPasSympa(__CLASS__);
    }
}
/**
* On teste :-)
*/
$oVendeur = new vendeur;
$oVendeur ->process();
$oVendeur ->setPasSympa();
$oVendeur ->process();
?>

Second exemple montrant la notion de changement d'état dynamique :

<?php
/**
* STATE abstrait : j'ai utilisé une interface cette fois car je n'avais rien à implémenter ici
*
*/
interface interrupteur {
    public function switchSate();
}
/**
* STATE concret : interrupteur allumé, qui va aussi changer l'état de l'objet cible
*
*/
class interrupteurOn implements interrupteur {
    public function switchSate() {
        echo 'Je suis allumé<br />';
        return new interrupteurOff;
    }
}
/**
* STATE concret : interrupteur éteint, qui va aussi changer l'état de l'objet cible
*
*/
class interrupteurOff implements interrupteur {
    public function switchSate() {
        echo 'Je suis éteint<br />';
        return new interrupteurOn;
    }
}
/**
* Notre objet cible, le bouton
*
*/
class bouton {
    private $oEtat;
    public function __construct() {
        $this ->oEtat = new interrupteurOn;
    }
    public function switchMe() {
        $this ->oEtat = $this ->oEtat ->switchSate();
    }
}
/**
* Et on teste!
*/
$oSwitch = new bouton;
$oSwitch ->switchMe();
$oSwitch ->switchMe();
$oSwitch ->switchMe();
$oSwitch ->switchMe();
$oSwitch ->switchMe();
$oSwitch ->switchMe();
?>

VISITOR

Le VISITOR (visiteur) autorise une classe à appeler un visiteur avec sa propre instance d'elle-même. Le visiteur possède des méthodes propres à chaque classe qu'elle peut visiter. Cela permet de séparer un algorithme de la structure d'une classe qui en a besoin. Cela se passera par l'intermédiaire d'une méthode visitee::accept().
Il se compose de :

  • un ABSTRACT VISITEE, classe d'abstraction pour les classes pouvant être visitée. Elle déclare une méthode accept()
  • x CONCRETE VISITEE implémentant l'ABSTRACT VISITEE
  • un ABSTRACT VISITOR qui va déclarer les méthodes de visites dont on a besoin
  • x CONCRETE VISITOR dédiés à la visite des VISITEE

Dans le petit exemple, nous imaginons un catalogue composé de jeux vidéos et de films. Les attributs des films et des jeux sont différents, même si le tout se ressemble. UN jeu par exemple tourne sur une certaine plateforme...tandis qu'un film a un producteur.

Nous allons utiliser le VISITOR afin d'homogénéiser tout ça :

<?php
/**
* ABSTRACT VISITEE
*
*/
abstract class visitee {
    abstract function accept(visitor $visitorIn);
}
/**
* CONCRETE VISITEE pour les jeux
*
*/
class gameVisitee extends visitee {
    private $sType;
    private $sPlatform;
    private $sTitle;
    public function __construct($sTitle, $sType, $sPlatform) {
        $this->sType = $sType;
        $this->sTitle=$sTitle;
        $this->sPlatform=$sPlatform;
    }
    public function getType() {
        return $this->sType;
    }
    public function getPlatform() {
        return $this->sPlatform;
    }
    public function getTitle() {
        return $this->sTitle;
    }
    public function accept(visitor $visitor) {
        $visitor->visitGame($this);
    }
}
/**
* CONCRETE VISITEE pour les films
*
*/
class movieVisitee extends visitee {
    private $sCategory;
    private $sProducer;
    private $sTitle;
    public function __construct($sTitle, $sCategory, $sProducer) {
        $this->sCategory = $sCategory;
        $this->sTitle=$sTitle;
        $this->sProducer=$sProducer;
    }
    public function getCategory() {
        return $this->sCategory;
    }
    public function getProducer() {
        return $this->sProducer;
    }
    public function getTitle() {
        return $this->sTitle;
    }
    public function accept(visitor $visitor) {
        $visitor->visitMovie($this);
    }
}
/**
* ABSTRACT VISITOR
*
*/
abstract class visitor {
    abstract function visitGame(gameVisitee $gameVisitee);
    abstract function visitMovie(movieVisitee $movieVisitee);
}
/**
* CONCRETE VISITOR renvoyant la description du VISITEE qu'il visite
*
*/
class catalogVistor extends visitor {
    private $sDescription;
    private function setDescr($sOutput) {
        $this->sDescription = $sOutput;
    }
    public function getDescr() {
        return $this->sDescription;
    }
    function visitGame(gameVisitee $gameVisitee) {
        $sOutput = <<<eos
<p><strong>{$gameVisitee->getTitle()}</strong><br />
TYPE : <em>{$gameVisitee->getType()}</em><br />
PLATFORM: <em>{$gameVisitee->getPlatform()}</em></p>
eos;
        $this->setDescr($sOutput);
    }
    function visitMovie(movieVisitee $movieVisitee) {
        $sOutput = <<<eos
<p><strong>{$movieVisitee->getTitle()}</strong><br />
CATEGORY : <em>{$movieVisitee->getCategory()}</em><br />
PRODUCER: <em>{$movieVisitee->getProducer()}</em></p>
eos;
        $this->setDescr($sOutput);
    }
}
/**
* Et le test :-)
* A noter que je profite ici d'une possibilité de PHP5 : en PHP5, les objets sont toujours passés par référence!
* Ce qui fait qu'en donnant tour à tour le catalogVisitor au gameVisitee puis au movieVisitee, je change son état.
* Du coup, je peux ensuite appeler sa méthode getDescr() après chacune de ses visites.
*/
$visitor = new catalogVistor;
$game = new gameVisitee('My fancy RPG', 'RPG', 'PC');
$movie = new movieVisitee('My great movie', 'action', 'me myself and I');
$game->accept($visitor);
echo $visitor->getDescr();
$movie->accept($visitor);
echo $visitor->getDescr();
?>

CHAIN OF RESPONSIBILITY

Le CHAIN OF RESPONSIBILITY (chaîne de responsabilité) permet de se déplacer dans une chaîne d'objets jusqu'à en trouver un qui va accepter de prendre la responsabilité du traitement.
Ce design pattern se compose :

  • de la classe à traiter
  • d'un ABSTRACT HANDLER avec une méthode permettant de définir le prochain maillon de la chaîne
  • de x CONCRETE HANDLER qui sont autant de maillons de la chaîne de responsabilité

Dans notre exemple, nous imaginons des joueurs qui ont fait un certain score dans un jeu.
Nous allons utiliser la chaîne pour leur donner un niveau en fonction de leur score :-)

On pourrait évidemment imaginer une classe intermédiaire qui va non seulement collecter nos joueurs, mais aussi les maillons de la chaîne, puis lancer le traitement :

<?php
/**
* La classe principale sur laquelle nous allons appliquer notre chaine de responsabilités
*
*/
class gamer {
    private $sName;
    private $iPoints;
    private $iLevel;
    public function __construct($sName, $iPoints) {
        if(!is_int($iPoints)) {
            throw new Exception('Points must be an integer');
        }
        $this->sName=$sName;
        $this->iPoints = $iPoints;
    }
    public function setLevel($iLevel) {
        if (!is_int($iLevel)) {
            throw new Exception('Level must be an integer');
        }
        $this->iLevel = $iLevel;
    }
    public function __get($sProp) {
        switch($sProp) {
            case 'NAME':
                return $this->sname;
                break;

            case 'POINTS':
                return $this->iPoints;
                break;

            case 'LEVEL':
                if (is_null($this->iLevel)) {
                    throw new Exception('Level has not been set');
                }
                return $this->iLevel;
                break;

            default:
                throw new Exception('Property '.$sProp.' cannot be get');
        }
    }
}
/**
* ABSTRACT HANDLER
*
*/
abstract class level extends SplObjectStorage {
    protected $nextLevel;
    public function setNext(level $nextLevel) {
        $this->nextLevel = $nextLevel;
    }
}
/**
* CONCRETE HANDLER qui va être le premier maillon de notre chaîne
*
*/
class level1 extends level {
    public function process() {
        foreach($this as $oGamer) {
            if ($oGamer->POINTS <= 100) {
                $oGamer->setLevel(1);
            } elseif(!is_null($this->nextLevel)) {
                $this->nextLevel->attach($oGamer);
                $this->nextLevel->process();
            } else {
                throw new Exception('Next in the chain of command has not been set, cannot handle request');
            }
        }
    }
}
/**
* CONCRETE HANDLER qui va être le second maillon de notre chaîne
*
*/
class level2 extends level {
    public function process() {
        foreach($this as $oGamer) {
            if ($oGamer->POINTS <= 200) {
                $oGamer->setLevel(2);
            } else {
                $this->nextLevel->attach($oGamer);
                $this->nextLevel->process();
            }
        }
    }
}
/**
* CONCRETE HANDLER qui va être le troisième et dernier maillon de notre chaîne
*
*/
class level3 extends level {
    public function process() {
        foreach($this as $oGamer) {
            if ($oGamer->POINTS <= 300) {
                $oGamer->setLevel(3);
            } else {
                $this->nextLevel->attach($oGamer);
                $this->nextLevel->process();
            }
        }
    }
}
/**
* Notre petit test :-)
*/
$neo = new gamer('neo', 99);
$dante = new gamer('dante', 150);
$jeremy = new gamer('jeremy', 225);

$level1 = new level1;
$level2 = new level2;
$level3 = new level3;
$level1->setNext($level2);
$level2->setNext($level3);
$level1->attach($neo);
$level1->attach($dante);
$level1->attach($jeremy);
$level1->process();

echo 'Niveau atteint par Neo : '.$neo->LEVEL;
echo 'Niveau atteint par Dante : '.$dante->LEVEL;
echo 'Niveau atteint par Jeremy : '.$jeremy->LEVEL;
?>

COMMAND

Le COMMAND (commande) permet de séparer le code initiateur d'une action, de l'action elle-même.
La commande va se charger de communiquer l'action à effecuer, avec les paramètres requis par cette dernière.

Notre petit exemple va nous faire construire une balise "span" personnalisée :

<?php
/**
* RECEIVER, la classe sur laquelle on applique le COMMAND
*
*/
class span {
    private $sContents;
    private $aStyles = array();
    public function __construct($sContents) {
        $this->sContents = $sContents;
    }
    public function getSpan() {
        $sStyle = $this->getStyles();
        return '<span '.$sStyle.'>'.$this->sContents.'</span>';
    }
    private function getStyles() {
        return 'style="'.implode(';', $this->aStyles).';"';
    }
    public function setFontColor($sColor) {
        $this->aStyles[]='color:'.$sColor;
    }
    public function setBackgroundColor($sColor) {
        $this->aStyles[]='background-color:'.$sColor;
    }
    public function setBorders($iSize, $sStyle, $sColor) {
        $this->aStyles[]='border:'.$iSize.'px '.$sStyle.' '.$sColor;
    }
}
/**
* ABSTRACT COMMAND qui prend le RECEIVER en paramètre et déclare la méthode execute()
*
*/
abstract class command {
    protected $receiver;
    public function __construct(span $span) {
        $this->receiver = $span;
    }
    abstract public function execute();
}
/**
* Une CONCRETE COMMANDE
*
*/
class commandFontColorRed extends command {
    public function execute() {
        $this->receiver->setFontColor('red');
    }
}
/**
* Une autre CONCRETE COMMANDE
*
*/
class commandFontColorblue extends command {
    public function execute() {
        $this->receiver->setFontColor('blue');
    }
}
/**
* ...etc
*
*/
class commandBackgroundColorOrange extends command {
    public function execute() {
        $this->receiver->setBackgroundColor('orange');
    }
}
class commandBackgroundColorGreen extends command {
    public function execute() {
        $this->receiver->setBackgroundColor('green');
    }
}
class commandBorderSolid extends command {
    public function execute() {
        $this->receiver->setBorders(1, 'solid', 'black');
    }
}
class commandBorderDottedDouble extends command {
    public function execute() {
        $this->receiver->setBorders(2, 'dotted', 'black');
    }
}
/**
* Notre test :-)
*/
$span = new span('hello');
echo $span->getSpan();
$command = new commandFontColorblue($span);
$command->execute();
echo $span->getSpan();
$command = new commandBackgroundColorOrange($span);
$command->execute();
echo $span->getSpan();
$command = new commandBorderDottedDouble($span);
$command->execute();
echo $span->getSpan();
?>

MEDIATOR

Le MEDIATOR (médiateur) définit un objet qui intègre les interactions d'une collection d'objets (COLLEAGUES).
Le médiateur connait tous les objets de sa collection, et chaque objet COLLEAGUE connait uniquement son médiateur.
Dans notre exemple, nous allons simuler un combat entre le héros et le méchant.
A noter que l'on pourrait s'amuser avec l'API de Réflexion pour optimiser et améliorer tout ça : http://www.php.net/manual/fr/language.oop5.reflection.php

Le héros et le méchant ont chacun leur familier. Ce qui arrive à l'un arrive à l'autre. Tous peuvent attaquer, et soigner :

<?php
/**
* ABSTRACT MEDIATOR
*
*/
abstract class abstractMediator {
    abstract public function hit(character $giver, character $receiver, $iPoints);
    abstract public function heal(character $giver, character $receiver, $iPoints);
}
/**
* CONCRETE MEDIATOR
*
*/
class mediator extends abstractMediator {
    private $hero;
    private $minion;
    private $villain;
    private $pet;

    public function setHero(hero $hero) {
        $this->hero = $hero;
    }
    public function setPet(pet $pet) {
        $this->pet = $pet;
    }
    public function setMinion(minion $minion) {
        $this->minion = $minion;
    }
    public function setVillain(villain $villain) {
        $this->villain = $villain;
    }
    public function hit(character $giver, character $receiver, $iPoints) {
        $sReceiver = get_class($receiver);
        $sGiver = get_class($giver);
        $this->$sReceiver->setPoints(-$iPoints);
        $this->$sReceiver->notify('got hit by '.$sGiver.' : '.$iPoints);
        if ($receiver instanceof hero) {
            $this->pet->setPoints(-$iPoints);
            $this->pet->notify('lost : '.$iPoints);
        } elseif($receiver instanceof pet) {
            $this->hero->setPoints(-$iPoints);
            $this->hero->notify('lost : '.$iPoints);
        } elseif($receiver instanceof villain) {
            $this->minion->setPoints(-$iPoints);
            $this->minion->notify('lost : '.$iPoints);
        } else {
            $this->villain->setPoints($iPoints);
            $this->villain->notify('lost : '.$iPoints);
        }
    }
    public function heal(character $giver, character $receiver, $iPoints) {
        $sReceiver = get_class($receiver);
        $sGiver = get_class($giver);
        $this->$sReceiver->setPoints($iPoints);
        $this->$sGiver->notify('healed '.$sReceiver.' : '.$iPoints);
        if ($receiver instanceof hero) {
            $this->pet->setPoints($iPoints);
            $this->pet->notify('gained : '.$iPoints);
        } elseif($receiver instanceof pet) {
            $this->hero->setPoints($iPoints);
            $this->hero->notify('gained : '.$iPoints);
        } elseif($receiver instanceof villain) {
            $this->minion->setPoints($iPoints);
            $this->minion->notify('gained : '.$iPoints);
        } else {
            $this->villain->setPoints($iPoints);
            $this->villain->notify('gained : '.$iPoints);
        }
    }
}
/**
* ABSTRACT COLLEAGUE
*
*/
abstract class character {
    private $mediator;
    protected $iLife;
    public function __construct(mediator $mediator) {
        $this->mediator=$mediator;
    }
    public function hit(character $char, $iPoints) {
        $this->mediator->hit($this, $char, $iPoints);
    }
    public function heal(character $char, $iPoints) {
        $this->mediator->heal($this, $char, $iPoints);
    }
    public function setPoints($iPoints) {
        $this ->iLife += $iPoints;
    }
    public function notify($sMsg) {
        echo get_class($this).' '.$sMsg;
    }
    public function getPoints() {
        return $this->iLife;
    }
}
/**
* CONCRETE COLLEAGUE HERO
*
*/
class hero extends character {
    protected $iLife = 100;
}
/**
* CONCRETE COLLEAGUE PET
*
*/
class pet extends character {
    protected $iLife = 100;
}
/**
* CONCRETE COLLEAGUE MINION
*
*/
class minion extends character {
    protected $iLife = 50;
}
/**
* CONCRETE COLLEAGUE VILLAIN
*
*/
class villain extends character {
    protected $iLife = 200;
}
/**
* Commençons le combat!
*/
$mediator = new mediator;
$hero = new hero($mediator);
$pet = new pet($mediator);
$minion = new minion($mediator);
$villain = new villain($mediator);

$mediator->setHero($hero);
$mediator->setPet($pet);
$mediator->setMinion($minion);
$mediator->setVillain($villain);

$villain->hit($hero, 50);
$hero->heal($hero, 20);
$hero->hit($villain, 30);

echo $hero->getPoints();
echo $pet->getPoints();
echo $villain->getPoints();
echo $minion->getPoints();
?>

INTERPRETER

Un INTERPRETER définit la grammaire d'un langage spécialisé dont on a besoin. Il l'interprête afin de le traduire.
Un exemple sera plus parlant. Les plus vieux d'entre vous (dont je fais partie) se souviendront sans doute d'un langage nommé LOGO.
Pour les autres, ce langage permettait de dessiner sur l'écran, grâce à des commandes assez basiques. Cela déplaçait une tortue sur l'écran...très fun ;-)
On reprend exactement le même principe ici!
Notre langage de dessin accepte 4 instructions :

  • right
  • left
  • up
  • down

L'API de Réflexion est utilisée ici pour invoquer les méthodes statiques des classes EXPRESSION.
Plus de détails sur cet API ici : http://www.php.net/manual/fr/language.oop5.reflection.php
Chaque instruction doit être suivie d'un entier représentant la distance à parcourir sur le dessin dans la direction souhaitée. On pourrait très facilement aller plus loin avec cette classe, ce serait d'ailleurs un bon exercice. Testez-là, c'est très amusant, vous verrez :-) Vous pouvez ajouter la possibilité d'initialiser et réinitialiser le départ, paramètrer la taille de l'image, la couleur du trait, son épaisseur, ajouter des commandes etc... (à noter que cet exemple requiert GD2 pour fonctionner, hein) :

<?php
/**
* Notre INTERFACE EXPRESSION
*
*/
interface expression {
    public static function interpret($iDistance);
}
/**
* Une EXPRESSION CONCRETE pour les mouvements vers la droite
*
*/
class right implements expression {
    public static function interpret($iDistance) {
        return array(0, 0, $iDistance, 0);
    }
}
/**
* Une EXPRESSION CONCRETE pour les mouvements vers la gauche
*
*/
class left implements expression {
    public static function interpret($iDistance) {
        return array(0, 0, -$iDistance, 0);
    }
}
/**
* Une EXPRESSION CONCRETE pour les mouvements vers le haut
*
*/
class up implements expression {
    public static function interpret($iDistance) {
        return array(0, 0, 0, -$iDistance);
    }
}
/**
* Une EXPRESSION CONCRETE pour les mouvements vers le bas
*
*/
class down implements expression {
    public static function interpret($iDistance) {
        return array(0, 0, 0, $iDistance);
    }
}
/**
* Notre INTERPRETER
*
*/
class interpreter {
    private $iX = 200;
    private $iY = 200;
    private $aDefaultDim = array(500,500);
    private $oExpressionStack;
    private $oTranslationStack;
    private $imh;
    public function __construct() {
        $this->oTranslationStack = new ArrayIterator;
    }
    public function interpret ($sChaine) {
        if(!is_string($sChaine)) {
            throw new Exception(__CLASS__.'::'.__FUNCTION__.'() : Parameter must be a string');
        }
        $this->oExpressionStack = new ArrayIterator(explode(' ', $sChaine));
        $this->evaluate();
        $this->buildMove();
    }
    private function evaluate() {
        while($this->oExpressionStack->valid()) {
            $sToken = $this->oExpressionStack->current();
            if(!class_exists($sToken)) {
                throw new Exception(__CLASS__.'::'.__FUNCTION__.'() : invalid expression '.$sToken);
            }
            $this->oExpressionStack->next();
            $iDistance = $this->oExpressionStack->current();
            if(!is_numeric($iDistance)) {
                throw new Exception(__CLASS__.'::'.__FUNCTION__.'() : invalid distance '.$iDistance);
            }
            $interpreter = new ReflectionMethod($sToken, 'interpret');
            $this->oTranslationStack[] = $interpreter->invoke(null, (int)$iDistance);
            $this->oExpressionStack->next();
        }
    }
    private function buildMove() {
        $this->imh = imagecreatetruecolor($this->aDefaultDim[0], $this->aDefaultDim[1]);
        $bgh = imagecolorallocate($this->imh, 0, 0, 0);
        $colh = imagecolorallocate($this->imh, 255, 255, 255);
        foreach($this->oTranslationStack as $aMove) {
            imageline($this->imh, $this->iX+$aMove[0], $this->iY+$aMove[1], $this->iX+$aMove[2], $this->iY+$aMove[3], $colh);
            $this->iX += $aMove[2];
            $this->iY += $aMove[3];
        }
    }
    public function getMove() {
        if(true === headers_sent()) {
            throw new Exception(__CLASS__.'::'.__FUNCTION__.'() : Unable to display image, headers already sent');
        }
        header('Content-type: image/png');
        imagepng($this->imh);
    }
}
/**
* Notre test! Nous allong afficher un joli dessin à la façon du vieux langage dinosaure LOGO
*/

try {
    $oInterpreter = new interpreter;
    $oInterpreter->interpret('up 100 right 50 down 100 left 10 up 90 left 30 down 90 left 10 right 10 up 50 right 30 up 5 left 30');
    $oInterpreter->getMove();
} catch(Exception $e) {
    echo $e;
}
?>

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