Le Design Pattern Monteur (Builder) en PHP

Le monteur (builder) est un modèle de conception souvent mal compris, confondu avec d’autres patterns de type construction.

L’objectif du monteur est de séparer le processus de construction de l’objet de sa représentation finale. En d’autres termes, cela signifie que le processus de construction est identique mais que le produit finit peut varier.

Un cas d’usage simple

Supposez que vous devez, à partir d’un logiciel de comptabilité, extraire vos dépenses mensuelles. Vous souhaitez pouvoir obtenir un rapport de dépenses sous la forme d’un tableau et aussi sous la forme d’un graphe.

Le processus de création sera probablement identique lorsqu’il va s’agir de construire le rapport (extraire les données de la base, ajouter des dépenses au rapport), mais le produit final ne sera pas du tout le même (une image contre un fichier au format HTML).

Le mauvais réflexe

Le mauvais réflexe consiste à développer plusieurs méthodes « en dur » qui exploiteront les données : Une méthode qui va générer du HTML, l’autre méthode qui va générer une image. Ces deux méthodes exploiteront elles même les données et dupliqueront les efforts.

Par exemple pour la version tabeau HTML:

$htmlOutput = '<table>' ;
foreach ($datas as $data){
    $htmlOutput .= "<tr><td>".$data['Type']."</td><td>".$data['value']."</td></tr>" ;
}
$htmlOutput.="</table>"

Et pour la version en graphe

$graph = new Graph();
foreach ($datas as $data){
   $graph->addSlice($data['Type'], $data['value']);
}

Cette solution présente plusieurs inconvénients :

  • Le code en charge d’exploiter les données est dupliqué (ici le parcours du tableau)
  • Le code en charge de convertir les données (transformation HTML / Image) et celui en charge de les lire (parcours et lecture des lignes) sont mélangés
  • Le code permettant de générer un rapport dans un format particulier n’est pas isolé, donc peu réutilisable à partir d’autres données

La solution du monteur / builder

Nous avons dit que le monteur permettait d’isoler le processus de création de la représentation finale du produit crée.

Commençons par identifier ce dont est constitué un rapport

  • Un titre
  • Une légende
  • Des dépenses (libellé / valeur)

Nous pouvons ainsi isoler les étapes de création d’un rapport et définir nos monteurs :

<?php
interface IReportBuilder
{
    public function addTitle($pTitle);
    public function addLegend($pLegend);
    public function addExpense($pType, $pAmount);
}

Maintenant que nous disposons de cette interface de création, nous pouvons mettre à jour notre méthode de génération de rapport (appelée directeur) afin qu’elle utilise l’inferface du monteur.

class ReportDirector
{
    public function createExpenseReport(IReportBuilder $pReportBuilder)
    {
        $data = $this->getExpenseData();
        $pReportBuilder->addTitle($data['meta']['title']);
        $pReportBuilder->addLegend($data['meta']['legend']);

        foreach ($data['datas'] as $line){
            $pReportBuilder->addExpense($line['type'], $line['amount']);
        }
        return $pReportBuilder;
    }
//... code de récupération des données de dépense
}

Le constat est simple : le code qui génère le rapport se moque de savoir s’il doit générer du HTML, une image ou un fichier CSV, ce qui l’importe est d’assembler le produit fini en le définissant pièce par pièce.

Développons maintenant nos monteurs

Le monteur de type HTML

Le monteur de type HTML va s’assurer de la bonne prise en charge des caractères spéciaux et générer un tableau HTML.

<?php
class HTMLReportBuilder implements IReportBuilder
{
    //... protected attributes
    public function addTitle($pTitle)
    {
        $this->_title = htmlentities($pTitle);
    }

    public function addLegend($pLegend)
    {
        $this->_legend = htmlentities($pLegend);
    }

    public function addExpense($pType, $pAmount)
    {
        $this->_expenses .= '<tr><td>'.
                      htmlentities($pType).'</td><td>'.
                      $pAmount.'</td></tr>';
    }

    public function getReport()
    {
       return '<h2>'.$this->_title.'</h2>'
               .'<p>'.$this->_legend.'</p>'
               .'<table>'
               .'<tr><th>Type de dépense</th><th>Montant</th>'
               .$this->_expenses
               .'</table>';
    }
}

Le monteur de type Image

Le monteur de type Image va générer une image aux dimensions paramétrables avec des sections à la taille proportionnelle au montant de dépense.

<?php
class BarReportBuilder implements IReportBuilder
{
   //... protected attributes
   public function __construct ($pWidth, $pHeight, $pFilePath)
    {
        $this->_width = $pWidth;
        $this->_height = $pHeight;
        $this->_filePath = $pFilePath;
    }

    public function addTitle($pTitle)
    {
        $this->_title = $pTitle;
    }

    public function addLegend($pLegend)
    {
        $this->_legend = $pLegend;
    }

    public function addExpense($pType, $pAmount)
    {
        $this->_expenses[$pType] = $pAmount;
        $this->_max += $pAmount;
    }

    public function getReport()
    {
        //creation de l'image
        $image = imagecreate($this->_width, $this->_height);

        //black filler
        $black = imagecolorallocate($image, 255, 255, 255);
        imagefilledrectangle($image, 0, 0 , $this->_width, $this->_height, $black);

        //now we're gonna fill the datas
        $indice = 0;
        $xPosition = self::BORDER_WIDTH;
        foreach ($this->_expenses as $type=>$expense) {
            $color = $this->_getColor($indice, $image);
            imagefilledrectangle($image,
                                 $xPosition,
                                 self::BORDER_WIDTH,
                                 $xPosition+($movedBy = ($expense/$this->_max) * ($this->_width-self::BORDER_WIDTH*2)),
                                 $this->_height-self::BORDER_WIDTH,
                                 $color
                                );
            $xPosition += $movedBy;
            $indice++;
        }
        imagegif($image, $this->_filePath);
    }
}

Pour aller plus loin

En allant plus loin, nous pourrions créer des Monteurs capables de retourner un simple entier qui représente la somme totale des dépenses mensuelles (TotalReportBuilder), ou un monteur capable de générer un rapport destiné à l’affichage en mode console ou mail…

Liens

Le code complet de l’exemple de builder de cet article sur github.

  1. Très bien tout ça !
    Jamais utilisé cette façon de faire, alors qu’il m’est arrivé je ne sais combien de fois d’avoir à faire un tableau HTML de données et son export CSV en utilisant pratiquement le même code.
    C’est noté !

  2. Bonne explication.
    Je l’utilise régulièrement, et dernièrement pour permettre la génération de donnée au format JSON/XML/ ou autres.

  3. je prend une claque avec les designs patterns je suis content de lire de bon articles sur ça.
    Merci pour ces articles.

  4. N’empeche que la bad practice fait moins de 10 lignes. J’ai la flemme de compter mais le pattern doit pas etre loin des 100.

    • Le mauvais réflexe de l’illustration ne fonctionne pas (s’appuie sur une bibliothèque existante fictive).

      L’exemple avec le monteur est plus conséquent aussi car il fait plus de choses.

      Enfin, les lignes dépensées dans le pattern sont réutilisables alors que les 10 lignes du mauvais réflexe ne le sont pas.

      Après, encore une fois, il faut transposer les exemples dans des cas concrets.

Laisser un commentaire


NOTE - Vous pouvez utiliser les éléments et attributs HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>