Interroger un cluster MongoDB Atlas avec C++

J’ai décidé de me replonger dans le langage C++ le temps d’un exercice destiné aux étudiants suivant ma formation sur MongoDB en école d’ingénieur. Tout d’abord, nous avons effectué toutes les requêtes de l’exercice sur les salles (PDF) à partir de l’interpréteur JavaScript du shell MongoDB, en direction de nos clusters créés sur Atlas. Ensuite, nous avons traduit ces requêtes dans le langage de programmation préféré de chaque étudiant. N’ayant pas pratiqué le C++ depuis l’époque où la monnaie nationale était encore le franc, j’ai quant à moi pris le pari de revenir à mes premiers amours et d’utiliser le langage de Stroustrup comme base…ce fut un plaisir !

Tout d’abord, il m’a fallu installer le pilote (driver) dédié à ce langage : mongocxx.

Installation et compilation du pilote mongocxx

Rien de plus simple pour y parvenir :

    cd mongo-cxx-driver
    mkdir build && cd build
    cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local ..
    make -j$(nproc)
    sudo make install

Le programme principal

Une fois le driver installé, j’ai pu commencer à coder le petit programme pour accéder à mon cluster Atlas et y exécuter toutes les requêtes préparées dans mon header exercices.hpp . Le voici :

#include <iostream>
#include <bsoncxx/json.hpp>
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>
#include <mongocxx/exception/exception.hpp>
#include "exercices.hpp" 

using bsoncxx::to_json;

int main() {

    const std::string USERNAME      = "mongosensei";
    const std::string PASSWORD      = "xxxxx";
    const std::string DATABASE      = "xxxxx";
    const std::string COLLECTION    = "salles";
    const std::string ATLAS_CLUSTER = "xxx.mongodb.net";

    mongocxx::instance inst{};

    try {
        std::string uriString = "mongodb+srv://" + USERNAME + ":" + PASSWORD + "@" + ATLAS_CLUSTER + "/" + DATABASE;
        mongocxx::uri uri(uriString);
        mongocxx::client conn(uri);

        std::vector<bsoncxx::document::value> exercices = getExercices();

        for (const auto& exercice : exercices) {
            auto numExercice = exercice["num"].get_int32();
            auto filterDocument = exercice["filtre"].get_document();
            auto projectionDocument = exercice["options"]["projection"].get_document();
            
            auto options = mongocxx::options::find{};
            options.projection(projectionDocument.view());

            auto collection = conn[DATABASE][COLLECTION];
            auto cursor = collection.find(filterDocument.view(), options);

            int nbDocuments = 0;

            for (auto&& doc : cursor) {
                nbDocuments++;
                std::cout << bsoncxx::to_json(doc) << std::endl;
            }

            std::cout << "Nb de documents pour l'exercice " << numExercice << " : " << nbDocuments << std::endl;
        }

    } catch (const mongocxx::exception& e) {
        std::cerr << "Erreur de connexion : " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Ce programme fait appel à un fichier qui contient la solution à chacun des exercices contenus dans le PDF dont le lien a été donné plus haut. Voici ce à quoi il ressemble :

#ifndef EXERCICES_HPP
#define EXERCICES_HPP

#include <vector>
#include <bsoncxx/builder/basic/document.hpp>

time_t parseDate(const std::string& dateStr) {
    std::tm tm = {};
    strptime(dateStr.c_str(), "%Y-%m-%d", &tm);
    return mktime(&tm);
}

std::vector<bsoncxx::document::value> getExercices() {
    using bsoncxx::builder::basic::kvp;
    using bsoncxx::builder::basic::make_document;
    using bsoncxx::builder::basic::make_array;

    std::string date_str = "2021-11-15";
    auto timestamp = std::chrono::milliseconds{parseDate(date_str) * 1000};

    std::vector<bsoncxx::document::value> exercices = {
        make_document(
            kvp("num", 1),
            kvp("filtre", make_document(
                kvp("smac", true)
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 2),
            kvp("filtre", make_document(
                kvp("capacite", make_document(
                    kvp("$gt", bsoncxx::types::b_int64{1000})
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 3),
            kvp("filtre", make_document(
                kvp("adresse.numero", make_document(
                    kvp("$exists", false)
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 4),
            kvp("filtre", make_document(
                kvp("avis", make_document(
                    kvp("$size", bsoncxx::types::b_int64{1})
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 5),
            kvp("filtre", make_document(
                kvp("styles", "blues")
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("styles", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 6),
            kvp("filtre", make_document(
                kvp("styles.0", "blues")
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("styles", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 7),
            kvp("filtre", make_document(
                kvp("$and", make_array(
                    make_document(
                        kvp("adresse.codePostal", bsoncxx::types::b_regex{"^84"}),
                        kvp("capacite", make_document(
                            kvp("$lt", 500)
                        ))
                    )
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("adresse.ville", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 8),
            kvp("filtre", make_document(
                kvp("$or", make_array(
                    make_document(
                        kvp("_id", make_document(kvp("$mod", make_array(2, 0))))
                    ),
                    make_document(
                        kvp("avis", make_document(
                            kvp("$exists", false)
                        ))
                    )
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 9),
            kvp("filtre", make_document(
                kvp("avis", make_document(
                    kvp("$elemMatch", make_document(
                        kvp("note", make_document(
                            kvp("$gte", 8),
                            kvp("$lte", 10)
                        ))
                    ))
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 10),
            kvp("filtre", make_document(
                kvp("avis.date", make_document(
                        kvp("$gt", bsoncxx::types::b_date{timestamp}
                    ))
                ))
            ),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
    };

    return exercices;
}

#endif // EXERCICES_HPP

La fonction parseDate est là pour nous faciliter la transformation des dates pour leur utilisation avec mes méthodes de la lib BSON, elle ne sert ici qu’une fois.

Pour construire les documents, j’ai utilisé la version basic du builder BSON, la version stream étant beaucoup moins lisible pour moi.

Compilation et exécution du code

J’ai essayé plusieurs solutions :

  • la compilation avec c++
  • la compilation avec g++
  • l’installation de l’outil pkg-config, MongoDB proposant un fichier .pc

D’abord, la version avec pkg-config :

sudo apt install pkg-config
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
c++ -o mongo_exercices --std=c++11 mongo_exercices.cpp $(pkg-config --cflags --libs libmongocxx)
./mongo_exercices

Ensuite la version avec g++, qui marche aussi avec c++ :

g++ -o mongo_exercices mongo_exercices.cpp -lmongocxx -lbsoncxx -std=c++11 \
    -I/usr/local/include/mongocxx/v_noabi \
    -I/usr/local/include/bsoncxx/v_noabi \
    -I/usr/local/include/bsoncxx/v_noabi/bsoncxx/third_party/mnmlstc \
    -L/usr/local/lib -Wl,-rpath=/usr/local/lib \
    && ./mongo_exercices

Voici la sortie produite par notre exécutable :

{ "_id" : 1, "nom" : "AJMI Jazz Club" }
{ "_id" : 2, "nom" : "Paloma" }
Nb de documents pour l'exercice 1 : 2
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 2 : 1
{ "_id" : 3 }
{ "_id" : { "$oid" : "65e08cc6fa1cb047c0a5e5ea" } }
Nb de documents pour l'exercice 3 : 2
{ "_id" : 2, "nom" : "Paloma" }
Nb de documents pour l'exercice 4 : 1
{ "styles" : [ "jazz", "soul", "funk", "blues" ] }
{ "styles" : [ "blues", "rock" ] }
Nb de documents pour l'exercice 5 : 2
{ "styles" : [ "blues", "rock" ] }
Nb de documents pour l'exercice 6 : 1
{ "adresse" : { "ville" : "Avignon" } }
{ "adresse" : { "ville" : "Le Thor" } }
Nb de documents pour l'exercice 7 : 2
{ "_id" : 2 }
{ "_id" : 3 }
{ "_id" : { "$oid" : "65e08cc6fa1cb047c0a5e5ea" } }
Nb de documents pour l'exercice 8 : 3
{ "nom" : "AJMI Jazz Club" }
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 9 : 2
{ "nom" : "AJMI Jazz Club" }
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 10 : 2

Pour pousser plus loin votre expérience avec C++, suivez ce lien !

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

Aparté – Vol au dessus d’un service informatique

Voilà quelques années j’étais en poste dans une entreprise qui avait commandé un audit à un cabinet d’expert afin de procéder à une réorganisation. Le but était fort louable : améliorer les flux d’information entre les services pour que les projets informatiques soient livrés à temps, conformes aux spécifications et sans bugs. A cette occasion, j’avais rédigé un petit mémo à l’intention des auditeurs…Il est finalement resté sur un disque dur et je ne suis retombé dessus que tout récemment.

Vous reconnaîtrez certainement des mauvaises habitudes qui ont cours dans votre entreprise car ces maux là sont hélas génériques !

Chronique d’un échec annoncé

Tout d’abord j’évoquais dans ce document le passé du service…j’aurais pu tout aussi bien parler de PASSIF !

  • L’équipe précédente, composée de 2 à 3 personnes, avait développé à la hâte un site truffé de bugs, elle avait été intégralement remerciée par la direction et nous avions été recrutés pour colmater les brèches et éteindre les incendies (XSS, injections SQL, mots de passes en clair et j’en passe) qui se déclaraient sur le site au fur et à mesure que nous découvrions le code écrit (rien n’était documenté).
  • Les délais de développement étaient sous-estimés pour faire plaisir aux services concernés. On appelle ça « vendre du rêve ». Le rêve est vite déçu.

La conséquence de tout ça a été la totale absence de crédibilité des développeurs qui passaient grosso modo pour un ramassis de fumistes incompétents. Parfois un « Pourquoi ça prend si longtemps ? La précédente équipe le faisait en 10 secondes ! » fusait, signifiant que le chemin était encore long vers la compréhension du fonctionnement normal d’un service informatique. Il fallait alors répondre calmement « Certes, mais vous avez vu ce que ça a donné ! » pour obtenir un silence gêné et contrit.

Les origines de la démotivation ou comment saborder un service en quelques leçons

  • Le pair programming était la règle, les binômes changeaient toutes les deux semaines…trop court !
  • Embauchés pour faire du Symfony 2, les développeurs se retrouvaient à corriger des bugs en PHP pur ou des requêtes SQL à longueur de journée. « Bientôt vous passerez sur des nouveaux projets ! ». Mais oui…
  • « Il est urgent d’attendre » était la règle: « Attendez l’arrivée du nouveau DSI ! », « Attendez la réorg’ ! »…Le feu ravageait le front, les projets naviguaient à vue et il fallait attendre sans cesse.
  • Les ambitions mais surtout les compétences des développeurs étaient pour le moins variées…certains refusaient de se remettre en question techniquement et livraient du junk code par bennes entières, ces trashers hermétiques à l’évolution nuisaient grandement à la qualité du code produit et les bons en avaient marre de payer pour les dilettantes…
  • Certains développeurs ont tenté d’introduire le TDD, SCRUM, les tests unitaires et fonctionnels. On leur a gentiment fait comprendre que c’était sympa tout ça mais qu’on avait pas le temps pour le moment. Mais promis plus tard on s’y intéressera…

Chacun sa place

  • Pourquoi le marketing continue de venir voir en douce les développeurs alors que des chefs de projets ont été embauchés ?
  • Pourquoi les specs sont-elles encore dictées à la machine à café, sur un coin de table ou entre deux portes alors que des process ont été mis en place, souvent après de longues négociations avec toutes les parties concernées ? L’impatience, le syndrome du gamin frustré qui n’a pas ce qu’il veut dans la seconde…
  • Les 320 allers-retours entre le marketing et le service info démotive les développeurs, qui sont bien incapables de dire quelle tâche ils auront à effectuer le lendemain, ni même si le projet sera poursuivi ou abandonné (combien de dizaines de milliers d’euros ont été foutus à la poubelle parce que finalement « ce projet n’est plus prioritaire » ?)
  • Les demandeurs pensent QUANTITÉ, les développeurs aimeraient faire de la QUALITÉ ! Ils se sentiraient bien mieux et auraient davantage envie de s’impliquer !

Vous reprendrez bien du GIGO (Garbage In, Garbage Out) ?

Traduction française : On ne récolte que ce que l’on sème ! Des spécifications floues donnent du code bancal et donc difficile à maintenir et encore plus à faire évoluer. On ne perd pas son temps en rédigeant une demande claire, on perd 10 fois plus de temps quand il faut réusiner un plat de spaghetti que même Chuck Norris ne saurait démêler.

Quelques portes ouvertes à enfoncer :

  • Coder c’est cool, tester c’est encore mieux ! Il nous est arrivé de tester en Prod ! Pourquoi ?
  • Mal spécifier c’est faire perdre du temps et donc de l’argent à sa boîte ! C’est provoquer de nombreux aller-retours qui font passer chaque service pour un ramassis de clowns
  • Le changement est bien mieux encaissé par les développeurs quand il n’a pas lieu 10 fois par jour !
  • Un interlocuteur et pas 25 ! Pourquoi Tic vient 5 minutes après Tac dire tout le contraire de ce sur quoi nous nous sommes mis d’accord après d’âpres négociations ?

MongoDB – lookup et l’agrégation

Cette étape de recherche (lookup) permet d’effectuer l’équivalent d’une jointure externe dans un système de gestion de bases de données relationnelles (SGBDR) et ne peut opérer que sur des collections non shardées situées dans la même base de données.
Look up

Syntaxe de lookup

{
    $lookup:
    {
        from: < collection à joindre >,
        localField: < champ dans les documents de la collection de départ >,
        foreignField: < champ dans les documents de la collection à joindre>,
        as: < nom du tableau qui sera ajouté aux documents du jeu de résultat >
    }
}

Dans chacun des documents reçus en entrée, l’étape $lookup va rajouter un tableau contenant des données en provenance de la collection sur laquelle la jointure a été requise avant de passer ces documents à la prochaine étape du pipeline le cas échéant. Le terme de jointure n’est pas toujours très approprié notamment parce qu’il est possible d’effectuer des requêtes décorrélées, c’est à dire sans clause d’égalité.

Afin de voir $lookup en situation, nous allons utiliser trois collections qui stockeront des élèves, leurs devoirs individuels et les projets sur lesquels ils travaillent à plusieurs.

Elèves, au travail !

Les travaux individuels

Commençons par les travaux individuels de nos élèves, voici les collections impliquées, aucun index n’a été mis pour garder les choses les plus simples possibles mais évidemment qu’il faudra y songer !

db.eleves.insertMany([
    {"nom": "Sébastien Ferrandez", "code": "NAT123"},
    {"nom": "Evelyne Durand", "code": "NAT125"},
    {"nom": "Christian Ton", "code": "NAT120"},
    {"nom": "Claire Annela", "code": "NAT127"}
])

db.devoirs.insertMany([
    {"code": "NAT123", "matiere": "SVT", "note": 12},
    {"code": "NAT123", "matiere": "Maths", "note": 10},
    {"code": "NAT125", "matiere": "Maths", "note": 11.75},
    {"code": "NAT120", "matiere": "Français", "note": 18},
    {"code": "NAT127", "matiere": "Latin", "note": 19}
])

Nous allons partir d’eleves pour aller vers devoirs; dans les deux cas notre nom de champ de « jointure » est code:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   }
])

Voici le résultat produit par ce pipeline, il est assez difficilement lisible mais nous avons l’idée générale : les notes sont « raccrochées » à l’élève !

{
	"_id": ObjectId("5d6e6115d9a18feb0291b605"),
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b60e"),
		"code": "NAT123",
		"matiere": "SVT",
		"note": 12
	}, {
		"_id": ObjectId("5d6e6174d9a18feb0291b60f"),
		"code": "NAT123",
		"matiere": "Maths",
		"note": 10
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b606"),
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b610"),
		"code": "NAT125",
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b607"),
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b611"),
		"code": "NAT120",
		"matiere": "Français",
		"note": 18
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b608"),
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b612"),
		"code": "NAT127",
		"matiere": "Latin",
		"note": 19
	}]
}

Ajoutons une étape project pour rendre cet affichage plus « digeste »…Nous éliminons les id et les champs redondants:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   },
   {
     $project: {
         "_id": 0,
         "detail_notes._id": 0,
         "detail_notes.code": 0
     }
   }
])

Voilà qui est nettement mieux:

{
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"matiere": "SVT",
		"note": 12
	}, {
		"matiere": "Maths",
		"note": 10
	}]
} {
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"matiere": "Français",
		"note": 18
	}]
} {
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"matiere": "Latin",
		"note": 19
	}]
}

Les projets

Voici notre collection projets, elle contient les codes des élèves impliqués ainsi que la note obtenue:

db.projets.insertMany([
    {"codes": ["NAT123", "NAT125"], "matiere": "Dessin", "note": 15}
])

Le champ codes est un tableau ici, pour que chaque élève puisse récupérer sa note, il va falloir éclater ce tableau à l’aide d’unwind avant d’utiliser lookup dessus, nous allons partir de projets cette fois:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Voilà le jeu de résultat lié:

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}]
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}]
}

Nous allons rajouter une étape addFields pour que le tableau contenant un seul document (eleve) devienne un document tout simple, voici le pipeline final:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $addFields: {
       "eleve": { $arrayElemAt: ["$eleve", 0] }
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Et voilà l’travail !

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}
}

PHP – Le design pattern Etat

Le design pattern Etat est un design pattern comportemental; son utilisation est préconisée dès lors que le comportement d’un objet dépend directement de l’état dans lequel il se trouve à l’instant T.

Programmeur énervé par l'absence de pattern Etat

Programmeur se mettant dans un état pas possible à la lecture d’un code écrit voilà 10 ans

Architecture du design pattern Etat

Les participants à ce design pattern Etat sont au nombre de 3:

  • Une abstraction (interface ou classe abstraite) qui liste les différents comportements devant être implémentés par des états concrets
  • Des états concrets réalisant chacune des méthodes listées dans l’abstraction évoquée ci-dessus
  • Un contexte définissant l’interface que le code client va utiliser et ayant en composition les états concrets auquel il va simplement déléguer les requêtes lui parvenant

Where’s the bill, Bill?

Prenons un exemple parlant: le commerce en ligne ! Une commande faite en ligne traverse toute une série d’états; elle peut être créée, validée, payée ou mise en recouvrement, annulée, remboursée, expédiée etc. Evidemment certaines transitions d’état ne peuvent pas se faire: quand elle est expédiée, elle ne peut pas être annulée et inversement.

Notre site de vente en ligne d’ouvrages informatique CODEBOOKS reçoit plusieurs centaines de commandes par jour.
Une commande est créée dans l’état « En Attente », ce qui signifie qu’elle est en attente d’une validation, manuelle ou automatique. Cette validation peut intervenir ou pas, si le moyen de paiement s’avère frauduleux ou s’il ne parvient pas dans les délais précisés dans les conditions générales de vente.

Une fois qu’elle est validée, elle est préparée puis expédiée afin d’être livrée. C’est notre scénario nominal, le Best Case Scenario.
D’autres scenarii peuvent se produire: l’acheteur réalise à la livraison qu’il a déjà le livre ou bien qu’il l’a commandé dans une langue qu’il ne sait pas lire; il devra être remboursé après renvoi du livre à CODEBOOKS. Une erreur de stock peut également conduire à valider une commande portant sur un livre non disponible, elle devra donc être annulée puis remboursée.

Tes états d’âme, Eric…(air bien connu)

Nos états

Chacun de nos états fera l’objet d’une classe concrète dérivant une classe abstraite (ou implémentant une interface, au choix). J’ai choisi une classe abstraite dans laquelle je factorise mon constructeur, pour éviter de le répéter bêtement dans chaque classe fille car il fait dans tous les cas la même chose. Cette classe abstraite implémente une interface qui va contraindre chacune des classes filles à implémenter l’ensemble des méthodes qu’un état pourra mettre à disposition. Notez bien que le contexte est en composition de chacun des états concrets, pour garder la trace de l’état courant d’une commande.

interface EtatInterface
{
    public function mettreEnAttente(): void;
    public function valider(): void;
    public function annuler(): void;
    public function rembourser(): void;
    public function expedier(): void;
    public function signalerLivre(): void;
}

abstract class EtatAbstract implements EtatInterface
{
    protected $contexte;
    
    public function __construct(ContexteInterface $contexte)
    {
        $this->contexte = $contexte;
    }
}

Après étude, nous avons déterminé les états possibles d’une commande:

  • En Attente: c’est l’état par défaut, celui dans lequel se trouve toute commande « atterrissant » sur notre système d’information
  • Annulé
  • Validé
  • Expédié
  • Remboursé
  • Livré

Vous allez me dire « Mais avec cette interface, l’état Annulé va se retrouver à devoir implémenter une méthode expedier alors qu’il ne doit pas pouvoir transiter vers cet état ! » et vous aurez tout à fait raison ! C’est comme ça, il nous faut une interface qui liste toutes les méthodes qu’on peut invoquer sur l’ensemble des états pour faire plaisir à ce bon vieux Liskov !

Dès qu’une méthode sera appelée sur un état qui ne doit pas l’implémenter, nous lèverons une exception maison nommée MethodNotImplementedException:

class MethodNotImplementedException extends \Exception {}

Le contexte

Notre contexte va être appelé par le code client, c’est lui qui va garder une trace de l’état courant de notre commande. Il a en composition une instance de chacun des différents types d’états disponibles dans lesquels il va s’auto-injecter et à sa construction, il mettra l’état courant à sa valeur par défaut, à savoir « en attente ». Il propose une série de getters (sauf pour enAttente car nous postulons qu’aucun état ne peut effectuer de transition vers cet état initial) ainsi que les méthodes que le code client appellera. Vous notez que ces méthodes font de la délégation bête et méchante. L’unique setter est crucial pour tenir le contexte à jour des changements d’état intervenus dans l’application.

interface ContexteInterface
{
    public function etatValide(): EtatInterface;
    public function etatAnnule(): EtatInterface;
    public function etatExpedie(): EtatInterface;
    public function etatLivre(): EtatInterface;
    public function etatActuel(): EtatInterface;
    public function changerEtat(EtatInterface $etat): void;
}

class Contexte implements ContexteInterface
{
    private $enAttente;
    private $valide;
    private $expedie;
    private $livre;
    private $rembourse;
    private $annule;
    private $etatActuel;

    public function __construct()
    {
        $this->enAttente = new EnAttente($this);
        $this->valide = new Valide($this);
        $this->expedie = new Expedie($this);
        $this->livre = new Livre($this);
        $this->rembourse = new Rembourse($this);
        $this->annule = new Annule($this);
        
        $this->etatActuel = $this->enAttente;
    }
    
    public function etatValide(): EtatInterface
    {
        return $this->valide;
    }
    
    public function etatAnnule(): EtatInterface
    {
        return $this->annule;
    }
    
    public function etatExpedie(): EtatInterface
    {
        return $this->expedie;
    }
    
    public function etatLivre(): EtatInterface
    {
        return $this->livre;
    }
    
    public function etatRembourse(): EtatInterface
    {
        return $this->rembourse;
    }
    
    public function etatActuel(): EtatInterface
    {
        return $this->etatActuel;
    }
    
    public function changerEtat(EtatInterface $etat): void
    {
        $this->etatActuel = $etat;
    }
    
    public function valider(): void
    {
        $this->etatActuel->valider();
    }
    
    public function expedier(): void
    {
        $this->etatActuel->expedier();
    }
    
    public function signalerCommeLivre(): void
    {
        $this->etatActuel->signalerLivre();
    }
    
    public function effectuerRemboursement(): void
    {
        $this->etatActuel->rembourser();
    }
    
    public function effectuerAnnulation(): void
    {
        $this->etatActuel->annuler();
    }
}

Nos états…concrets !

EnAttente

Prenons notre première classe concrète: EnAttente. Cet état peut effectuer des transitions vers les états Validé (si le paiement est valide) ou Annulé (dans le cas contraire). Il ne peut pas le faire vers lui-même ainsi que vers les états Remboursé, Expédié et Livré car à ce stade le paiement de la commande n’est pas une certitude.

class EnAttente extends EtatAbstract
{   
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Déjà en attente');
    }
    
    public function valider(): void
    {
        $this->contexte->changerEtat($this->contexte->etatValide());
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
}

Annulé

Comme tous les états, Annule ne peut aller vers lui-même et il n’effectue de transition qu’en direction de Remboursé.

class Annule extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Déjà annulé');
    }
    
    public function rembourser(): void
    {
        $this->contexte->changerEtat($this->contexte->etatRembourse());
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
}

Validé

Valide peut évoluer en Annulé (opposition bancaire ou autres) ou en Expédié, si tout se déroule normalement.

class Valide extends EtatAbstract
{    
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Déjà validé');
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
    
    public function expedier(): void
    {
        $this->contexte->changerEtat($this->contexte->etatExpedie());
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
}

Expedié

Cet état peut effectuer des transitions uniquement vers Livré.

class Expedie extends EtatAbstract
{   
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Déjà expédié');
    }
    
    public function signalerLivre(): void
    {
        $this->contexte->changerEtat($this->contexte->etatLivre());
    }
}

Remboursé

C’est un état terminal.

class Rembourse extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Déjà Remboursé');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
}

Livré

Cet état ne peut transiter que vers Annulé, quand le client reçoit un produit qui ne lui convient pas. L’annulation donnera ensuite lieu à un remboursement.

class Livre extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Déjà Livré');
    }
}

Le code client

Nous avons à notre disposition une classe Commande; elle possède une référence vers le contexte, qui va ainsi garder trace des différents états qu’elle traverse. C’est elle que notre code client va appeler.

class Commande
{
    private $contexte;
    
    public function __construct(ContexteInterface $contexte)
    {
        $this->contexte = $contexte;
    }
    
    public function valider(): void
    {
        $this->contexte->valider();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }

    public function expedier(): void
    {
        $this->contexte->expedier();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
    public function signalerCommeEtantLivre(): void
    {
        $this->contexte->signalerCommeLivre();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
    public function procederAuRemboursement(): void
    {
        $this->contexte->effectuerRemboursement();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
 
    public function effectuerUneAnnulation(): void
    {
        $this->contexte->effectuerAnnulation();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
}

$contexte = new Contexte();
$commande = new Commande($contexte);

Voici notre scénario nominal, ou best case scenario (fingaz crossed!) :

$commande->valider();
$commande->expedier();
$commande->signalerCommeEtantLivre();

La commande va traverser 4 états: elle va passer de « en attente » à « validée », puis à « expédiée » et enfin à « livrée ».

Voici le scénario où la commande est validée mais une erreur de stock conduit à devoir l’annuler et la rembourser:

$commande->valider();
$commande->effectuerUneAnnulation();
$commande->procederAuRemboursement(); // état TERMINAL

Dans le scénario suivant, le produit est livré mais l’acheteur réalise qu’il l’a déjà ou bien qu’il s’est trompé lors de sa commande et le produit de remplacement n’est plus disponible dans le stock:

$commande->valider();
$commande->expedier();
$commande->signalerCommeEtantLivre();
$commande->effectuerUneAnnulation();
$commande->procederAuRemboursement(); // état TERMINAL

Enfin, dans notre dernier scénario, le paiement n’est pas arrivé à temps (mandat, virement bancaire, chèque…) et il faut annuler la commande ! C’est le plus simple:

$commande->effectuerUneAnnulation();

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