Archives de catégorie : Design Patterns

PHP – Le design pattern Flyweight

Soyons honnêtes, si vous êtes un développeur web, les chances pour que vous ayez à utiliser le design pattern Flyweight en PHP sont assez minces, en raison du fait que Flyweight est surtout utile quand vous avez un très grand nombre d’objets en RAM et qu’il vous est important d’en économiser l’instanciation, ce qui arrive rarement lorsque vous développez (bien !) une application web. Vous le trouverez davantage dans des domaines comme les traitements de texte ou les jeux vidéo qui utilisent des environnements multi-tâches (multithreaded). En PHP, le multithreading est à ce jour utilisable seulement en ligne de commande, pas dans un environnement de serveur Web.

Cela ne signifie pas pour autant que ce design pattern n’est JAMAIS utilisé, il est par exemple implémenté dans l’ORM Doctrine, pour la gestion des types.

Le principe de fonctionnement du design pattern

Flyweight signifie « poids mouche » en bon français, mais pourquoi donc ? Parce que l’idée est de garder les objets dont nous allons nous servir les plus petits possible, comme la catégorie de boxeurs à laquelle ce nom fait référence…petits certes, mais efficaces !

Ces objets vont avoir deux états:

  • un état intrinsèque, qui ne dépend pas du contexte de l’objet
  • un état extrinsèque

Le premier réside dans la classe même tandis que le second lui sera fourni lors de l’appel à une de ses méthodes.

Prenons comme objet poids mouche un produit culturel donc l’état intrinsèque, immuable, sera uniquement composé de son nom. La classe ProduitCulturel se conformera à une interface assez simple, dont l’unique méthode afficher prend en paramètre l’état extrinsèque, c’est à dire tout ce qui est susceptible de venir enrichir l’état intrinsèque de l’objet. Le contexte est passé sous forme de tableau pour garder l’exemple le plus simple possible mais un objet dédié à ces contextes serait largement préférable.

interface ProduitCulturelInterface
{
    public function affiche(array $contexte): void;
}

class ProduitCulturel implements ProduitCulturelInterface
{
    protected string $nom;

    public function __construct(string $nom)
    {
        $this->nom = $nom;
    }

    public function affiche(array $contexte): void
    {
        echo 'Nom: ' . $this->nom . PHP_EOL;

        foreach ($contexte as $cle => $valeur) {
            echo ucfirst($cle) . ': ' . $valeur . PHP_EOL;
        }
    }
}

Pour gérer nos poids mouches, nous allons devoir utiliser une fabrique, car c’est à elle que s’adressera le code client pour obtenir ces objets. Il ne doit en aucun cas réaliser lui-même les instanciations ! Voilà ladite fabrique, assez rudimentaire: elle possède une méthode getProduitCulturel qui vérifie si une instance est déjà disponible avec le nom donné. Si c’est le cas, elle la renvoie, évitant une nouvelle instanciation, sinon elle instancie la classe ciblée et la range sagement dans son tableau interne, qui sert de « parc à poids mouches » comme le nomme si joliment le livre du GoF.

class FabriqueDeProduitsCulturels
{
    protected array $produits = [];

    public function getProduitCulturel(string $nom): ProduitCulturel
    {
        if (array_key_exists($nom, $this->produits)) {
            $produit = $this->produits[$nom];
        } else {
            $produit = new ProduitCulturel($nom);
            $this->produits[$nom] = $produit;
        }

        return $produit;
    }
}

Libre à vous d’en faire une version statique, pour ma part je ne suis pas tellement en faveur des appels statiques car ils complexifient les tests unitaires.

Notre code client va utiliser une classe Commande (aucun rapport avec le design pattern du même nom !) qui proposera dans son interface publique une méthode permettant l’ajout de produits culturels. Ces produits liés à la commande seront stockés dans un tableau du même nom tandis que leur contexte ira dans un tableau dédié, au même indice. Ainsi nous avons d’un côté les poids mouches et de l’autre le contexte auquel ils sont liés. Le constructeur de notre classe Commande se verra injecter son unique dépendance, la fabrique !

Afficher les produits d’une commande consistera tout simplement à boucler sur les produits et à invoquer leur méthode d’affichage, qui requiert le contexte en paramètre.

class Commande
{
    protected FabriqueDeProduitsCulturels $fabrique;

    protected array $produits = [];

    protected array $contextes = [];

    public function __construct(FabriqueDeProduitsCulturels $fabrique)
    {
        $this->fabrique = $fabrique;
    }

    public function ajouteProduit(string $nom, array $contexte)
    {
        $produit = $this->fabrique->getProduitCulturel($nom);
        $this->produits[] = $produit;
        $this->contextes[] = $contexte;
    }

    public function afficheProduits()
    {
        foreach ($this->produits as $index => $produit) {
            $produit->affiche($this->contextes[$index]);
        }
    }
}

Voici comment tout cela sera utilisé:

$commande = new Commande(new FabriqueDeProduitsCulturels());

$commande->ajouteProduit('Livre', ['prix' => 10.99, 'titre' => '1984', 'auteur' => 'George Orwell']);
$commande->ajouteProduit('Disque', ['prix' => 19.99, 'titre' => '1984', 'auteur' => 'Van Halen']);

$commande->afficheProduits();

Pour résumer…

Flyweight n’est utile qui si vous avez un nombre trèèèèès important d’objets de type semblable (si c’est le cas, demandez-vous déjà si c’est bien normal !) car son but est d’économiser la mémoire durant le temps d’exécution d’un processus (généralement multitâches)

Il met en jeu une fabrique qui sera chargée d’instancier le cas échéant les objets poids mouches et des classes concrètes contenant des états intrinsèques. Une fois l’objet obtenu par l’utilisation de la fabrique, il ne pourra plus être modifié (il est immuable) mais le client pourra lui transmettre son état extrinsèque par l’appel d’une de ses méthodes. Ici nous avons utilisé une méthode terriblement triviale qui réalise un simple affichage mais nous aurions pu tout aussi bien invoquer un service web pour commander les produits sur une API externe.

Ce billet a été réalisé en s’inspirant librement du livre du GoF et d’un ouvrage paru chez ENI Éditions.

Ruby: design pattern Adapter

Adapt or die!

Le design pattern Adapter (ou Adaptateur en bon français) a déjà été évoqué ici-même il y a quelques années, aussi vous laisserai-je le plaisir d’aller y voir les détails d’implémentation si toutefois le code proposé ici ne vous parlait pas immédiatement !

Rafraîchissons-nous la mémoire

L’exemple est rigoureusement le même, si l’on excepte les particularités dues au langage Ruby (pas de classe abstraite, pas d’interface), mais je vais en rappeler les contours: nous avons un inspecteur du permis de conduire qui fait faire les mêmes manœuvres à tous ses candidats. L’ennui est que ces manœuvres ne s’effectuent pas de la même façon selon que l’on conduise un bateau à voile, à moteur, une voiture, une moto etc. Ainsi, notre inspecteur, qui manifestement n’a pas très envie de changer ses pratiques, va travailler avec des adaptateurs et non plus directement avec les classes feuilles de l’arbre matérialisant la chaîne d’héritage. Il n’y verra que du feu car ce sont les adaptateurs qui feront le travail à sa place !

Allez, en voiture !

class InspecteurPermisConduire

  def initialize(conducteur)
    @candidat = conducteur
  end

  def changerCandidat(conducteur)
    @candidat = conducteur
  end

  def fairePasserExamen
    @candidat.demarrer
    @candidat.accelerer
    @candidat.tournerDroite
    @candidat.accelerer
    @candidat.tournerGauche
    @candidat.ralentir
    @candidat.reculer
    @candidat.immobiliser
  end

  private

  attr_reader :candidat
end

class Conducteur
  def initialize(conducteur)
    raise "Impossible d'instancier la classe Conducteur"
  end

  def demarrer
    raise NotImplementedError
  end

  def tournerGauche
    raise NotImplementedError
  end

  def tournerDroite
    raise NotImplementedError
  end

  def accelerer
    raise NotImplementedError
  end

  def ralentir
    raise NotImplementedError
  end

  def reculer
    raise NotImplementedError
  end

  def immobiliser
    raise NotImplementedError
  end
end

class Automobiliste

  def demarrer
    puts "tourner la clé de contact ou mettre la carte"
  end

  def tournerGauche
    puts "tourner le volant vers la gauche"
  end

  def tournerDroite
    puts "tourner le volant vers la droite"
  end

  def accelerer
    puts "appuyer sur la pédale d'accélération"
  end

  def ralentir
    puts "relâcher la pédale d'accélération et/ou " +
             "appuyer sur la pédale de frein"
  end

  def reculer
    puts "passer la marche arrière et accélérer"
  end

  def immobiliser
    puts "mettre le frein à main"
  end
end

class Navigateur
  def initialize
    raise "Impossible d'instancier la classe abstraite Navigateur"
  end

  def demarrer
    raise NotImplementedError
  end

  def reculer
    raise NotImplementedError
  end

  def tournerBabord
    raise NotImplementedError
  end

  def tournerTribord
    raise NotImplementedError
  end

  def accelerer
    raise NotImplementedError
  end

  def ralentir
    raise NotImplementedError
  end

  def jeterAncre
    raise NotImplementedError
  end
end

class Marin

  def initialize
    raise "Impossible d'instancier la classe abstraite Marin"
  end

  # méthode commune à tous les marins
  def jeterAncre
    puts "jeter l'ancre à la mer"
  end
end

class MarinVoile < Marin

  def initialize
    # vide mais on peut imaginer de la logique ici
  end

  def demarrer
    puts "Cette fonctionnalité n'est pas disponible"
  end

  def tournerBabord
    puts "diriger les voiles et la barre pour aller à babord"
  end

  def tournerTribord
    puts "diriger les voiles et la barre pour aller à tribord"
  end

  def accelerer
    puts "positionner les voiles et déterminer l'allure"
  end

  def ralentir
    puts "positionner les voiles et déterminer l'allure"
  end

  def reculer
    puts "positionner les voiles et manœuvrer pour reculer"
  end
end

class MarinMoteur < Marin

  def initialize
    # vide mais on peut imaginer de la logique ici
  end

  def demarrer
    puts "démarrer le moteur"
  end

  def tournerBabord
    puts "manoeuvrer la barre ou le volant pour aller à babord"
  end

  def tournerTribord
    puts "manoeuvrer la barre ou le volant pour aller à tribord"
  end

  def accelerer
    puts "augmenter la vitesse du moteur"
  end

  def ralentir
    puts "dimininuer la vitesse du moteur ou le couper"
  end

  def reculer
    puts "passer la marche arrière"
  end
end

class AdaptateurMarin

  def initialize(marin)
    @marin = marin
  end

  def demarrer
    @marin.demarrer
  end

  def tournerGauche
    @marin.tournerBabord
  end

  def tournerDroite
    @marin.tournerTribord
  end

  def accelerer
    @marin.accelerer
  end

  def ralentir
    @marin.ralentir
  end

  def reculer
    @marin.reculer
  end

  def immobiliser
    @marin.jeterAncre
  end

  private

  attr_reader :marin
end

puts "AUTOMOBILISTE"
inspecteur = InspecteurPermisConduire.new(Automobiliste.new)
inspecteur.fairePasserExamen
puts "MARIN MOTEUR"
adaptateur = AdaptateurMarin.new(MarinMoteur.new)
inspecteur = InspecteurPermisConduire.new(adaptateur)
inspecteur.fairePasserExamen
puts "MARIN VOILE"
adaptateur = AdaptateurMarin.new(MarinVoile.new)
inspecteur.changerCandidat(adaptateur)
inspecteur.fairePasserExamen

Ruby: design pattern Builder

Veuillez monter !

Le design pattern Builder, appelé « Monteur » en français est un design pattern de création dont le but est d’aider à créer des objets complexes. Pour cela, il intègre une classe appelée directeur qui contrôle l’algorithme de construction.

Composants du design pattern Builder

Les composants participant à ce pattern sont les suivants:

  • Le directeur, qui prend en composition un monteur
  • Une abstraction pour les monteurs
  • Des monteurs concrets
  • Des parties
  • Des produits (la cible finale !)

Dans l’exemple qui suit, nous aurons:

  • Des monteurs de camions et de voitures
  • Des produits : Camion et Voiture
  • Des parties : porte, moteur, roue
  • Un directeur qui orchestre la construction de tout ça

Nos abstractions seront des classes abstraites simulées (pas d’interface en Ruby, ni de classes abstraites stricto sensu).

class Piece
  def donnerNom
    raise "Impossible d'instancier la classe ProduitEntretien"
  end
end

class PorteCamion < Piece
  def donnerNom
    "Porte de camion"
  end
end

class PorteVoiture < Piece
  def donnerNom
    "Porte de voiture"
  end
end

class RoueCamion < Piece
  def donnerNom
    "Roue de camion"
  end
end

class RoueVoiture < Piece
  def donnerNom
    "Roue de voiture"
  end
end

class MoteurVoiture < Piece
  def donnerNom
    "Moteur de voiture"
  end
end

class MoteurCamion < Piece
  def donnerNom
    "Moteur de camion"
  end
end

class Vehicule

  def initialize
    @configuration = {}
  end

  def ajouter_piece(nom, piece)
    @configuration[nom] = piece
  end

  protected

  attr_reader :configuration
end

class Camion < Vehicule
end

class Voiture < Vehicule
end

class Directeur

  def initialize (monteur)
    @monteur = monteur
  end

  def monter
    @monteur.creer_vehicule
    @monteur.poser_portes
    @monteur.poser_moteur
    @monteur.poser_roues
    @monteur.peindre
  end

  private

  attr_reader :monteur
end

class Monteur

  def initialize
    raise "Impossible d'instancier la classe abstraite Monteur"
  end

  def creer_vehicule
    raise NotImplementedError
  end

  def poser_portes
    raise NotImplementedError
  end

  def poser_moteur
    raise NotImplementedError
  end

  def poser_roues
    raise NotImplementedError
  end

  def peindre
    raise NotImplementedError
  end

  def donner_vehicule
    raise NotImplementedError
  end
end

class MonteurVoitures < Monteur

  def initialize(options)
    @options = options
  end

  def creer_vehicule
    @voiture = Voiture.new
  end

  def poser_portes
    @voiture.ajouter_piece('porte AVG', PorteVoiture.new)
    @voiture.ajouter_piece('porte AVD', PorteVoiture.new)
    @voiture.ajouter_piece('porte arrière', PorteVoiture.new)

    if 3 < @options['nb_portes']
      @voiture.ajouter_piece('porte ARG', PorteVoiture.new)
      @voiture.ajouter_piece('porte ARD', PorteVoiture.new)
    end
  end

  def poser_moteur
    @voiture.ajouter_piece('moteur', MoteurVoiture.new)
  end

  def poser_roues
    @voiture.ajouter_piece('roue AVG', RoueVoiture.new)
    @voiture.ajouter_piece('roue AVD', RoueVoiture.new)
    @voiture.ajouter_piece('roue ARG', RoueVoiture.new)
    @voiture.ajouter_piece('roue ARD', RoueVoiture.new)
  end

  def peindre
    puts "Je peins la voiture en " + @options['couleur_peinture']
  end

  def donner_vehicule
    @voiture
  end

  private

  attr_reader :voiture, :options
end


class MonteurCamions < Monteur

  def initialize(options)
    @options = options
  end

  def creer_vehicule
    @camion = Camion.new
  end

  def poser_portes
    @camion.ajouter_piece('porte AVG', PorteCamion.new)
    @camion.ajouter_piece('porte AVD', PorteCamion.new)
  end

  def poser_moteur
    @camion.ajouter_piece('moteur', MoteurCamion.new)
  end

  def poser_roues
    @camion.ajouter_piece('roue AVG', RoueCamion.new)
    @camion.ajouter_piece('roue AVD', RoueCamion.new)
    @camion.ajouter_piece('roue ARG', RoueCamion.new)
    @camion.ajouter_piece('roue ARD', RoueCamion.new)
  end

  def peindre
    puts "Je peins le camion en " + @options['couleur_peinture']
  end

  def donner_vehicule
    @camion
  end

  private

  attr_reader :camion, :options
end


monteur_voitures = MonteurVoitures.new({'nb_portes' => 5, 'couleur_peinture' => 'rouge'})
monteur_camions = MonteurCamions.new({'couleur_peinture' => 'bleu'})

[monteur_voitures, monteur_camions].each do |monteur|
  directeur = Directeur.new(monteur)
  directeur.monter
  vehicule = monteur.donner_vehicule
end

Ruby: design pattern Abstract Factory

Abstract Factory vs. Factory

La différence entre le design pattern Abstract Factory et Factory est très minime: Factory ne fabrique qu’un type de produit (d’objet) alors qu’Abstract Factory crée des familles de produits, fonctionnellement proches.

Choisir les abstractions

Pour que les abstractions qui sont à la racine de l’arbre représentant la hiérarchie d’héritage demeurent des classes abstraites (simulées, car de telles classes n’existent pas en Ruby), j’y ai placé le constructeur en le forçant à lever une exception pour rendre l’instanciation impossible, ce qui est le propre d’une classe abstraite. Ceci nous force à « répéter » la méthode initialize dans les classes filles, mais ce n’est pas bien grave car le jour où elles seront amenées à différer selon les cas, elles seront déjà isolées dans les bonnes sous-classes. Le but était de rester le plus fidèle possible à ce qu’est ce design pattern, tout en prenant en compte les contraintes du langage.

class ProduitEntretien
  def initialize
      raise "Impossible d'instancier la classe ProduitEntretien"
  end

  def points_vente
    raise NotImplementedError
  end

  attr_reader :caracteristiques
end

class LessiveIndustrielle < ProduitEntretien
  def initialize(caracteristiques)
    @caracteristiques = caracteristiques
  end

  def points_vente
    ['Grande Distribution']
  end
end

class ProduitVaisselleIndustriel < ProduitEntretien
  def initialize(caracteristiques)
    @caracteristiques = caracteristiques
  end

  def points_vente
    ['Grande Distribution']
  end
end

class LessiveEcologique < ProduitEntretien
  def initialize(caracteristiques)
    @caracteristiques = caracteristiques
  end

  def points_vente
    ['Grande Distribution', 'Supérettes Bio', 'Marchés']
  end
end

class ProduitVaisselleEcologique < ProduitEntretien
    def initialize(caracteristiques)
        @caracteristiques = caracteristiques
    end
  def points_vente
    ['Supérettes Bio', 'Marchés']
  end
end

class FabriqueProduitEntretien
  def initialize
    raise "Impossible d'instancier la classe FabriqueProduitEntretien"
  end

  def fabriquer_lessive
    raise NotImplementedError
  end

  def fabriquer_produit_vaisselle
    raise NotImplementedError
  end
end

class FabriqueProduitEntretienEcologique < FabriqueProduitEntretien
  def initialize
    # TODO: à compléter
  end

  def fabriquer_lessive
    LessiveEcologique.new(['tensioactifs' => 'naturels', 'colorants' => 'naturels', 'parfum' => 'huiles essentielles'])
  end

  def fabriquer_produit_vaisselle
    ProduitVaisselleEcologique.new(['base' => 'savon Marseille', 'additifs' => ['soude', 'vinaigre']])
  end
end

class FabriqueProduitEntretienIndustriel < FabriqueProduitEntretien
  def initialize
    # TODO: à compléter
  end

  def fabriquer_lessive
    LessiveIndustrielle.new(['tensioactifs' => 'chimiques', 'colorants' => 'chimiques', 'parfum' => 'synthétiques'])
  end

  def fabriquer_produit_vaisselle
    ProduitVaisselleIndustriel.new(['tensioactifs' => 'chimiques', 'colorants' => 'chimiques', 'parfum' => 'synthétiques'])
  end
end

fabrique_industrielle = FabriqueProduitEntretienIndustriel.new
lessive_industrielle = fabrique_industrielle.fabriquer_lessive
produit_vaisselle_industriel = fabrique_industrielle.fabriquer_produit_vaisselle

fabrique_ecologique = FabriqueProduitEntretienEcologique.new
lessive_ecologique = fabrique_ecologique.fabriquer_lessive
produit_vaisselle_ecologique = fabrique_ecologique.fabriquer_produit_vaisselle

p produit_vaisselle_industriel.points_vente
p produit_vaisselle_ecologique.points_vente

p produit_vaisselle_industriel.caracteristiques
p produit_vaisselle_ecologique.caracteristiques

Ruby on Rails: design pattern Factory

Pour traiter du design pattern Factory en Ruby (dans le framework Rails), j’ai repris les exemples faits en PHP il y a déjà 7 ans !

Comme les principes ne diffèrent pas d’un langage à l’autre, seul le code figurera ici. Pour les explications, je vous renvoie au lien ci-dessus !

# on importe de quoi utiliser constantize
require 'active_support/inflector'

# l'interface des fabriques (ici une classe abstraite)
class FabriqueFacture
  def initialize
    raise "Impossible d'instancier la classe abstraite FabriqueFacture"
  end

  def fabriquer
    raise NotImplementedError
  end
end

# les fabriques concrètes
class FabriqueEnteteFacture < FabriqueFacture
  CLASSE_CIBLE = 'Entete'
  private_constant :CLASSE_CIBLE

  def initialize
    # on écrase le constructeur qui lève une exception dans la classe mère
  end

  def fabriquer
    CLASSE_CIBLE.constantize.new
  end
end

class FabriqueCorpsFacture < FabriqueFacture
  CLASSE_CIBLE = 'Corps'
  private_constant :CLASSE_CIBLE

  def initialize(produits)
    @produits = produits
  end

  def fabriquer
    CLASSE_CIBLE.constantize.new(@produits)
  end

  private

  attr_accessor :produits
end

class FabriquePiedPageFacture < FabriqueFacture
  CLASSE_CIBLE = 'PiedPage'
  private_constant :CLASSE_CIBLE

  def initialize
    # même remarque que pour le constructeur de FabriqueEnteteFacture
  end

  def fabriquer
    CLASSE_CIBLE.constantize.new
  end
end

# les produits CONCRETS
class PiedPage
  def formater
    "Je formate mon pied de page"
  end
end

class Entete
  def formater()
    "Je formate mon entête"
  end
end

class Corps
  def initialize (produits)
    @produits = produits
  end

  def formater
    "Je formate mon corps avec mes #{@produits.count} produits"
  end
end

# le code client pour tester toute la chaîne

class Facturation
  def initialize(fabrique_entete, fabrique_corps, fabrique_pied)
    if !((fabrique_entete.is_a? FabriqueFacture) && 
        (fabrique_corps.is_a? FabriqueFacture) && 
        (fabrique_pied.is_a? FabriqueFacture))
      raise "Les fabriques doivent dériver FabriqueFacture"
    end
    @entete = fabrique_entete.fabriquer
    @corps = fabrique_corps.fabriquer
    @pied = fabrique_pied.fabriquer
  end

  def declencher
    puts @entete.formater
    puts @corps.formater
    puts @pied.formater
  end

  private

  attr_accessor :entete, :corps, :pied
end

produits = ['Gourde', 'Ballon', 'Pioche']

facturation = Facturation.new(
    FabriqueEnteteFacture.new,
    FabriqueCorpsFacture.new(produits),
    FabriquePiedPageFacture.new
)

facturation.declencher

PHP – Le design pattern Strategy

Le design pattern Strategy fait partie de la famille des design patterns comportementaux; il facilite l’utilisation d’algorithmes interchangeables à l’exécution, c’est à dire dynamiquement. Il obéit au bon vieux principe de la programmation orientée objet : Encapsuler ce qui varie.

Design pattern Strategy

Les algorithmes à encapsuler

Pour un comportement identique (reagir) nous avons trois implémentations différentes. Ce sont elles que nous souhaitons isoler pour réduire au maximum l’impact du changement qui tôt ou tard se produira dans notre code. Ce comportement commun attend en paramètre un objet se conformant à l’interface StrategieInterface et ses implémentations variables consistent à ici à appliquer un traitement rudimentaire à la méthode donnerPhrase de cet objet en paramètre.

interface StrategieInterface
{
    public function reagir(PersonneInterface $personne): string;
}
 
class Enerve implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return strtoupper($personne->donnerPhrase().' !!!').PHP_EOL;
    }
}

class Geek implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return str_replace('o', '0', $personne->donnerPhrase()).PHP_EOL;
    }
}

class Jovial implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return ucfirst($personne->donnerPhrase()).' :)'.PHP_EOL;
    }
}

Voici notre classe Personne et l’interface qu’elle implémente. La seule chose qu’elle fait est de retourner la chaîne de caractères bonjour !

interface PersonneInterface
{
    public function donnerPhrase(): string;
}

class Personne implements PersonneInterface
{
    public function donnerPhrase(): string
    {
        return 'bonjour';
    }
}

L’objet Contexte

Voici enfin notre objet Contexte, partie intégrante de notre pattern. Il gère simplement une référence à la stratégie, à laquelle il transmet les requêtes des clients via la délégation à la méthode reagir. C’est en quelque sorte le liant entre le code client et nos différentes stratégies concrètes.

class Contexte
{
    private $strategie;

    public function __construct(StrategieInterface $strategie)
    {
        $this->strategie = $strategie;
    }
    
    public function exprimeReaction(PersonneInterface $personne): string
    {
      return $this->strategie->reagir($personne);
    }
}

Pour finir, écrivons le code client qui va utiliser notre design pattern. Evidemment, nous ne nous adressons pas directement aux stratégies mais nous passons par l’objet Contexte, que nous initialiserons successivement avec des instances de chacune des stratégies concrètes.

$personne = new Personne();
$humeurs = [new Enerve(), new Geek(), new Jovial()];

foreach ($humeurs as $humeur) {
    $contexte = new Contexte($humeur);
    echo $contexte->exprimeReaction($personne);
}

A garder en tête:

  • Un trop grand nombre de if (syndrome appelé la forêt d’ifs) est souvent le signe que Strategy doit être envisagé
  • Gare à la multiplication intempestive des stratégies concrètes, leur nombre peut vite augmenter !
  • Toutes les stratégies concrètes implémentent la même abstraction, il peut parfois arriver qu’elles reçoivent des informations qui ne leur seront pas utiles

Le design pattern Prototype en PHP

Prototype est un design pattern de création: son but est de répliquer des instances dites « prototypes » via un mécanisme de clonage.
Il est prescrit lorsqu’il convient d’éviter de trop nombreuses instanciations d’une classe, notamment lorsque celle-ci possède une logique assez complexe dans son constructeur, ce qui idéalement ne devrait jamais arriver.

Il est important d’économiser les ressources car le coût d’un clonage d’objet reste bien inférieur à celui d’une instanciation. Evidemment, vous ne verrez pas la différence si vous travaillez avec une dizaine d’objets mais ce ne sera sans doute pas le cas si votre application doit gérer 100 000 objets simultanément le jour d’une facturation par exemple.

prototype de voiture

Protype attendant un clonage incognito

Les composants de Protoype

  • une abstraction (classe abstraite ou interface) qui force la présence d’une méthode dédiée au clonage dans les classes concrètes
  • une ou plusieurs classes concrètes dériveront l’abstraction, implémentant de fait la méthode de clonage. Les instanciations seront faites sur ces classes
  • des clones de ces classes seront effectués par le code client, qui participe à ce pattern

Voici notre abstraction; elle force l’implémentation d’une méthode __clone() et par chance, PHP en fourni une « out of the box » comme on dit (oui, on peut aussi dire « nativement ») ! Le reste est très basique: un constructeur sans aucune logique métier, qui prend ce qu’on lui donne et le range consciencieusement, un setter et deux getters.

L’abstraction

abstract class HumainAbstract
{   
    protected $prenom;
    
    protected $sexe;
        
    public function __construct(string $prenom)
    {
        $this->prenom = $prenom;
    }
    
    public function donnerSexe(): string
    {
        return $this->sexe; 
    }
    
    public function changerPrenom(string $prenom)
    {
        $this->prenom = $prenom;
    }
    
    public function donnerPrenom(): string
    {
        return $this->prenom; 
    }
    
    abstract public function __clone();
}

Les classes concrètes

Les classes concrètes – les prototypes – vont dériver cette classe abstraite et, pour rester simples, ne feront absolument rien dans leur méthode __clone() ! Elles ont une variable d’instance qu’il est juste possible de lire mais pas de modifier. Seul le prénom est modifiable (regardez dans la classe abstraite !).

class Male extends HumainAbstract
{
    protected $sexe = 'M';

    public function __clone()
    {
    }
}

class Femelle extends HumainAbstract
{
    protected $sexe = 'F';
    
    public function __clone()
    {
    }
}

Le code client

Le client est un acteur à part entière du design pattern prototype, c’est lui qui va cloner les prototypes.
Ici nous décidons que, les premiers humains ayant sans doute communiqué entre eux par un langage très primitif, notre prototype de mâle s’appellera « Rrrnnngrrwggl », tandis que la femelle portera le doux sobriquet de « Nyyyynyaaa ».

Tous les êtres humains descendant de ces respectables parents à la pilosité prononcée ne seront que des clones dont nous changerons uniquement le prénom. Vous noterez que nous utilisons l’affectation dynamique, c’est à dire que chaque clone sera rangé dans une variable dont le nom sera incrémenté à chaque itération de notre boucle.

$prenoms = [
        'René', 'Eric', 'Jean', 'Robert', 'Marius',
        'Kevin', 'Léo', 'Jacques', 'Loïc', 'John',
        'Alexis', 'Kenneth', 'Nathanaël', 'Christophe'
    ];
    
$male1 = new Male('Rrrnnngrrwggl');

$numeroClone = 0;
$clones = [];

foreach ($prenoms as $prenom) {
    ++$numeroClone;
    
    $nomClone = 'clone'.$numeroClone;
    $$nomClone = clone $male1;
    $$nomClone->changerPrenom($prenom);
    $clones[] = $$nomClone;
}

$prenoms = [
        'Lise', 'Marie', 'Ninon', 'Rachida', 'Ana',
        'Martine', 'Svetlana', 'Eve', 'Carole',
        'Sylvie', 'Laurie', 'Zhang', 'Fatoumata'
    ];

$femelle1 = new Femelle('Nyyyynyaaa');
    
foreach ($prenoms as $prenom) {
    ++$numeroClone;
    
    $nomClone = 'clone'.$numeroClone;
    $$nomClone = clone $femelle1;
    $$nomClone->changerPrenom($prenom);
    $clones[] = $$nomClone;
}

foreach ($clones as $clone) {
    echo $clone->donnerSexe().'/'.$clone->donnerPrenom().PHP_EOL;
}

A retenir

  • N’implémentez pas Prototype si vous devez gérer un petit nombre de copies, ce serait tuer une mouche au lance-roquettes
  • Lors d’un appel à __clone(), le constructeur n’est pas appelé (c’est le but)
  • Si votre objet à cloner a des objets en compositions, pensez à les cloner eux aussi pour éviter de pointer vers une référence qui n’est pas la bonne…
  • Prenez garde aux références circulaires (A dépend de B qui dépend de C qui dépend de A…) entre les objets

PHP : le design pattern Proxy

Proxy, proxy…proxy de cache, proxy Web…vous avez sans doute déjà lu ce terme quelque part, n’est-ce pas ? Si oui, alors vous avez déjà sa principale raison d’être en tête : un proxy s’intercale entre vous et…quelque chose !

En programmation, ce quelque chose est un objet « distant »…distant parce qu’il peut se trouver ailleurs sur le réseau mais pas seulement ! Il peut très bien se trouver sur la même machine mais dans un autre espace d’adressage. En pratique, le proxy implémente la même interface que l’objet auquel il sert d’écran, car il va se substituer à lui !

Ce design pattern fait état de relations entre des objets, voilà pourquoi on dit qu’il est structurel ! Proxy (ou Procuration ou encore Surrogate en anglais) a des similitudes avec un autre pattern structurel : Décorateur. Cependant, il convient de bien garder à l’esprit que si Décorateur a pour but d’ajouter des fonctionnalités à l’objet décoré, Proxy est souvent là pour effectuer un contrôle d’accès à un objet.

Si vous avez déjà travaillé avec le framework Symfony et l’ORM Doctrine, cette notion de proxy ne vous est pas inconnue car vous manipulez des proxies en bien des occasions !

Gardes du corps peu enclins à la discussion

Les proxies du président de la République ! Pour émettre une requête à ce dernier, c’est par eux qu’il faudra passer !

L’interprète, un mandataire idéal !

Voilà un nouveau terme français pour notre Proxy : mandataire. Quel meilleur exemple que celui de l’interprète, par lequel il est impératif de passer lors des sommets internationaux, si l’on veut être compris des grands de ce monde (et éventuellement lui faire porter la responsabilité d’un incident diplomatique) ! Voilà un exemple très simple pour illustrer ça : des interfaces que le sujet réel (le président russe) et son proxy (l’interprète russe-français) implémentent tous deux et un président hôte qui prend en composition un interprète avec lequel il va parler tandis que celui-ci va de son côté discuter avec le sujet réel. Dans cet exemple, j’en profite pour utiliser la dérivation d’interfaces, le mot clé final, une classe abstraite…En réalité j’aurais largement pu simplifier le code, j’aurais pu aussi injecter directement l’interprète dans le constructeur du président hôte, qui mène les conversations, au lieu de mettre un setter dont je ne suis pas spécialement adepte, bref j’aurais pu faire des tas de choses autrement mais je veux garder le code efficace et amusant autant que possible !

interface PersonneInterface
{
    public function parlerDuTemps(): string;
}

interface PresidentInterface extends PersonneInterface
{
    public function parlerDuRechauffementClimatique(): string;
    public function parlerDesGuerres(): string;
}
 
final class PresidentRusse implements PresidentInterface
{
    public function parlerDuTemps(): string
    {
        return 'Я очень рад быть здесь, погода прекрасная в Париже';
    }

    public function parlerDuRechauffementClimatique(): string
    {
        return 'это очень серьезная проблема !';
    }

    public function parlerDesGuerres(): string
    {
        return 'какая война?';
    }
}

interface InterpreteInterface
{
    public function boireUnVerreEau(): string;
}

abstract class Interprete implements InterpreteInterface
{
    protected $presidentHote;
    
    protected $presidentInvite;

    public function __construct(PresidentInterface $presidentHote,
                              PresidentInterface $presidentInvite)
    {
        $this->presidentHote = $presidentHote;
        $this->presidentInvite = $presidentInvite;
    }
    
    public function boireUnVerreEau(): string
    {
        return 'Glou Glou Glou'.PHP_EOL;
    }
}
 
final class InterpreteRusse extends Interprete implements PresidentInterface
{
    public function parlerDuTemps(): string
    {
        $temps = 'Au sujet du temps, le président russe me dit : "';
        $temps .= $this->presidentInvite->parlerDuTemps().'"'.PHP_EOL;
        
        return $temps;
    }

    public function parlerDuRechauffementClimatique(): string
    {
        $climat = 'Au sujet du climat, le président russe me dit : "';
        $climat .= $this->presidentInvite->parlerDuRechauffementClimatique().'"';
        
        return $climat.PHP_EOL;
    }

    public function parlerDesGuerres(): string
    {
        if ($this->presidentHote instanceof PresidentFrancais) {
            return 'Le président russe ne souhaite pas évoquer le sujet 
                    avec le président français ! Prenons plutôt Vodka !'.PHP_EOL;
        }
        
        $guerre = 'Au sujet des guerres, le président russe me dit : "';
        $guerre .= $this->presidentInvite->parlerDesGuerres().'"'.PHP_EOL;
        
        return $guerre;
    }
}
 
final class PresidentFrancais implements PresidentInterface
{
    private $interprete;
     
    public function attacherInterprete(InterpreteInterface $interprete): void
    {
        $this->interprete = $interprete;
    }
    
    public function parlerDuTemps(): string
    {
        return 'Président, il fait bon vivre à Paris, n'est-ce pas ?'.PHP_EOL;
    }

    public function parlerDuRechauffementClimatique(): string
    {
        return 'Que pensez-vous du réchauffement climatique ?'.PHP_EOL;
    }

    public function parlerDesGuerres(): string
    {
        return 'Que vous inspirent les conflits mondiaux ?'.PHP_EOL;
    }
    
    public function discuterSurPerron(): void
    {
        if (!$this->interprete) {
            throw new RuntimeException('Où est l\'interprète ?');
        }
         
        echo $this->parlerDuTemps();
        echo $this->interprete->parlerDuTemps();
        
        echo $this->parlerDuRechauffementClimatique();
        echo $this->interprete->parlerDuRechauffementClimatique();
        
        echo $this->parlerDesGuerres();
        echo $this->interprete->parlerDesGuerres();
    }
}
 
$presidentFrancais = new PresidentFrancais();
$presidentRusse = new PresidentRusse();
$InterpreteRusse = new InterpreteRusse($presidentFrancais, $presidentRusse);

$presidentFrancais->attacherInterprete($InterpreteRusse);
 
try {
    $presidentFrancais->discuterSurPerron();
} catch (RuntimeException $exception) {
    echo "Allô, ici le chef du protocole !", PHP_EOL;
    echo "Le président vient de me dire '" . 
              $exception->getMessage(), "'", PHP_EOL;
    echo "Vite, allez chercher un interprète !", PHP_EOL;
}

echo $InterpreteRusse->boireUnVerreEau();

Une fois de plus, voilà un design pattern fait la part belle à l’abstraction !

Vous notez ici que la tentation serait grande pour notre interprète de proposer des fonctionnalités que ne propose pas le président dont il assure la traduction. En faisant ainsi, nous nous éloignerions de Proxy – dont le rôle consiste majoritairement à faire de la délégation – pour adopter une approche Décorateur.

Notre interprète russe joue également le rôle de proxy de protection en filtrant les accès au président russe sur des questions épineuses: en l’occurrence, le président russe n’a pas tellement envie de parler des guerres avec son homologue français et l’interprète a reçu lors de son briefing des instructions sans équivoque…heureusement qu’en bon russe, il a prévu un verre de vodka pour désamorcer tout début de crise diplomatique ! Faire des vérifications de droit d’accès, voilà aussi un autre aspect de ce design pattern.

Le chargement fainéant

Personne n’emploie ce terme en réalité, mais il me fait rire ! Le lazy loading consiste à différer le chargement du vrai objet (celui qui nous mandate, qui nous donne procuration) jusqu’au moment où l’on s’en servira effectivement ! Ceci est notamment utile lorsqu’un objet est un plutôt gourmand en ressources.

Prenons l’exemple d’une classe qui sert habituellement à manipuler des images. Lorsqu’on l’instancie, elle range dans une variable d’instance prévue à cette effet la totalité du flux de l’image qu’on lui passe en paramètre.

Notre proxy, qui possède le même super-type que la classe image (une classe abstraite implémentant une interface) ne va pas effectuer ce chargement mais il va attendre le dernier moment pour instancier la classe qui le mandate et appeler dessus les méthodes demandées par le client :

interface ImageInterface
{
    public function donnerTaille(): int;
    public function aContenu(): bool;
    public function afficherContenu(): ?string;
}

abstract class AbstractImage implements ImageInterface
{
	protected $cheminFichier;
	protected $contenuFichier;
	protected $tailleFichier;
	
	public function __construct($cheminFichier)
	{
		$this->cheminFichier = $cheminFichier;
	}
	
	public function donnerTaille(): int
	{
		return $this->tailleFichier;
	}
	
	public function aContenu(): bool
	{
		return null !== $this->contenuFichier;
	}
}

class StandardImage extends AbstractImage
{
	public function __construct($cheminFichier)
	{
		parent::__construct($cheminFichier);
		$this->contenuFichier = file_get_contents(
                                          $this->cheminFichier);
		$this->tailleFichier = filesize(
                                          $this->cheminFichier);
	}
	
	public function afficherContenu(): ?string
	{
		return $this->contenuFichier;
	}
}

class ProxyImage extends AbstractImage {
	
	private $vraieImage;
	
	public function __construct($cheminFichier)
	{
		parent::__construct($cheminFichier);
		$this->tailleFichier = filesize($this->cheminFichier);
	}
	
	public function afficherContenu(): ?string
	{
		if (!$this->vraieImage) {
			$this->vraieImage = new StandardImage(
                                              $this->cheminFichier);
		}
		
		return $this->vraieImage->afficherContenu();
	}
}

final class GestionnaireImage
{
	public function traiterImage (ImageInterface $image): void
	{
		echo $image->donnerTaille() . ' octets';
		echo 'Contenu présent ?'.($image->aContenu());
		echo $image->afficherContenu();
	}
}

$gestionnaireImage = new GestionnaireImage();

$image = new StandardImage('elephant.jpg');
echo $gestionnaireImage->traiterImage($image);

$proxy = new ProxyImage('elephant.jpg');
echo $gestionnaireImage->traiterImage($proxy);

Le client (GestionnaireImage) travaille d’ordinaire avec des objets de la classe StandardImage, qui, dès qu’elle est instanciée, stocke le flux complet du fichier ciblé dans une variable d’instance. Ceci peut s’avérer extrêmement coûteux si l’image est de grande taille ou si un grand nombre d’images sont requises en même temps par différents utilisateurs de notre classe, voire les deux !

Nous intercalons donc un objet Proxy entre le client de notre code et le sujet ciblé : ProxyImage possède le même super-type que StandardImage, il est donc tout à fait capable d’agir en qualité de mandataire ! Son rôle sera de différer la construction du sujet ciblé jusqu’au moment où son utilisation sera requise; il doit pour cela posséder une référence à ce sujet, voilà pourquoi vous voyez la donnée membre privée $vraieImage dans ProxyImage ! C’est le mandataire qui instancie le sujet, au départ il possède sur le fichier image une référence indirecte (le nom de fichier) puis finit par obtenir une référence directe (l’objet StandardImage, avec le flux complet).

Dans le cas de StandardImage, le contenu du fichier cible est intégralement stocké dans la propriété privée dédiée à cet effet, mais pas dans le cas de ProxyImage qui n’instanciera la classe mandatée que lorsque la méthode afficherContenu sera invoquée. La valeur retournée par $image->aContenu() vaudra TRUE dans le cas de StandardImage et FALSE dans le cas du Proxy; c’est bien le signe que le Proxy fait l’économie de la lecture du flux du fichier image sur lequel nous travaillons. Cependant, lorsque le client demande l’affichage de l’image, le Proxy ne peut faire autrement que d’instancier StandardImage pour invoquer dessus la méthode qui va retourner le flux utile à cet affichage.

Au final, le mandataire est plus efficace que le sujet réel qu’il masque puisque lors de l’appel à donnerTaille dans traiterImage, il n’a pas récupéré l’intégralité du flux binaire du fichier cible. C’est évidemment ce qu’il fait lorsqu’il doit afficher celui-ci, ne pouvant faire autrement.

Pour utiliser cet exemple, il vous faudra bien entendu une image nommée elephant.jpg que je vous fournis un peu plus bas !

Quand on doit différer les opérations qui s’avèrent coûteuses lors de la création d’un objet au moment où elle seront effectivement requises alors le design pattern Proxy peut s’avérer d’une aide précieuse !

La belle image qui a servi à mes tests !

La belle image qui a servi à mes tests !

PHP : le design pattern Observateur

Observateur, un design pattern comportemental

Après avoir vu Adaptateur, Décorateur, Template Method ou Factory, nous allons nous concentrer sur un design pattern comportemental : Observateur. Comme tous les design patterns comportementaux (au sens GoF du terme), Observateur décrit la manière dont des objets interagissent entre eux.

Quels sont ces objets ?

Ce design pattern met en jeu deux types d’objets :

  • un sujet
  • des observateurs

Un sujet notifie un ou plusieurs observateurs qu’un changement d’état vient de se produire chez lui.

Personne observant le ciel aux jumelles

Un observateur attendant avec impatience une notification en provenance d’un sujet.

Dans quel état j’erre ?

Le sujet possède un état interne; lorsque cet état est altéré, il va notifier ses observateurs de ce changement. Prenons un exemple trivial, qui va parler au plus grand nombre : lorsqu’une personne présente sur un réseau social (notre sujet) fête son anniversaire, son état change car sa propriété âge est incrémenté d’une unité et quiconque suit ce sujet (un observateur, donc) reçoit une notification l’avertissant de ce changement d’état. L’observateur se met à jour et affiche la bonne nouvelle à l’écran: « X fête son anniversaire, n’oubliez pas de lui souhaiter ! »…

Structure du design pattern Observateur

Voici la version telle que définie par le Gang of Four :

Structure du design pattern Observateur

Toute la puissance de ce design pattern réside dans le fait que sa structure, à base d’abstractions, induit un couplage faible entre les deux types d’objets qui le composent : un sujet ne sait rien du type concret de ses observateurs, si ce n’est qu’il se conforme en tous points à l’interface Observer, pas plus qu’il n’est au courant de leur nombre. Le sujet n’aura donc aucunement à se risquer à d’hasardeuses hypothèses sur la nature de ceux qui l’observent et n’importe quel objet sera en mesure de l’observer, du moment que la classe concrète dont il est issu implémente l’interface Observer.

Lorsque j’évoque ici la notion d’interface, c’est au sens large du terme et pas stricto sensu. A l’origine – voilà maintenant 20 ans – GoF préconisait l’utilisation de classes abstraites, à dériver en classes concrètes. Mais vous ne le savez que trop bien, l’héritage n’est pas une panacée universelle, loin s’en faut.

Quelques points clés de ce modèle :

  • Sujet connait l’ensemble de ses observateurs. Leur nombre n’est pas limité.
  • Les observateurs s’enregistrent lorsqu’ils veulent observer un sujet et s’en détachent lorsqu’ils ne le veulent plus.
  • Lorsque son état est altéré, Sujet notifie l’ensemble de ses observateurs, sans notion d’ordre et sans distinction !
  • Lorsqu’il est notifié, un observateur peut obtenir des informations en provenance du sujet. Il peut décider de gérer une notification ou bien de l’ignorer.
  • Rien n’interdit à un observateur de suivre plusieurs sujets. Il faut évidemment que cet observateur puisse déterminer l’origine des notifications qu’il reçoit.

Un petit bémol, toutefois : en l’état actuel des choses, la fonction de mise à jour de l’observateur (Update) ne permet pas de savoir ce qui a changé dans le sujet, laissant à la charge de l’observateur la responsabilité de « deviner » ce qui a entrainé une modification.

La mise à jour, côté observateur

Nous allons voir comment à l’autre bout du fil, les observateurs gèrent leur mise à jour d’état avec la fonction Update.

Tu tires ou tu pousses ?

Deux modes de fonctionnement s’offrent à nous :

  • par poussée (push)
  • par traction (pull)

Le mode push – « Tiens, prends ça ! »

Le sujet pousse des informations aux observateurs. L’inconvénient de cette façon de faire est que le lien entre le sujet et ses observateurs se resserre. Le sujet peut se retrouver à pousser des informations dont certains observateurs n’auront pas besoin. Que pousser et à qui ?

Le mode pull – « Viens te servir ! »

Le sujet notifie simplement ses observateurs qu’un changement vient d’avoir lieu, à eux de savoir ce qui a changé !

Des versions basiques d’Observateur

Les contrats à respecter

Vous le savez maintenant, un sujet notifie N observateurs. On lui attache/détache ces observateurs à l’aide des méthodes appropriées et il les informe d’un changement via l’appel à la méthode mettreAJour de chacun d’entre eux. Voici à quoi vont ressembler nos contrats d’interface (dans la version GoF, ces abstractions sont des classes abstraites, ici nous utilisons des interfaces).

interface SujetInterface
{
    public function attacher(ObservateurInterface $observateur): void;
    public function detacher(ObservateurInterface $observateur): void;
    public function notifier(): void;
}

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet): void;
}

La notification en version pull

Le sujet

Notre sujet stocke l’ensemble de ses observateurs dans une variable d’instance privée prévue à cet effet. Ce tableau est rempli ou vidé au fur et à mesure que les observateurs sont attachés ou détachés. Un setter mettreAJourLesNouvelles est prévu pour modifier la variable d’instance $nouvelles et un getter donnerLesNouvelles servira aux observateurs pour aller récupérer le dernier état du sujet. Souvenez-vous qu’en mode pull, ce sont les observateurs qui doivent se renseigner sur l’état du sujet et éventuellement mettre à jour le leur avec ces informations tirées depuis le sujet.

class Sujet implements SujetInterface
{
    private $nouvelles;

    private $observateurs;
    
    public function attacher(ObservateurInterface $observateur): void
    {
        $this->observateurs[] = $observateur;
    }
 
    public function detacher(ObservateurInterface $observateur): void
    {
        $key = array_search($observateur, $this->observateurs);
 
        if (false !== $key) {
            unset($this->observateurs[$key]);
        }
    }
 
    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this);
        }
    }

    public function mettreAJourLesNouvelles(string $nouvelles): void 
    {
        $this->nouvelles = $nouvelles;
        $this->notifier();
    }
    
    public function donnerLesNouvelles(): string 
    {
        return $this->nouvelles;
    }
}

L’observateur

C’est lui qui fait le travail, une fois qu’il est notifié à travers sa méthode mettreAJour. Il reçoit le sujet à l’origine de la notification de mise à jour car il peut observer plusieurs sujets; il nous faut donc impérativement savoir qui a changé parmi les objets qu’on observe. Ici nous avons réduit la structure de la classe à sa plus simple expression: l’observateur reçoit une notification de nouvelle de la part d’un sujet, il va TIRER l’information depuis ce sujet et mettre son état interne en accord avec celui du sujet, afin d’avoir les toutes dernières nouvelles.

class Observateur implements ObservateurInterface
{
    private $dernieresNouvelles;
    
    public function mettreAJour(SujetInterface $sujet): void
    {
        $this->dernieresNouvelles = $sujet->donnerLesNouvelles();
    }
}

Le code client

Voici le code pour utiliser cette structure en mode pull, il est très simple: nous créons deux observateurs que nous attachons à notre unique sujet et nous mettons à jour l’état de notre sujet, qui va déclencher deux notifications (une pour chaque observateur).

$sujet = new Sujet();
$observateur = new Observateur();
$autreObservateur = new Observateur();
$sujet->attacher($observateur);
$sujet->attacher($autreObservateur);
$sujet->mettreAJourLesNouvelles("La bourse dévisse !");

La notification en version push

Changement d’interface

En mode push, c’est le sujet qui pousse les données modifiées à ses observateurs. Il sait donc ce qu’il leur faut, ce qui induit un couplage plus serré que dans le mode pull. Il peut aussi leur passer son état complet mais attention si les informations sont volumineuses…Les observateurs ont-ils besoin de recevoir un sac postal complet alors qu’ils attendent une carte postale ?

Quoiqu’il en soit, l’interface de l’observateur change et donc l’appel de la méthode aussi, dans le sujet:

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet, array $donnees): void;
}

Le sujet

Le sujet fera désormais:

    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this, ["nouvelles" => $this->nouvelles]);
        }
    }

L’observateur

L’observateur ira quant à lui se servir à ladite clé dans le tableau $donnees, qui dans la pratique sera évidemment bien plus fourni:

    public function mettreAJour(SujetInterface $sujet, array $donnees): void
    {
        $this->dernieresNouvelles = $donnees["nouvelles"];
    }

Le code client ne bougera pas d’un pouce, c’est la cuisine interne de l’objet qui change très légèrement. Le principe est rigoureusement identique !

Notre version d’Observateur

Qu’est-ce qui interdit à nos sujets d’être des observateurs d’autre sujets ? Rien !
Nous allons simuler un réseau social.

Nos interfaces

Elles ne changent pas beaucoup des précédentes:

interface SujetInterface
{
    public function attacher(ObservateurInterface $observateur): void;
    public function detacher(ObservateurInterface $observateur): void;
    public function notifier(): void;
}

interface SuiveurInterface
{
    public function suivre(SujetInterface $sujet): void;
    public function nePlusSuivre(SujetInterface $sujet): void;
}

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet): void;
}

Nous avons juste rajouté une interface SuiveurInterface qui liste ce que doit savoir faire un suiveur: suivre et cesser de suivre.

Le membre

Dans notre réseau social, nous avons deux types de membres: des personnes et des entreprises ou des associations qui ont des pages. Ces deux catégories de membres peuvent suivre des membres, comme sur Facebook. Pouvant être à la fois sujet et observateur, ils doivent implémenter l’ensemble des interfaces. Nous factorisons les comportements dans une classe abstraite qui va contenir pour un membre, les membres qu’il suit et les membres qui le suivent. Les membres suivis iront dans $sujets et les suiveurs, dans $observateurs.

Faisons simple: de base, un membre a juste un nom et nous mettons un getter dessus, même pas de setter.
Vous constatez que la méthode mettreAJour ne contient que le sujet, la notification se fera donc en mode pull.

abstract class AbstractMembre implements SujetInterface, SuiveurInterface, ObservateurInterface
{
    protected $nom;
    
    protected $observateurs;
    
    protected $sujets;
    
    public function __construct(string $nom)
    {
        $this->nom = $nom;
    }
    
    public function attacher(ObservateurInterface $observateur): void
    {
        echo $this->donnerNom().PHP_EOL;
        echo "\t".$observateur->donnerNom()." vous à ajouté à ses contacts".PHP_EOL;
        $this->observateurs[] = $observateur;
    }
 
    public function detacher(ObservateurInterface $observateur): void
    {
        $key = array_search($observateur, $this->observateurs);
 
        if (false !== $key) {
            echo $this->donnerNom().PHP_EOL;
            unset($this->observateurs[$key]);
            echo "\tVous avez enlevé ".$observateur->donnerNom()." de vos contacts".PHP_EOL;
        }
    }
 
    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this);
        }
    }

    public function donnerNom(): string
    {
        return $this->nom;
    }
    
    public function suivre(SujetInterface $sujet): void
    {
        // pour éviter qu'un membre puisse s'auto-suivre
        if ($sujet === $this) {
            return;
        }
        
        $this->sujets[$sujet->donnerNom()] = clone $sujet;
        $sujet->attacher($this);
    }
    
    public function nePlusSuivre(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
    
        $key = array_search($sujet, $this->sujets);
 
        if (false !== $key) {
            unset($this->sujets[$key]);
        }
        
        echo "\tVous ne suivez plus ".$sujet->donnerNom().PHP_EOL;
    }
    
    public function mettreAJour(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
        $nom = $sujet->donnerNom();
        
        if (array_key_exists($nom, $this->sujets)) {
            $sujetStocke = $this->sujets[$nom];
            
            if ($sujet instanceof PageCommerciale) {
                $urlMagasin = $sujet->donnerUrlMagasin();
                $urlMagasinStocke = $sujetStocke->donnerUrlMagasin();
                
                if ($urlMagasin !== $urlMagasinStocke) {
                    echo "\tLe site web de $nom vaut désormais $urlMagasin";
                }

            } elseif ($sujet instanceof Membre) {
                $age = $sujet->donnerAge();
                $ageStocke = $sujetStocke->donnerAge();

                if ($ageStocke !== $age) {
                    echo "\t$nom fête son anniversaire, il a $age ans";
                    $this->sujets[$nom] = clone $sujet;
                }
                
                $hobbies = $sujet->donnerHobbies();
                $hobbiesStocke = $sujetStocke->donnerHobbies();

                if ($hobbiesStocke !== $hobbies) {
                    echo "\t$nom a de nouveaux hobbies: ".implode(", ", $hobbies);
                    $this->sujets[$nom] = clone $sujet;
                }
            }
            echo PHP_EOL;
        }
    }
}

Les classes concrètes

On en compte deux, une pour les membres et une pour les pages commerciales. Un membre a un âge et des hobbies, une page commerciale ne comporte que l’URL du site marchand. Là aussi j’ai vraiment gardé l’essentiel dans chaque classe, qui est sur-simplifiée. Voici notre classe Membre:

final class Membre extends AbstractMembre {
 
    private $age;
     
    private $hobbies;
     
    public function changerAge(int $age): void {
        $this->age = $age;
        $this->notifier();
    }
     
    public function changerHobbies(array $hobbies): void {
        $this->hobbies = $hobbies;
        $this->notifier();
    }
 
    public function donnerAge(): ?int {
        return $this->age;
    }
    
    public function donnerHobbies(): ?array {
        return $this->hobbies;
    }
}

et voici notre classe PageCommerciale:

final class PageCommerciale extends AbstractMembre {
 
    private $urlMagasin;
     
    public function changerUrlMagasin(string $urlMagasin): void {
        $this->urlMagasin = $urlMagasin;
        $this->notifier();
    }
 
    public function donnerUrlMagasin(): ?string {
        return $this->urlMagasin;
    }
}

Chaque classe comporte des setters qui vont modifier l’état des objets et donc générer des notifications aux objets qui les observent.

Retour sur la méthode mettreAJour

    public function mettreAJour(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
        $nom = $sujet->donnerNom();
        
        if (array_key_exists($nom, $this->sujets)) {
            $sujetStocke = $this->sujets[$nom];
            
            if ($sujet instanceof PageCommerciale) {
                $urlMagasin = $sujet->donnerUrlMagasin();
                $urlMagasinStocke = $sujetStocke->donnerUrlMagasin();
                
                if ($urlMagasin !== $urlMagasinStocke) {
                    echo "\tLe site web de $nom vaut désormais $urlMagasin";
                }

            } elseif ($sujet instanceof Membre) {
                $age = $sujet->donnerAge();
                $ageStocke = $sujetStocke->donnerAge();

                if ($ageStocke !== $age) {
                    echo "\t$nom fête son anniversaire, il a $age ans";
                    $this->sujets[$nom] = clone $sujet;
                }
                
                $hobbies = $sujet->donnerHobbies();
                $hobbiesStocke = $sujetStocke->donnerHobbies();

                if ($hobbiesStocke !== $hobbies) {
                    echo "\t$nom a de nouveaux hobbies: ".implode(", ", $hobbies);
                    $this->sujets[$nom] = clone $sujet;
                }
            }
            echo PHP_EOL;
        }
    }

Cette méthode est au niveau de la classe abstraite; elle fait une vérification sur le type concret des sujets car les informations à tirer depuis ce sujet ne seront pas les mêmes pour les observateurs concrets. Vous voyez arriver le problème si nous rajoutons des classes concrètes en bas de l’arbre d’héritage, votre if va enfler et il vous faudra peut-être avoir recours à un nouveau design pattern pour gérer ces différentes façons d’opérer selon les types concrets, lequel ?

Notez que nous clonons les objets; il nous faut fossiliser leur état à l’instant t pour pouvoir le comparer avec celui du sujet reçu en paramètre de la méthode.

Le code client

$magasin = new PageCommerciale('ACME Web Store');
$star = new Membre('Rasmus Lerdorf');
$copain = new Membre('Jean-Michel Apeuprey');
$autreStar = new Membre('Novak Djokovic');
$membre = new Membre('Sébastien Ferrandez');

$membre->suivre($magasin);
$membre->suivre($star);
$membre->suivre($autreStar);
$membre->suivre($copain);
$copain->suivre($membre);
$magasin->suivre($autreStar);
$copain->changerAge(41);
$membre->changerHobbies(["guitare"]);
$star->changerAge(36);
$star->changerHobbies(["linux", "c++", "c"]);
$autreStar->changerHobbies(["musculation", "karaoké"]);
$magasin->changerUrlMagasin('http://www.acmestore.com');
// Je n'aime plus le tennis !
$membre->nePlusSuivre($autreStar);
// Je me suis fâché avec Jean-Michel, il n'est plus mon ami !
$membre->detacher($copain);

TADAAAM !

Voici la sortie générée par l’exécution du code client, on n’est pas trop mal on dirait ! N’hésitez pas à proposer des alternatives ou poser des questions dans les commentaires !

Sortie écran pour les observateurs

Où trouve t-on des implémentations d’Observateur ?

Typiquement, dans la programmation par événements où des classes souscrivent auprès d’une classe gestionnaire d’évènements qui les informera lorsque l’évènement auquel elles ont souscrit se produit (en appelant parfois des fonctions de callback).

Lorsque vous cliquez sur un article pour l’ajouter et que votre panier se met à jour, vous avez un bel exemple d’Observateur.

Sur les réseaux sociaux, comme dans notre exemple trivial, nous recevons des notifications dès qu’un événement d’une personne que l’on suit se produit (changement de statut, nouveau post etc.)

En PHP, la SPL propose des classes pour mettre en place le design pattern Observateur avec SPLObserver.

Enfin dans Symfony, les évènements sont gérés selon ce principe…Les observateurs sont des listeners ou des event subscribers auquel on dispatche des notifications contenant des events.

PHP : le design pattern Adaptateur

Chez Auto Load, tout est permis !

Pour illustrer notre exemple du design pattern Adaptateur, nous allons cette fois nous prendre pour des inspecteurs du permis de conduire ! Chez AUTO LOAD, nous nous vantons d’obtenir un taux de réussite de 99% à l’examen du permis de conduire et ce sans soudoyer de quelque manière que ce soit nos candidats ! Voyons sans tarder les principales classes qui composent notre exemple. Tout d’abord, la classe InspecteurPermisConduire, qui est notre classe cliente. C’est elle qui va faire usage de nos autres classes.

class InspecteurPermisConduire {
    private $_candidat;
	
    public function __construct(ConducteurInterface $conducteur)
    {
        $this->_candidat = $conducteur;
    }
    public function changerCandidat(ConducteurInterface $conducteur)
    {
        $this->_candidat = $conducteur;
    }
    public function fairePasserExamen()
    {
        $this->_candidat->demarrer();
        $this->_candidat->accelerer();
        $this->_candidat->tournerDroite();
        $this->_candidat->accelerer();
        $this->_candidat->tournerGauche();
        $this->_candidat->ralentir();
        $this->_candidat->reculer();
        $this->_candidat->immobiliser();
    }
}

Une instance donnée d’InspecteurPermisConduire prend en composition tout objet se conformant à l’interface ConducteurInterface : tout d’abord, elle l’exige dans son constructeur, c’est à dire qu’aucune instance d’InspecteurPermisConduire ne peut exister sans un objet qui implémente en totalité ConducteurInterface. Ensuite, nous nous réservons la possibilité de changer de type de candidat à l’exécution (runtime) grâce à un setter. Enfin, la valeur ajoutée de notre classe réside dans sa méthode fairePasserExamen, qui fait effectuer à tout conducteur les manœuvres de base, indispensables pour décrocher le précieux sésame !

Voici justement notre Automobiliste :

interface ConducteurInterface {
	public function demarrer();
	public function tournerGauche();
	public function tournerDroite();
	public function accelerer();
	public function ralentir();
	public function reculer();
	public function immobiliser();
}

class Automobiliste implements ConducteurInterface {
	public function demarrer() {
		echo "tourner la clé de contact ou mettre la carte";
	}
	public function tournerGauche() {
		echo "tourner le volant vers la gauche";
	}
	public function tournerDroite() {
		echo "tourner le volant vers la droite";
	}
	public function accelerer() {
		echo "appuyer sur la pédale d'accélération";
	}
	public function ralentir() {
		echo "relâcher la pédale d'accélération et/ou", 
                      "appuyer sur la pédale de frein";
	}
	public function reculer() {
		echo "passer la marche arrière et accélérer";
	}
	public function immobiliser() {
		echo "mettre le frein à main";
	}
}

Notre classe concrète Automobiliste implémente les fonctions imposées par l’interface ConducteurInterface. Cette interface recense les principales manoeuvres dont la maîtrise est exigée de tout conducteur de véhicule qui se respecte : démarrer et arrêter le véhicule, avancer, reculer, tourner, accélérer et ralentir. Attention cependant, certains véhicules – comme un vélo – ne se démarrent pas, il va falloir prendre en compte cet aspect là…

Bref, à ce stade là, tout est pour le mieux dans le meilleur des mondes ! [Candide]

Schéma UML de l'exemple de design pattern adaptateur

Dès que le vent soufflera, je recodera… (air bien connu)

Jusqu’au jour où Otto Hekohl, le propriétaire de notre école de conduite, vient nous annoncer qu’étant donné que nous sommes situés en bord de mer, il serait plutôt judicieux – en plus d’être diablement bénéfique pour notre chiffre d’affaire – que nous puissions faire passer le permis bateau à des candidats qui sont de plus en plus nombreux et demandeurs…

Photos de gens sautant d'un yacht

Elle est parfois dure la vie à bord d’un frêle esquif !

Avant de se faire congédier par notre patron pour avoir regardé trop de vidéos de chatons montant sur le dos de tortues durant ses heures de travail, Jojo Dingo, le deuxième programmeur de l’entreprise, avait envisagé d’intégrer ce changement de notre stratégie commerciale. Voici la structure logicielle dont nous héritons, maintenant que Jojo a toutes ses journées de libre pour s’adonner à sa félinophilie maladive :

interface NavigateurInterface {
	public function demarrer();
	public function reculer();
	public function tournerBabord();
	public function tournerTribord();
	public function accelerer();
	public function ralentir();
	public function jeterAncre();
}

abstract class Marin implements NavigateurInterface {
	public function jeterAncre() {
		echo "jeter l'ancre à la mer";
	}
}

class UnsupportedMethodException extends Exception {}

class MarinVoile extends Marin {
	public function demarrer() {
		throw new UnsupportedMethodException
                     ("Cette fonctionnalité n'est pas disponible");
	}
	public function tournerBabord() {
		echo "diriger les voiles et la barre pour aller à babord";
	}
	public function tournerTribord() {
		echo "diriger les voiles et la barre pour aller à tribord";
	}
	public function accelerer() {
		echo "positionner les voiles et déterminer l'allure";
	}
	public function ralentir() {
		echo "positionner les voiles et déterminer l'allure";
	}
	public function reculer() {
		echo "positionner les voiles et manœuvrer pour reculer";
	}
}

class MarinMoteur extends Marin {
	public function demarrer() {
		echo "démarrer le moteur";
	}
	public function tournerBabord() {
		echo "manoeuvrer la barre ou le volant pour aller à babord";
	}
	public function tournerTribord() {
		echo "manoeuvrer la barre ou le volant pour aller à tribord";
	}
	public function accelerer() {
		echo "augmenter la vitesse du moteur";
	}
	public function ralentir() {
		echo "dimininuer la vitesse du moteur ou le couper";
	}
	public function reculer() {
		echo "passer la marche arrière";
	}
}

Jojo a bien compris qu’il nous fallait des classes qui nous permettent de gérer les gens qui s’adonnent à la navigation de plaisance motorisée ou à voile et qu’un bateau ne se manœuvrait pas comme une voiture.

Attardons-nous sur la structure de ces classes : nous avons une classe abstraite Marin qui implémente une interface NavigateurInterface et dont les deux filles concrètes MarinVoile et MarinMoteur réalisent la majeure partie des fonctions.

Schéma UML du design pattern adaptateur

Marin est abstraite car trop générique, elle réalise cependant l’implémentation de jeterAncre, qui est commune à toute forme de navigation (bon, c’est vrai, les Optimist n’ont pas d’ancre). Jojo a choisi de créer une exception pour gérer le cas où la fonction démarrer n’est pas prise en charge, plutôt que de l’isoler dans une interface NavigateurMoteurInterface dédiée aux bateaux qui ont ce mode de propulsion. C’est un choix de conception, qui a le mérite de favoriser la généricité, inutile de faire une vérification de la présence de cette méthode en utilisant par exemple method_exists dans la classe client InspecteurPermisConduire. C’est la classe concrète qui gère l’absence de cette méthode en levant une exception correctement typée.

Comment ne pas se faire mener en bateau ?

Adeptes de longue date de la programmation orientée objet, nous souhaitons minimiser l’impact de ce changement d’architecture logicielle dans nos classes existantes. C’est là où l’adaptateur entre en jeu !

L’adaptateur fonctionne exactement sur le même principe que l’adaptateur de votre sèche-cheveux quand vous allez, en Irlande, par exemple ! Il prend en entrée la prise française de votre sèche-cheveux favori et l’adapte aux prises à trois broches que vous trouvez sur l’île Émeraude ! Vous n’avez pas eu à dénuder les fils de votre appareil pour y fixer une prise irlandaise, et heureusement : l’adaptateur fait le travail pour vous ! Et vous n’y avez vu que du bleu !

S’adapter…au changement

L’adaptateur a pour but de faire collaborer des interfaces de prime abord incompatibles, comme ConducteurInterface et NavigateurInterface. C’est un intermédiaire précieux (et réutilisable, ce qui nous met en joie !) qui va nous éviter de déstabiliser notre code base et qui va découpler le client (notre inspecteur du permis de conduire) de notre adapté (ici, nos marins). Plus le couplage est lâche, mieux c’est, je ne vous apprends rien !

L’adaptateur prend en composition l’adapté, et il sera chargé d’effectuer la translation entre les demandes à destination de l’interface originelle (ConducteurInterface) et la nouvelle interface cible (NavigateurInterface).

Pour que cette translation s’effectue de manière transparente pour le système, il faut naturellement que l’adaptateur implémente l’interface originelle. Regardez donc :

class AdaptateurMarin implements ConducteurInterface {
	private $_marin;
	
	public function __construct(Marin $marin) {
		$this->_marin = $marin;
	}
	public function demarrer() {
		$this->_marin->demarrer();
	}
	public function tournerGauche() {
		$this->_marin->tournerBabord();
	}
	public function tournerDroite() {
		$this->_marin->tournerTribord();
	}
	public function accelerer() {
		$this->_marin->accelerer();
	}
	public function ralentir() {
		$this->_marin->ralentir();
	}
	public function reculer() {
		$this->_marin->reculer();
	}
	public function immobiliser() {
		$this->_marin->jeterAncre();
	}
}

Le polymorphisme avant tout ! Notre adaptateur prend en charge des objets de la classe Marin, c’est à dire potentiellement toutes les sous-classes qui en dérivent. Tant mieux car nous en avons deux : MarinVoile et MarinMoteur !

Nous n’avons pas du tout modifié notre classe existante, nous avons isolé la nouveauté dans une nouvelle classe, autonome, avec des responsabilités clairement identifiées.

Les comportements que vous isolez dans des unités de code indépendantes sont réutilisables, ceux que vous enfermez dans des classes ne le sont pas forcément !

Pour que le système prenne en compte cette nouvelle possibilité (à nous l’argent facile !), il nous faudra juste modifier le code appelant. Notre client avait déjà un modificateur d’implémenté, à la bonne heure, servons nous en donc !

$adaptateur = new AdaptateurMarin(new MarinMoteur());
$inspecteur->changerCandidat($adaptateur);
$inspecteur->fairePasserExamen();

La méthode changerCandidat exige en paramètre une implémentation de la classe ConducteurInterface, ça tombe bien c’est précisément ce qu’est Marin et ses filles concrètes.

Souvenez vous toutefois qu’une exception est levée dans notre adaptateur MarinVoile, il faut donc en réalité modifier quelque peu le code appelant, qui va devoir les intercepter et les traiter le cas échéant. N’allons pas chercher midi à quatorze heures, nous nous contenterons d’en afficher le message. « KISS » is the law !

try {
	$inspecteur->fairePasserExamen();
} catch (UnsupportedMethodException $exception) {
	echo $exception->getMessage();
}

Voici maintenant l’architecture de notre code, avec l’adaptateur qui est venu s’intercaler entre l’existant et nos nouvelles fonctionnalités, qui vont nous permettre de gérer marins d’eau douce, flibustiers, jeunes loups de mer, moules à gaufres et autres ectoplasmes !

schema2

Pour lire cet article dans des conditions optimales, il est fortement conseillé d’écouter autant de fois que nécessaire ce titre (en plus d’aller les voir en concert en mai 2014) :