Accueil / Tutoriels / Builder

Builder

Publié le 23 Mai 2025
Conception

Builder est un pattern de création très utile lorsqu’on veut construire un objet complexe étape par étape. Il est particulièrement utile quand un objet comporte beaucoup de paramètres pour sa création, s’il existe plusieurs façon de construire un objet ou si l’on veut éviter des “constructeurs à rallonge” (qui empile pleins de paramètres).

Principe

L’idée est de séparer la construction d’un objet de sa représentation finale. Cela va permettre de créer différentes représentations d’un objet en utilisant le même processus de construction.

Ainsi, le code est plus lisible en évitant d’avoir une classe avec un constructeur comprenant 10 paramètres. Les détails de la construction de l’objet sont cachés dans le builder.

Exemples d’utilisations

Génération de formulaires HTML complexes de composants UI

Pour un CMS ou une interface dynamique, on a besoin de générer dynamiquement des formulaires à partir de métadonnées (champs requis, option, valeurs par défaut, etc…).

Nous pouvons utiliser un Builder qui va assembler étape par étape les différentes parties du formulaire en injectant des règles de validation, les types de champs, les labels, etc… On retrouve ce procédé dans des frameworks comme Symfony (FormBuilderInterface) ou Laravel pour du back-end, mais aussi React ou Angular côté Front-end.

Construction dynamique de requêtes SQL

Sur un site web avec un système de recherche avancée ou de filtres dynamiques, on veut permettre à l’utilisateur de filtrer par statut, mot-clé, plage de date/prix. Les filtres sont optionnels et les combinaisons possibles sont nombreuses.

Un Builder va permettre de construire dynamiquement la requête SQL en ajoutant les clauses une à une en fonction des filtres actifs. Des framework comme Symfony avec le QueryBuilder de Doctrine ou Laravel avec Eloquent utilisent ce principe.

Points d’attentions

Comme tout design pattern, le Builder a ses inconvénients et il faut savoir quand l’utiliser et quand l’éviter.

Si un objet possède deux ou trois propriétés et n’a pas d’option complexe, le Builder n’est pas utile. En effet, il va falloir créer une classe supplémentaire (notre Builder), rajouter des méthodes et gérer notre méthode build(). Un simple constructeur fera l’affaire.

Le builder peut rendre le code plus verbeux :


(new UserBuilder())
   ->withName("John")
   ->witheEmail("john@email.com")
   ->withAvatar("avatar.jpg")
   ->build();

Alors qu’un simple constructeur ou une Factory peut suffire :


new User("John", "john@email.com", "avatar.jpg");

Ce n’est pas forcément un problème, mais en cas d’abus on perd en lisibilité.

En utilisant un Builder, il faut à terme maintenir jusqu’à trois classes (la classe cible, la classe Builder et éventuellement une interface ou une Factory. En cas de modification de la classe cible, il faudra également modifier le Builder pour prendre en compte cette nouvelle propriété, en ajoutant une nouvelle méthode, et en modifiant la méthode pour notre build.

Notre objet peut comporter des champs obligatoires. Si le Builder ne valide pas ces champs dans sa méthode build(), on peut créer des objets invalides. Il faut donc penser à ajouter des vérifications dans la méthode build() et renvoyer des exceptions/erreurs si ces champs ne sont pas remplis, ce qui peut alourdir le code.

Implémenter un Builder

On cherche à obtenir un objet cible qui contient ses propres propriétés mais qui n’est pas en charge de sa construction.

Pour la création de notre objet cible, nous allons implémenter un Bulder qui contient les mêmes propriétés que la classe cible. Le Builder va fournir des méthodes de configuration (withXXX() et setXXX()) pour modifier ces propriétés et qui retournent l’objet builder à chaque fois (pattern fluent). Enfin, le Builder va exposer une méthode (souvent nommée build()) qui retourne l’objet complet.

Il est possible d’ajouter dans notre builder des règles de validation, des valeurs par défaut ou des traitements supplémentaires.

Voici un exemple en Go


package requestBuilder

import (
   "bytes"
   "errors"
   "io"
   "net/http"
   "net/url"
)

type HttpRequestBuilder struct {
   method      string
   baseURL     string
   headers     map[string]string
   queryParams map[string]string
   body        []byte
}

func NewHttpRequestBuilder() *HttpRequestBuilder {
   return &HttpRequestBuilder{
      headers:     make(map[string]string),
      queryParams: make(map[string]string),
   }
}

func (b *HttpRequestBuilder) SetMethod(method string) *HttpRequestBuilder {
   b.method = method
   return b
}

func (b *HttpRequestBuilder) SetURL(url string) *HttpRequestBuilder {
   b.baseURL = url
   return b
}

func (b *HttpRequestBuilder) AddHeader(key, value string) *HttpRequestBuilder {
   b.headers[key] = value
   return b
}

func (b *HttpRequestBuilder) AddQueryParam(key, value string) *HttpRequestBuilder {
   b.queryParams[key] = value
   return b
}

func (b *HttpRequestBuilder) SetBody(body []byte) *HttpRequestBuilder {
   b.body = body
   return b
}

func (b *HttpRequestBuilder) Build() (*http.Request, error) {
   if b.method == "" {
      return nil, errors.New("HTTP method is required")
   }
   if b.baseURL == "" {
      return nil, errors.New("URL is required")
   }

   // Construction de l'URL avec query params
   u, err := url.Parse(b.baseURL)
   if err != nil {
      return nil, err
   }
   q := u.Query()
   for k, v := range b.queryParams {
      q.Set(k, v)
   }
   u.RawQuery = q.Encode()

   // Corps de la requête
   var bodyReader io.Reader
   if b.body != nil {
      bodyReader = bytes.NewBuffer(b.body)
   }

   req, err := http.NewRequest(b.method, u.String(), bodyReader)
   if err != nil {
      return nil, err
   }

   // Ajout des headers
   for k, v := range b.headers {
      req.Header.Set(k, v)
   }

   return req, nil
}


package main

import (
   "fmt"
   "training.go/builder/requestBuilder"
)

func main() {
   builder := requestBuilder.NewHttpRequestBuilder().
      SetMethod("POST").
      SetURL("https://api.example.com/data").
      AddHeader("Authorization", "Bearer token").
      AddHeader("Content-Type", "application/json").
      AddQueryParam("limit", "10").
      SetBody([]byte(`{"name": "Toto"}`))

   req, err := builder.Build()
   if err != nil {
      panic(err)
   }

   fmt.Println("Request built successfully:")
   fmt.Println(req.Method, req.URL)
   fmt.Println("Headers:", req.Header)
}

Le même exemple en PHP


namespace Practice\designPatterns\builder;

class HttpRequestBuilder
{
   private string $method = 'GET';
   private string $url = '';
   private array $headers = [];
   private array $queryParams = [];
   private ?string $body = null;

   public function withMethod(string $method): self
   {
       $this->method = strtoupper($method);
       return $this;
   }

   public function withUrl(string $url): self
   {
       $this->url = $url;
       return $this;
   }

   public function addHeader(string $key, string $value): self
   {
       $this->headers[$key] = $value;
       return $this;
   }

   public function addQueryParam(string $key, string $value): self
   {
       $this->queryParams[$key] = $value;
       return $this;
   }

   public function withBody(string $body): self
   {
       $this->body = $body;
       return $this;
   }

   public function build(): array
   {
       if (empty($this->url)) {
           throw new \InvalidArgumentException("URL is required");
       }

       $query = http_build_query($this->queryParams);
       $fullUrl = $this->url . (!empty($query) ? '?' . $query : '');

       return [
           'method'  => $this->method,
           'url'     => $fullUrl,
           'headers' => $this->headers,
           'body'    => $this->body,
       ];
   }
}


declare(strict_types=1);

use Practice\designPatterns\builder\HttpRequestBuilder;

require "./vendor/autoload.php";

$builder = new HttpRequestBuilder()
   ->withMethod('POST')
   ->withUrl('https://api.example.com/data')
   ->addHeader('Authorization', 'Bearer token')
   ->addHeader('Content-Type', 'application/json')
   ->addQueryParam('limit', '10')
   ->withBody(json_encode(['name' => 'Toto']));

$request = $builder->build();

// Simulons l'envoi
echo "Méthode : " . $request['method'] . PHP_EOL;
echo "URL : " . $request['url'] . PHP_EOL;
echo "Headers : " . print_r($request['headers'], true);
echo "Body : " . $request['body'] . PHP_EOL;

Conclusion

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern Builder. Vous pouvez retrouver mon article sur les patrons de conception ou les autres tutoriels sur Prototype, Strategy, Visitor, Chain of responsibility et Facade.