Le design pattern Observer (Observateur)

Ce design pattern est à mon sens l’un des plus utilisés dans tout projet web, avec Strategy (qui fera l’objet d’un autre article).

Que fait-il?

img011

Si l’on observe ce diagramme UML, on se rend compte que l’on a:

  • une classe abstraite Observable (le sujet, qui va mettre à jour ses données)
  • Une interface Observer (qui observe le sujet et agit en conséquence)
  • Une classe ConcreteObserver qui implémente  cette interface
  • Une classe ConcreteObservable qui hérite de cette classe abstraite

 

Quelle est le mécanisme mis en oeuvre?

  1. La classe concrète Observable met à jour ses données
  2. Elle notifie les classes concrètes Observer par la fonction notifyObservers
  3. Les classes concrètes Observer vont alors se mettre à jour avec la fonction update, en recevant les nouvelles données de la classe concrète Observable

C’est aussi simple que ça. La seule chose à faire pour que les classes concrètes observers réagissent à chaque mise à jour de la classe concrète observable, c’est qu’il faut rattacher les observers à l’observable par la fonction addObservers.

Mise en pratique 1: implémentons notre propre design pattern Observer

1) Créons l’interface Observer (ceux qui observent)

namespace DP\ObserverBundle\Interfaces;

use DP\ObserverBundle\AbstractClass\Observable;

interface Observer
{
    public function update(Observable $observable);
}

2) Créons la classe abstraite Observable (le sujet)

namespace DP\ObserverBundle\AbstractClass;

use DP\ObserverBundle\Interfaces\Observer;
use Doctrine\Common\Collections\ArrayCollection;

abstract class Observable
{
    /**
     *
     * @var arrayCollection
     */
    private $observers;
    private $changed;

    public function __construct()
    {
        $this->observers = new ArrayCollection();
        $this->changed     = false;
    }

    public function addObservers(Observer $observer)
    {
        $this->observers->add($observer);
    }

    public function notifyObservers()
    {
        if ($this->hasChanged()) {
            foreach ($this->observers as $observer) {
                $observer->update($this);
            }
        }
        $this->clearChanged();
    }

    public function detach(Observer $observer)
    {
        $this->observers->remove($observer);
    }

    public function hasChanged()
    {
        return $this->changed;
    }

    public function setChanged()
    {
        $this->changed = true;
    }

    public function clearChanged()
    {
        $this->changed = false;
    }
}

Les observers sont une collection, beaucoup plus pratique à gérer qu’un tableau pour rajouter et retirer des éléments.
Comme vous pouvez le voir, la fonction addObservers ne fait que prendre un observer en paramètre et le rajouter à la collection (idem mais en sens inverse pour le detach).
La fonction notifyObservers parcourt tous les diagrammes

L’attribut changed a été rajouté. Ce n’est pas sur le diagramme UML, mais vous allez vite en comprendre l’utilité.

Ceci a été fait afin au cas où l’observable met à jour ses données trop régulièrement, toutes les 5 secondes par exemple. Si l’on met en place une condition (par ex, notifie les observers seulement si ta valeur a changé de 0,5 ou de 1, pas de quelques dixièmes), alors on changera le flag changed uniquement si cette condition est vérifiée. Vous n’aurez pas souvent besoin de cela, mais cette fonction existe si la nécessité est présente.

3) La classe concrète Observable: Données Meteo

Ici, nous allons dire que le sujet observé est une station météo qui transmet ses nouvelles valeurs à des programmes qui vont mettre à jour l’affichage.

namespace DP\ObserverBundle\Entity\Observable;

use DP\ObserverBundle\AbstractClass\Observable;

/**
 * DonneesMeteo => celui qui est observé
 */
class DonneesMeteo extends Observable
{
    /**
     * @var float
     *
     * @ORM\Column(name="temperature", type="float")
     */
    private $temperature;

    /**
     * @var float
     *
     * @ORM\Column(name="humidite", type="float")
     */
    private $humidity;

    /**
     * @var float
     *
     * @ORM\Column(name="pression", type="float")
     */
    private $pressure;

    function getTemperature()
    {
        return $this->temperature;
    }

    function getHumidity()
    {
        return $this->humidity;
    }

    function getPressure()
    {
        return $this->pressure;
    }

    public function setMesures($temperature, $humidity, $pressure)
    {
        $this->temperature = $temperature;
        $this->humidity    = $humidity;
        $this->pressure    = $pressure;
        $this->setChanged();
        $this->notifyObservers();
    }
}

A chaque fois que celui qui est observé va mettre à jour ses données, la fonction notifyObservers va être appelée et les observers vont se mettre à jour.

4) Les observers: AffichageConditions, AffichagePrevisions, AffichageStats

namespace DP\ObserverBundle\Entity\Observer;

use DP\ObserverBundle\Interfaces\Observer;
use DP\ObserverBundle\AbstractClass\Observable;

/**
 * AffichageConditions
 *
 */
class AffichageConditions implements Observer
{
    private $temperature;
    private $humidity;
    private $pressure;

    function getTemperature()
    {
        return $this->temperature;
    }

    function getHumidite()
    {
        return $this->humidity;
    }

    function getPression()
    {
        return $this->pressure;
    }
    
    function setCurrentPressure($currentPressure)
    {
        $this->currentPressure = $currentPressure;
    }

    public function __construct(Observable $observable)
    {
        $observable->addObservers($this);
    }

    public function update(Observable $observable)
    {
        if ($observable instanceof \DP\ObserverBundle\Entity\Observable\DonneesMeteo) {
            $this->temperature = $observable->getTemperature();
            $this->humidity    = $observable->getHumidity();
            $this->pressure    = $observable->getPressure();
        }
    }

    public function getNewValues()
    {
        return array('temperature' => $this->temperature, 'humidite' => $this->humidity, 'pression' => $this->pressure);
    }
}
namespace DP\ObserverBundle\Entity\Observer;

use DP\ObserverBundle\Interfaces\Observer;
use DP\ObserverBundle\AbstractClass\Observable;
use DP\ObserverBundle\Entity\Observable\DonneesMeteo;

/**
 * AffichageConditions
 *
 */
class AffichagePrevisions implements Observer
{
    private $currentPressure;
    private $lastPressure;
    private $prevision;
    
    function getCurrentPressure()
    {
        return $this->currentPressure;
    }

    function getLastPressure()
    {
        return $this->lastPressure;
    }
    
    function getPrevision()
    {
        return $this->prevision;
    }

    public function __construct(Observable $observable, $currentPressure)
    {
        $this->currentPressure = $currentPressure;
        $observable->addObservers($this);
    }

    public function update(Observable $observable)
    {
        if ($observable instanceof DonneesMeteo) {
            $this->lastPressure    = $this->currentPressure;
            $this->currentPressure = $observable->getPressure();
        }
    }

    public function getNewValues()
    {
        if ($this->currentPressure > $this->lastPressure) {
            $this->prevision = "Improving weather on the way!";
        } else if ($this->currentPressure == $this->lastPressure) {
            $this->prevision = "More of the same";
        } else if ($this->currentPressure < $this->lastPressure) {
            $this->prevision = "Watch out for cooler, rainy weather";
        }

        return array('prevision' => $this->prevision);
    }
}
namespace DP\ObserverBundle\Entity\Observer;

use DP\ObserverBundle\Interfaces\Observer;
use DP\ObserverBundle\AbstractClass\Observable;

/**
 * AffichageConditions
 *
 */
class AffichageStats implements Observer
{
    private $maxTemp;
    private $minTemp;
    private $sumTemp = 0.0;
    private $numReadings;

    public function __construct(Observable $observable, $minTemp, $maxTemp)
    {
        $this->minTemp = $minTemp;
        $this->maxTemp = $maxTemp;
        $observable->addObservers($this);
    }

    function getMaxTemp()
    {
        return $this->maxTemp;
    }

    function getMinTemp()
    {
        return $this->minTemp;
    }

    function getSumTemp()
    {
        return $this->sumTemp;
    }
    
    public function update(Observable $observable)
    {
        if ($observable instanceof \DP\ObserverBundle\Entity\Observable\DonneesMeteo) {
            $temp = $observable->getTemperature();
            $this->sumTemp += $temp;
            $this->numReadings++;

            if ($temp > $this->maxTemp) {
                $this->maxTemp = $temp;
            }

            if ($temp < $this->minTemp) {
                $this->minTemp = $temp;
            }

            $this->getNewValues();
        }
    }

    public function getNewValues()
    {
        $temperature = ($this->sumTemp / $this->numReadings) . "/" . $this->maxTemp . "/" . $this->minTemp;
        
        return array("AvgMaxMinTemperature" => $temperature);
    }
}

 

5) Le contrôleur qui réunit tout le monde

<?php

namespace DP\ObserverBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use DP\ObserverBundle\Entity\Observable\DonneesMeteo;
use DP\ObserverBundle\Entity\Observer\AffichageConditions;
use DP\ObserverBundle\Entity\Observer\AffichageStats;
use DP\ObserverBundle\Entity\Observer\AffichagePrevisions;

class DefaultController extends Controller
{

    /**
     * @Route("/", name="observer")
     * @Template()
     */
    public function indexAction()
    {
        $donneesMeteo        = new DonneesMeteo(); // sujet, celui qui est observé
        $affichageConditions = new AffichageConditions($donneesMeteo); // l'observateur, à qui on passe l'observé en argument 
        $affichageStats      = new AffichageStats($donneesMeteo, 10, 30); // l'observateur, à qui on passe l'observé en argument
        $affichagePrevisions = new AffichagePrevisions($donneesMeteo, 1000); // l'observateur, à qui on passe l'observé en argument

        $donneesMeteo->setMesures(25, 10, 1200); // le sujet met à jour ses données

        return array(
            'affichageConditions' => $affichageConditions->getNewValues(),
            'affichageStats'      => $affichageStats->getNewValues(),
            'affichagePrevisions' => $affichagePrevisions->getNewValues()
        );
    }
}

 

Ce qui donnera, dans le template:

Current conditions : 25 degree C, 10 % humidity and 1200 hPa.

Sum/Max/Min Temperature = 25/30/10

Forecast: Improving weather on the way!

Le code est disponible sur le dépôt github: https://github.com/jpsymfony/dp-observer.git.

Le répertoire correspondant est ObserverBundle.

Rédigé par

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.