Le design pattern Observer
Le design pattern Observer permet de créer un mécanisme de souscription pour notifier de nombreux objets à propos d’événements qui pourraient se produire au niveau de l’objet qu’ils observent.
Imaginez que vous soyez fan d’un groupe de musique et que ce groupe ait une fan page sur un réseau social. Il est évident que vous voudriez être notifié si le groupe se produisait à proximité de chez vous, n’est-ce pas ?
Vous pourriez aller tous les jours sur la fan page, ou alors ils pourraient envoyer des e-mails à tous les membres de la fan page, mais cela produirait beaucoup de spams pour rien...
Il serait judicieux d’implémenter un système où les fans du groupe s’inscrivent à un système de notification précis : "Notifiez-moi si le groupe Tartempion passe près de Marseille".
Et dans ce cas, on ne recevrait que les e-mails qui nous concernent.
C’est exactement le système que vous permet de créer le design pattern Observer !
Le
Subjet
observé produit des événements d’intérêt pour d’autres objets. Ces événements sont publiés quand l’objet change d’état et/ou exécute certains comportements. LeSubjet
contient des fonctions permettant aux objets de "souscrire" aux différents types d’événements.Quand un événement se produit, le
Subjet
parcourt sa liste de souscription et lui notifie l’événement.L’observateur (le souscripteur) implémente une interface qui lui permet de récupérer les informations liées aux événements publiés. Dans le diagramme UML présenté, il s’agit de la fonction
observe()
.Il arrive que la méthode
observe()
des classes concrètes reçoive directement une instance duSubjet
qui aura été mis à jour, plutôt qu’une liste d’arguments.
Le langage PHP possède déjà des objets de type Subject
et Observer
: les interfaces SplSubject
et SplObserver
! À l’aide de ces contrats, voici comment nous pourrions procéder pour modéliser et résoudre le problème présenté précédemment.
D’abord, il faudrait définir quel objet est à observer : ici, ce serait la programmation des concerts du groupe Tartempion.
<?php
final class ConcertsPlanner implements \SplSubject
{
private $observers;
private $state;
public function __construct()
{
$this->observers = new \SplObjectStorage();
}
public function attach(\SplObserver $observer): void
{
$this->observers->attach($observer);
}
public function detach(\SplObserver $observer): void
{
$this->observers->detach($observer);
}
public function getState()
{
return $this->state;
}
public function plan($groupName, $date, $location)
{
$this->state = [
'group' => $groupName,
'date' => date('d/m/Y', $date,
'location' => $location,
];
// ...
$this->notify();
}
public function notify(): void
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
}
À chaque nouvelle planification de concert, les comptes utilisateurs qui auront choisi d’être notifiés recevront le message. Voici à quoi pourrait ressembler cette classe Fan
:
<?php
final class Fan implements \SplObserver
{
public function getLocation()
{
return 'Marseille';
}
public function getFollowedGroups()
{
return [
'Tartanpion',
'...',
];
}
public function update(\SplSubject $subject): void
{
$state = $subject->getState();
if (in_array($state['group'], $this->getFollowedGroups())
&& $state['location'] === $this->getLocation()) {
// Notifier l'utilisateur !
}
}
}
Enfin, voici un code Client
d’utilisation de ce code :
<?php
$concertsPlanner = new ConcertsPlanners();
$fans = (new FansRepository())->retrieveFans();
foreach ($fans as $fan) {
$concertsPlanner->attach($fan);
}
// ...
$concertsPlanner->plan('Tartanpion', '12 juin 2020', 'Bordeaux');
// tous les fans du groupe Tartanpion habitant à Bordeaux seront
// notifiés
Le design pattern Strategy
Le design pattern Strategy vous permet de :
définir une famille d’algorithmes ;
les séparer en classes distinctes ;
faire en sorte qu’ils soient interchangeables dans votre application.
Reparlons une dernière fois d’e-commerce (promis :ange:). Vous avez développé un site de vente de peluches de collection et, pour faire simple, vous avez implémenté la solution de paiement PayPal.
Mais voilà, certains de vos clients vous demandent de pouvoir payer à l’aide d’autres méthodes : par chèque, par carte bancaire, par Stripe... votre code commence à contenir des if/else partout, et vous vous rendez compte que quelque chose ne va pas : comment faire pour maintenir un système qui propose de faire la même chose (payer) de multiples façons (algorithmes) ?
Pour cela, nous allons implémenter le design pattern Strategy !
Le
Context
garde une référence à une stratégie et délègue à cet objet l’action à réaliser.L’interface
StrategyInterface
est le contrat qui lie tous les algorithmes au travers d’une fonction (ici, la fonctionoperation(data)
).Les stratégies concrètes implémentent les différents moyens de paiement utilisables par le
Context
.Évidemment, nous dépendons d’une interface, donc le
Context
n’est pas couplé à une stratégie concrète particulière (rappelez-vous, le principe de Liskov).
Implémentons ce design pattern pour modéliser une solution à notre problème en PHP. :soleil:
D’abord, la classe Context
. Dans notre contexte, ce pourrait être une classe Client
qui a une fonction buy
(acheter, en anglais) :
<?php
use CartInterface;
final class Customer
{
private $paymentMethod;
public function setPaymentStrategy(PaymentMethodInterface $paymentMethod)
{
$this->paymentMethod = $paymentMethod;
}
/**
* Effectue le paiement de la command
*/
public function buy(CartInterface $cart)
{
return $this->paymentMethod->execute(Cart $cart);
}
}
Nous pourrions imaginer que cette fonction prenne en argument un objet de type CartInterface
(panier) qui contient toutes les informations nécessaires pour procéder au paiement.
Ensuite, il faut définir notre interface de Stratégie. Ici, elle s’appelle PaymentMethodInterface
:
<?php
use CartInterface;
interface PaymentMethodInterface
{
public function execute(CartInterface $cart);
}
Ensuite, les différentes méthodes de paiement. Après tout, c’est le problème que nous essayons de résoudre ! :D
<?php
use PaymentMethodInterface;
use CartInterface;
final class ByCheck implements PaymentMethodInterface
{
public function execute(CartInterface $cart)
{
//...
}
}
Et le paiement par PayPal :
<?php
use PaymentMethodInterface;
use CartInterface;
final class ByPaypal implements PaymentMethodInterface
{
public function execute(CartInterface $cart)
{
// ...
}
}
Et enfin, l’utilisation dans votre application qui est cliente du système dans le diagramme UML !
<?php
$customer = new Customer();
// dans le formulaire, le visiteur choisit l'option Paypal
$customer->setPaymentStrategy(new ByPaypal());
// on procède à l'acte d'achat !
$customer->buy($customer->getCart());
Le design pattern State
Prenons un exemple. Imaginez que vous conceviez un jeu vidéo de combat au tour par tour. Vous avez à votre disposition des créatures qui ont leurs propres compétences, et qui peuvent combattre à votre place.
Seulement voilà, certaines de ces compétences impliquent un changement de leur état :
si elles sont "endormies", elles ne peuvent pas attaquer en se déplaçant ;
si elles sont "aveuglées", leurs attaques ont de grandes chances d’échouer ;
si elles sont "rendues folles", elles réagiront n’importe comment à vos ordres ;
etc.
Si vous deviez développer cela au plus vite, vous finiriez probablement avec ce type d’implémentations :
<?php
class SomeMonster implements MonsterInterface
{
public function attack()
{
if (!$this->isSleepy()) {
if ($this->isBlind()) {
// ...
}
if ($this->isCrazy()) {
// ...
}
if ($this->isPoisoned()) {
// ...
}
}
// ...
}
public function move()
{
if (!$this->isSleepy())
{
// ...
}
// ...
}
}
Vous êtes un peu plus aguerri et à ce stade, vous sentez déjà que ce n’est pas du code solide :
chaque classe grossit à mesure que l’on implémente de nouveaux états ;
chaque "monstre" devient de plus en plus difficile à tester.
Et encore, imaginez qu’un monstre puisse subir différents états négatifs ! La complexité de ce code augmentera encore, devenant ainsi vite inmaintenable.
Le rôle du design pattern State va être de vous aider à extraire la gestion des différents états de vos objets dans des classes dédiées :
Tout d’abord, vos objets d’origine vont être responsables du changement de leur state (état) : c’est pourquoi ils auront une propriété
state
et un mutateur (ou un setter).Ensuite, nous allons créer une interface
StateInterface
qui aura en signature les fonctions dont le comportement change en fonction du state.Enfin, les implémentations concrètes vont définir le comportement de nos objets (nos petits monstres !) dans l’application en fonction de l’état.
Implémentons notre problème précédent en définissant le comportement d'un monstre appelé Mondozor, en fonction de 3 états : endormi, fou et aveugle !
Tout d’abord, les interfaces pour Monster et État :
<?php
use MonsterState;
interface Monster
{
public function attack() : void;
public function move() : void;
public function changeState(MonsterState $state) : Monster;
}
<?php
use Monster;
interface MonsterState
{
public function attack() : void;
public function move() : void;
public function setMonster(Monster $monster) : MonsterState;
}
Définissons ensemble Mondozor !
<?php
use Monster;
use MonsterState;
final class Mondozor implements Monster
{
/** @var MonsterState */
private $state;
public function __construct(MonsterState $state)
{
$this->changeState($state);
}
public function attack() : void
{
return $this->state->attack();
}
public function move() : void
{
return $this->state->move();
}
public function changeState(MonsterState $state) : Monster
{
$this->state = $state;
$this->state->setMonster($this);
return $this;
}
}
Et maintenant, les implémentations des différents états de Mondozor :
<?php
use Monster;
use MonsterState;
final class Sleepy implements MonsterState
{
/** @var Monster */
private $monster;
public function attack() : void
{
$this->monster->doNothing();
echo '😴';
}
public function move() : void
{
$this->monster->doNothing();
echo '😴';
}
public function setMonster(Monster $monster) : MonsterState
{
$this->monster = $monster;
return $this;
}
}
Et si Mondozor est rendu fou :
<?php
use Monster;
use MonsterState;
final class Crazy implements MonsterState
{
/** @var Monster */
private $monster;
public function attack() : void
{
// un pourcentage de chance de se blesser
if ($this->isTooMuchCrazy())
{
$this->monster->hurtItself();
echo '🤪';
return;
}
$this->monster->attackOtherMonster();
echo '😈';
}
public function move() : void
{
// un pourcentage de chance d'aller dans une autre direction
if ($this->isTooMuchCrazy())
{
$this->monster->moveRandomly();
echo '🤪';
return;
}
$this->monster->moveToTheDefinedDirection();
echo '➡';
}
public function setMonster(Monster $monster) : MonsterState
{
$this->monster = $monster;
return $this;
}
// ...
}
Exercez-vous !
Et si je vous faisais travailler sur un mini-projet de jeu de combat entre monstres ?
Vous aurez des monstres avec une barre de santé et des points de dégâts. Seulement, si dès le départ les statistiques sont fixes, le plus fort des monstres gagnera toujours : c'est pourquoi vous allez affecter les monstres de différents états de façon aléatoire.
Vous ne repartirez pas de l'exemple décrit précédemment, mais de cette archive qui implémente le système de jeu, ce qui va vous permettre de vous concentrer sur l'implémentation du Design Pattern State :soleil:. Regardons-la ensemble dans le screencast ci-dessous :
Vous allez devoir compléter les états Sleepy, Crazy et Healthy pour les monstres, selon les instructions laissées en commentaire dans le code.
En résumé
Il est rare de devoir implémenter son propre Observer ou Event Dispatcher. Par contre, la programmation événementielle est une très bonne stratégie pour développer des applications dynamiques, faciles à faire évoluer et à tester. Des applications solides. ;)
Le design pattern Strategy servira dans un contexte d’application où vous devrez connecter différents types de services qui font la même chose, mais de différentes façons. Il ressemble étrangement au design pattern Adapter, non ? Relisez donc attentivement les passages relatifs à ces deux design patterns pour en comprendre les nuances. :ange:
Le design pattern State est beaucoup plus commun dans nos applications, car la notion d’état se retrouve dans une majorité de sites : publication, e-commerce, jeux, gestion logistique ou RH. Attention à ne pas complexifier trop vite vos applications ! Si le nombre d’états est fini et petit (disons 2, voire 3 états), on peut probablement se satisfaire de quelques "if/else" dans notre code.
Ce cours arrive vers sa fin – bravo à vous ! Repassons sur tout ce que nous avons vu ensemble, avant un dernier quiz pour valider vos acquis !