Le problème avec le pattern Builder

Anthony TENNERIELLO
norsys-octogone
Published in
16 min readMay 17, 2024

Il y a de fortes chances pour que le design pattern Builder, le modèle de conception Monteur en français, vous soit familier, voire même que vous l’utilisiez déjà dans vos projets. C’est mon cas et, à force de travailler avec, j’ai commencé à m’interroger sur la manière dont je le vois implémenté, voire sur l’intérêt de le mettre en place.

Nous allons revenir rapidement sur la définition de ce modèle de conception, puis je vais essayer de vous exprimer ce qui me dérange dans son implémentation, avant de tenter d’apporter des pistes de réflexion à la résolution de ce problème. Le tout grâce à un exemple simple que je vous propose en PHP (version 8.3).

Vous pouvez bien entendu suivre cet article, même si vous ne connaissez pas PHP, car les design patterns ne sont pas propres à un langage.

Un design pattern ?

Tout le monde n’est pas à l’aise avec ce concept. Les design patterns existent car nous rencontrons régulièrement les mêmes problématiques. En conséquence, ça serait un énorme gaspillage de temps et d’énergie que de devoir trouver une solution s’adaptant à notre contexte à chaque fois que nous rencontrons le même problème. C’est justement pour éviter ça que les modèles de conception ont été créés.

En me référant à l’ouvrage Design Patterns (1999) rédigé par le “Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides) et traduit en français par Jean-Marie Lasvergères, le pattern Builder a été conçu dans le but de « dissocie[r] la construction d’un objet complexe de sa représentation, de sorte que le même processus de construction permette des représentations différentes ». Dès que nous avons affaire à un objet complexe, c’est-à-dire un objet comprenant un grand nombre de propriétés, offrant ainsi la possibilité de constituer cet objet d’une multitude de manières différentes, le pattern Builder est un bon outil pour nous simplifier la tâche. C’est d’autant plus vrai si l’objet en question possède des propriétés facultatives.

Dans ce contexte, notre premier réflexe serait d’avoir un constructeur que nous modifions dès que nous avons besoin d’une nouvelle propriété. Cependant, lorsqu’il commence à en avoir une dizaine dans le constructeur, le code devient illisible et compliqué à maintenir. En effet, dès que nous souhaitons ajouter une propriété à notre objet, il nous faut modifier chaque appel à son constructeur.

Ça ne donne pas envie d’ajouter des propriétés à nos objets 😅

La pattern Builder permet de déléguer la construction de l’objet à un autre objet. Grâce aux méthodes définies dans ce second objet (le builder), nous pouvons construire pas à pas notre objet (appelé produit), tout en rendant ça flexible et maintenable.

En quoi ça rend l’évolution de notre objet plus flexible ? 🤔

Lorsque nous souhaitons ajouter une nouvelle propriété à notre objet, nous n’avons plus besoin de modifier son constructeur, uniquement d’ajouter une méthode à son builder. Nous adaptons ensuite uniquement les appels au builder qui ont besoin de cette propriété.

Avec quelques exemples, ça sera tout de suite plus clair.

L’exemple du Query Builder

$qb = $this->createQueryBuilder('p')
->where('p.price > :price')
->setParameter('price', $price)
->orderBy('p.price', 'ASC');

if (!$includeUnavailableProducts) {
$qb->andWhere('p.available = TRUE');
}

$query = $qb->getQuery();

return $query->execute();

Vous avez sûrement reconnu le query builder de Doctrine. C’est un excellent exemple pour montrer l’intérêt de ce pattern.

Nous commençons par créer une nouvelle requête. Puis, nous lui ajoutons des instructions, des paramètres et récupérons l’objet Query, que nous pouvons ensuite exécuter. Nous construisons ainsi une requête sur mesure permettant de répondre à un besoin précis.

Construire une requête de cette manière est bien plus lisible et surtout flexible ! Si de nouvelles méthodes sont ajoutées à cet objet, notre code fonctionnera toujours sans action supplémentaire de notre part.

Dernier avantage, nous n’avons pas besoin de nous soucier des paramètres de l’objet Query qui ne nous intéressent pas. Nous utilisons uniquement ce dont nous avons besoin.

Mangeons une pizza 🍕

La meilleure façon de comprendre ce pattern est encore de le mettre en place nous-même. Pour cela, je vais prendre le célèbre exemple de la préparation d’une pizza, que j’ai adapté à partir d’autres articles traitant également de ce sujet.

Tout d’abord, créons notre objet Pizza avec pour propriétés les ingrédients de ladite pizza.

class Pizza
{
private string $base = '';
private string $sauce = '';
private string $cheese = '';

/** @var string[] $sideDishes */
private array $sideDishes = [];

public function getBase(): string
{
return $this->base;
}

public function setBase(string $base): void
{
$this->base = $base;
}

public function getSauce(): string
{
return $this->sauce;
}

public function setSauce(string $sauce): void
{
$this->sauce = $sauce;
}

public function getCheese(): string
{
return $this->cheese;
}

public function setCheese(string $cheese): void
{
$this->cheese = $cheese;
}

/**
* @return string[]
*/
public function getSideDishes(): array
{
return $this->sideDishes;
}

/**
* @param string[] $sideDishes
*/
public function setSideDishes(array $sideDishes): void
{
$this->sideDishes = $sideDishes;
}
}

Vous remarquerez qu’il n’y a pas de constructeur. C’est tout l’intérêt ! Nous commençons par créer un objet vide, puis nous complétons petit à petit les valeurs de ses propriétés selon nos besoins, d’où l’utilisation de setters.

Maintenant que nous avons notre produit, nous allons pouvoir passer au builder. Le cœur de ce pattern consiste à utiliser une interface ou une classe abstraite pour le builder. Dans notre contexte, chaque recette étendra la classe abstraite PizzaBuilder, nous permettant d’en ajouter une simplement en créant une nouvelle classe. Donc, sans avoir besoin de modifier le code existant. Oui, c’est lOpen/Closed Principle !

Nous allons d’abord créer la classe abstraite PizzaBuilder, puis nous allons préparer deux recettes : la Margherita et la Pepperoni. Si vous souhaitez déguster une autre pizza, il vous sera facile de l’implémenter 😉

abstract class PizzaBuilder
{
protected Pizza $pizza;

public function createPizza(): static
{
$this->pizza = new Pizza();

return $this;
}

public function getPizza(): Pizza
{
return $this->pizza;
}

abstract public function buildBase(): static;
abstract public function buildSauce(): static;
abstract public function buildCheese(): static;
abstract public function buildSideDishes(): static;
}

Comme vous pouvez le constater, le builder crée un objet vide, puis déclare les méthodes que ses classes enfants devront implémenter, afin de construire l’objet concret.

class MargheritaPizzaBuilder extends PizzaBuilder
{
public function buildBase(): static
{
$this->pizza->setBase('fine');

return $this;
}

public function buildSauce(): static
{
$this->pizza->setSauce('tomate');

return $this;
}

public function buildCheese(): static
{
$this->pizza->setCheese('mozzarella');

return $this;
}

public function buildSideDishes(): static
{
$this->pizza->setSideDishes([
'basilic',
]);

return $this;
}
}
class PepperoniPizzaBuilder extends PizzaBuilder
{
public function buildBase(): static
{
$this->pizza->setBase('fine');

return $this;
}

public function buildSauce(): static
{
$this->pizza->setSauce('tomate');

return $this;
}

public function buildCheese(): static
{
$this->pizza->setCheese('mozzarella');

return $this;
}

public function buildSideDishes(): static
{
$this->pizza->setSideDishes([
'pepperoni',
'olives',
'champignons',
'poivrons',
]);

return $this;
}
}

Et voilà ! Nous pouvons dès à présent préparer une délicieuse pizza.

Et si nous souhaitons ajouter un nouvel ingrédient à la pizza ?

Nous allons commencer par ajouter la propriété et le setter correspondant à la classe Pizza, puis une nouvelle méthode abstraite à implémenter dans notre PizzaBuilder. Nous n’avons nullement besoin de modifier le code existant, uniquement d’en ajouter.

One more thing

Vous pouvez facilement cuisiner toutes les pizzas que vous voulez, mais vous vous rendrez vite compte que vous allez répéter du code. Il existe en effet un autre composant optionnel que vous pouvez utiliser pour rendre ça plus fluide : le directeur.

Le directeur utilise le builder pour construire un objet en suivant un ordonnancement précis.

readonly class Cook
{
public function __construct(
private PizzaBuilder $pizzaBuilder,
) {
}

public function cookPizza(): void
{
$this->pizzaBuilder->createPizza()
->buildBase()
->buildSauce()
->buildCheese()
->buildSideDishes();
}

public function getPizza(): Pizza
{
return $this->pizzaBuilder->getPizza();
}
}

Dorénavant, chaque pizza que vous allez préparer suivra exactement le même déroulement. Voyons voir ce que ça donne.

$margheritaCook = new Cook(new MargheritaPizzaBuilder());
$margheritaCook->cookPizza();
$margherita = $margheritaCook->getPizza();

Grâce à notre classe abstraite PizzaBuilder, vous pouvez fournir n’importe quelle instance de classe qui l’étend à notre cuisinier pour préparer la pizza de votre choix.

Pour mieux comprendre le rôle du directeur, un parallèle avec la musique serait plus approprié.

Il faut voir le directeur comme un maestro qui orchestre des musiciens (le builder où chaque musicien est une méthode), jouant une symphonie (le produit).

Un pattern SOLID

Puisque nous avons isolé la création de l’objet dans une classe dédiée, nous respectons le principe de responsabilité unique.

De plus, grâce à l’utilisation de la classe abstraite PizzaBuilder, nous respectons également l’Open/Closed Principle, comme évoqué précédemment.

Cette même classe se conforme aussi au principe de substitution de Liskov, grâce au directeur. Il nous suffit de lui fournir une autre instance du builder pour qu’il nous prépare une pizza différente.

Plusieurs principes SOLID s’appliquent donc à ce pattern, qui a pour vocation à être un template général qui peut s’adapter à une majorité de cas concret.

Est-il réellement SOLID ?

Tout est parfait dans le meilleur des mondes alors ?

En se basant sur cette implémentation et étant donné que la logique de création de ma pizza est gérée par la classe PizzaBuilder, rien ne peut m’empêcher de faire ça :

$pizza = new Pizza();
$pizza->setBase('tarte au citron');

À partir de là, dans n’importe quelle méthode qui attend une instance de Pizza, je ne peux qu’espérer qu’elle a été créée à partir de l’un de nos builders. D’une certaine manière, je rends le produit dépendant du builder, car je ne peux pas garantir que ma pizza possède bien tous les ingrédients indispensables si elle n’a pas été créée à partir d’un builder.

De mon point de vue, le problème avec cette implémentation du pattern est que je ne peux jamais être certain que l’objet que je reçois est intègre. De ce fait, si je veux éviter d’avoir une erreur, je dois vérifier la validité de mon objet à chaque fois que je souhaite l’utiliser. C’est extrêmement lourd et dommage de résoudre un problème tout en en créant un autre.

Améliorations

Nous allons essayer de revoir notre copie. Nos améliorations vont d’abord se porter sur l’objet Pizza, puisque l’objectif que je nous fixe est de le rendre plus robuste afin qu’il ne soit plus possible de créer un objet non valide.

Utilisons des types plus stricts

Premièrement, le type string dans ce contexte n’est pas assez restrictif, comme nous l’avons bien vu. C’est trop facile de fournir une mauvaise valeur à l’objet.

Nous allons commencer par remplacer le type string par des énumérations.

readonly class Pizza
{
public Base $base;
public Sauce $sauce;
public Cheese $cheese;

/** @var SideDish[] $sideDishes */
public array $sideDishes;

public function setBase(Base $base): void
{
$this->base = $base;
}

public function setSauce(Sauce $sauce): void
{
$this->sauce = $sauce;
}

public function setCheese(Cheese $cheese): void
{
$this->cheese = $cheese;
}

/**
* @param SideDish[] $sideDishes
*/
public function setSideDishes(array $sideDishes): void
{
$this->sideDishes = $sideDishes;
}
}

Les énumérations nous assurent que les valeurs des propriétés ne peuvent être que parmi celles que nous avons définies.

Je ne vais pas détailler chaque énumération, mais vous trouverez ci-dessous l’exemple de la base.

enum Base: string
{
case Thin = 'fine';
}

Le type string peut être vulgarisé comme un type mixed. En effet, il peut contenir une valeur correspondant à n’importe lequel des autres types, comme le montre l’exemple ci-dessous.

'test'                      // string
'7' // int
'19.4' // float
'true' // boolean
'[\'value1\', \'value2\']' // array
'{"field":"value"}' // même du json

Lorsqu’il est possible de substituer le type string par un type plus précis, il est préférable de le faire. Nous avons de la chance, c’est notre cas.

En revanche, malgré les énumérations PHP, nous souffrons tout de même d’une limitation. La propriété sideDishes est toujours de type array. Nous utilisons la PHPDoc pour informer les développeurs que le setter attend un tableau d’énumérations SideDish. Or, la PHPDoc n’est pas prise en compte lors de l’exécution. Donc rien ne bloque vraiment le fait de fournir un tableau de Sauce par exemple. Les génériques permettraient de résoudre ce problème. Hélas, PHP ne les gère pas.

Je n’ai pas de solution élégante à ce problème. Il pourrait être possible d’ajouter une vérification de type dans le setter. Ça ressemblerait à l’extrait de code suivant.

/**
* @param SideDish[] $sideDishes
*/
public function setSideDishes(array $sideDishes): void
{
foreach ($sideDishes as $sideDish) {
if (!$sideDish instanceof SideDish) {
throw new \LogicException('Invalid value for side dish.');
}
}

$this->sideDishes = $sideDishes;
}

Cette solution fonctionnerait, mais c’est dommage de devoir parcourir tout le tableau pour vérifier le type de ses éléments. (Heureusement qu’une pizza n’aura jamais besoin de 10 000 garnitures).

Retirons les valeurs par défaut

Vous l’avez sûrement remarqué, les valeurs par défaut ont été retirées des propriétés. Celles-ci étant requises, elles doivent être initialisées. Si un développeur crée une instance de Pizza sans initialiser les propriétés via les setters, il verra une belle erreur.

$pizza = new Pizza();
$pizza->base;

// PHP Fatal error: Uncaught Error: Typed property Pizza::$base must not
// be accessed before initialization

Ça m’a également permis de rendre ma classe readonly, ainsi que de modifier la visibilité des propriétés à public et, par conséquent, pouvoir enlever les getters. Ce qui signifie qu’une fois que j’ai défini des valeurs à mes propriétés, elles ne peuvent plus être modifiées, rendant de fait mon objet immuable. Dans ce contexte c’est pertinent, puisqu’au moment où le cuisinier vous sert votre pizza vous n’allez pas changer la pâte, donc autant l’interdire.

Étant donné qu’il n’est pas possible de définir une valeur par défaut à une propriété readonly (hormis dans le constructeur), puisque ça en ferait une constante, je ne pouvais pas le faire dans la première version que je vous ai montrée.

Retirons les setters

Cependant, nous pouvons toujours créer un objet Pizza sans lui fournir d’ingrédient. Certes, ça provoque une erreur, mais nous pouvons faire mieux.

Nous allons retirer les setters et obliger les développeurs à fournir les ingrédients au constructeur, de sorte qu’il ne soit plus possible d’instancier notre produit à vide.

readonly class Pizza
{
public function __construct(
public Base $base,
public Sauce $sauce,
public Cheese $cheese,
/** @var SideDish[] $sideDishes */
public array $sideDishes,
) {
}
}

En bonus, notre classe a gagné en légèreté tout en étant bien plus robuste !

Inversons notre approche

Dans le premier exemple que je vous ai présenté, nous commencions par créer un objet Pizza vide, que nous alimentions petit à petit. Maintenant que nous avons ajouté un constructeur à notre classe et que celui-ci attend tous les ingrédients nécessaires à la préparation de notre pizza, ça n’est plus possible. À vrai dire, c’est mieux ainsi. Nous étions obligés d’ajouter cette souplesse à notre produit pour nous permettre de lui appliquer le pattern Builder et c’est justement ce qui autorisait à créer des pizzas immangeables. Nous allons donc modifier notre builder en conséquence.

Avant cela, je vous propose de prendre la bonne habitude de définir une interface, qui nous fournit les méthodes à implémenter par notre builder.

interface PizzaBuilder
{
public function baseIs(Base $base): static;
public function sauceIs(Sauce $sauce): static;
public function cheeseIs(Cheese $cheese): static;

/**
* @param SideDish[] $sideDishes
*/
public function sideDishesAre(array $sideDishes): static;
public function buildPizza(): Pizza;
}

Nous disposons toujours de méthodes nous permettant d’ajouter des ingrédients à notre pizza et qui retournent l’instance courante du builder, ce qui nous offre la possibilité de chaîner nos appels. À la différence que cette fois-ci, ces méthodes attendent les ingrédients en paramètre. Nous y reviendrons.

La méthode nous permettant d’initialiser une nouvelle pizza a bien entendu disparue et nous avons remplacé la méthode getPizza qui retournait l’instance courante de Pizza par buildPizza, qui se charge d’instancier et retourner une pizza.

Voyons maintenant l’implémentation concrète.

class ClassicPizzaBuilder implements PizzaBuilder
{
private Base $base;
private Sauce $sauce;
private Cheese $cheese;

/** @var SideDish[] $sideDishes */
private array $sideDishes;

public function baseIs(Base $base): static
{
$this->base = $base;

return $this;
}

public function sauceIs(Sauce $sauce): static
{
$this->sauce = $sauce;

return $this;
}

public function cheeseIs(Cheese $cheese): static
{
$this->cheese = $cheese;

return $this;
}

public function sideDishesAre(array $sideDishes): static
{
$this->sideDishes = $sideDishes;

return $this;
}

public function buildPizza(): Pizza
{
return new Pizza(
$this->base,
$this->sauce,
$this->cheese,
$this->sideDishes,
);
}
}

Comme vous pouvez le constater, au lieu d’instancier un objet Pizza vide, puis de lui attribuer des valeurs grâce aux méthodes du builder, nous faisons maintenant l’inverse. Pour rendre cela possible, notre builder possède les mêmes propriétés que la classe Pizza.

Nous n’avons plus besoin d’adapter le produit afin qu’il soit possible de le construire dans une seconde classe. Dorénavant, c’est notre builder qui s’adapte au produit.

Changeons de directeur

Nous avons un nouveau builder flambant neuf, mais nous avons toujours besoin d’un cuisinier.

Comme précédemment, nous allons commencer par lui définir une interface.

interface PizzaCook
{
/**
* @param SideDish[] $sideDishes
*/
public function clientOrderCustomPizza(
Client $client,
Base $base,
Sauce $sauce,
Cheese $cheese,
array $sideDishes
): void;
public function clientOrderAMargherita(Client $client): void;
public function clientOrderAPepperoni(Client $client): void;
}

Il y a pas mal de changements, des explications s’imposent.

Dans le premier exemple, il y avait autant de builder que de recette de pizza. Est-ce réellement la responsabilité du builder que de connaître les ingrédients de la pizza qu’il doit fabriquer ? Le builder n’a qu’une seule responsabilité : construire un objet Pizza.

C’est au cuisinier qu’incombe la responsabilité de lui fournir les ingrédients, comme c’est au cuisinier de savoir dans quel ordre il faut les ajouter. De ce fait, notre directeur implémente une méthode par recette de pizza qu’il sait préparer.

Concrètement, au lieu de créer un nouveau builder pour gérer une nouvelle recette, avec toute la duplication de code que cela occasionne, nous avons simplement à ajouter une méthode à notre directeur, qui appelle le builder en lui fournissant les ingrédients nécessaires.

Il est temps de vous montrer le résultat.

class ClassicPizzaCook implements PizzaCook
{
public function clientOrderCustomPizza(
Client $client,
Base $base,
Sauce $sauce,
Cheese $cheese,
array $sideDishes
): void {
$pizza = (new ClassicPizzaBuilder())
->baseIs($base)
->sauceIs($sauce)
->cheeseIs($cheese)
->sideDishesAre($sideDishes)
->buildPizza();

$client->receiveAPizza($pizza);
}

public function clientOrderAMargherita(Client $client): void
{
$this->clientOrderCustomPizza(
client: $client,
base: Base::Thin,
sauce: Sauce::Tomato,
cheese: Cheese::Mozzarella,
sideDishes: [SideDish::Basilic],
);
}

public function clientOrderAPepperoni(Client $client): void
{
$this->clientOrderCustomPizza(
client: $client,
base: Base::Thin,
sauce: Sauce::Tomato,
cheese: Cheese::Mozzarella,
sideDishes: [
SideDish::Pepperoni,
SideDish::Olive,
SideDish::Mushroom,
SideDish::Pepper,
],
);
}
}

Je ne l’ai pas encore abordé, mais il y a un autre changement majeur : l’ajout d’un client. Dans une pizzeria, le cuisinier prépare une pizza parce qu’un client la lui demande. Ainsi, je fournis un objet Client au cuisinier, afin qu’il lui serve la pizza commandée une fois prête.

$client->receiveAPizza($pizza);

Nous reviendrons sur ce client dans le chapitre suivant.

Enfin, nous avons ajouté une méthode permettant de créer une pizza sur mesure. Celle-ci attend bien entendu tous les ingrédients obligatoires, afin de ne pas autoriser la création d’une pizza invalide. Dans mon contexte, elle a surtout pour objectif de me permettre la factorisation du code des autres méthodes qui préparent une pizza.

Mettons en place un client

Notre client est très simple, puisque son seul rôle est de recevoir sa pizza prête.

interface Client
{
public function receiveAPizza(Pizza $pizza): void;
}
class FirstClient implements Client
{
private Pizza $pizza;

public function receiveAPizza(Pizza $pizza): void
{
$this->pizza = $pizza;
}
}

J’ai du mal à voir l’intérêt d’ajouter un client 🤔

Patience ! Je ne vous ai pas montré encore comment utiliser cette nouvelle version.

$client = new FirstClient();
$cook = new ClassicPizzaCook();

$cook->clientOrderAMargherita($client);

Je me permets également de vous rappeler l’ancienne méthode.

$margheritaCook = new Cook(new MargheritaPizzaBuilder());
$margheritaCook->cookPizza();
$margherita = $margheritaCook->getPizza();

Hors instanciation de nos objets, nous avons divisé le code par trois.

Je prends en compte la première ligne de la première version, car celle-ci définit quel type de pizza nous souhaitons. Elle joue ici un rôle métier.

Le client nous a ainsi permis de simplifier la création concrète d’une pizza, tout en rendant le code plus compréhensible. Nous avons auto-documenté notre développement en utilisant une méthode dont le nom désigne son comportement métier et non technique, contrairement à getPizza.

Une bonne pratique de la Programmation Orientée Objet est de prendre pour modèle les objets réels (métier) afin de concevoir nos objets dans le code et leurs interactions entre eux dans notre projet.

Outre le fait de simplifier l’utilisation de notre implémentation, le client ajoute de la flexibilité à notre code. Aujourd’hui, il ne fait rien, mais nous pouvons très facilement lui ajouter du comportement sans avoir à retoucher au cuisinier. Il dispose de l’objet Pizza en propriété. Il nous suffit de lui ajouter des méthodes afin de le faire interagir avec notre pizza. Nous pourrions par exemple la manger sur place, repartir avec ou la livrer à quelqu’un d’autre.

Que retenir de cet exemple ?

J’aime beaucoup cet exemple, car il est simple, mais permet de voir très facilement l’intérêt de ce pattern. En outre, j’ai expliqué au début de cet article que le pattern Builder est utile lorsque nous avons besoin d’instancier un objet complexe avec beaucoup de paramètres. Or, ce n’est pas le cas ici. Pourtant, l’exemple reste pertinent. Nous pouvons facilement et avec très peu de code préparer la même recette de pizza. Notre implémentation est réutilisable, pour le plus grand bonheur des développeurs qui nous succéderont.

De plus, comme je l’ai déjà démontré, l’implémentation de nouvelles recettes de pizza est fortement facilitée. Je vous recommande d’essayer pour vous en rendre compte.

Cet exemple est aussi intéressant pour montrer les risques de ce pattern. En effet, il me semble important de garder en tête que ce n’est pas parce que nous avons créé un magnifique builder, qu’il sera nécessairement utilisé à chaque fois que nous souhaitons obtenir une instance du produit.

L’implémentation que je vous ai proposé offre l’avantage de rendre plus robuste l’utilisation de ce pattern et nous permet de retrouver confiance en nos produits.

En préparant cet article, j’ai réfléchi à plusieurs approches, dont celle que j’étais le plus tenté de mettre en œuvre : utiliser un constructeur privé afin d’interdire l’instanciation de l’objet directement via l’opérateur new. Or, j’ai rapidement réalisé qu’il ne fallait pas interdire son instanciation directe. Il peut en effet exister des cas où nous avons besoin de le faire et où il serait extrêmement lourd de passer par le builder. Les tests unitaires ou fonctionnels peuvent être un exemple parmi d’autres. J’ai préféré cette approche qui offre la flexibilité de choisir la façon dont nous souhaitons élaborer notre objet sans que cette flexibilité nuise à la validité métier du produit crée. Nous pouvons tout aussi bien produire une instance simple ou complexe de la pizza. Dans tous les cas notre pizza sera intègre, pour le plus grand plaisir de vos papilles.

Elle est également facilement maintenable tout en restant simple à mettre en œuvre. Vous pouvez en effet faire évoluer votre pizza sans avoir à tout réécrire. De plus, que vous construisiez votre pizza avec son constructeur ou le builder, ça n’a aucun impact sur le reste de votre programme, puisque votre objet est dans tous les cas intègre.

Cette approche n’a pas pour prétention d’être la solution absolue, mais a le bénéfice d’apporter une piste intéressante afin de répondre à la problématique initialement soulevée.

Conclusion

J’ai souhaité vous proposer cet article dans le but de revenir sur ce pattern, de présenter un exemple remis au goût du jour et surtout de mettre en lumière les points d’attention à avoir lorsque nous l’utilisons.

L’objectif n’est pas d’émettre un jugement sur ce pattern. Nous le croisons régulièrement et il nous rend souvent bien service.

Enfin, je suppose que si nous le rencontrons autant, c’est aussi parce que, dans la grande famille des design patterns, il fait partie des plus abordables. Donc de ceux que nous sommes le plus tentés d’appliquer. Néanmoins, il ne convient pas à tous les cas d’usage.

Vous l’aurez compris, je n’en ai pas encore terminé avec les design patterns, ni avec Builder.

Remerciements

Je tiens à remercier chaleureusement Christophe Vaudry et Thomas Verhoken pour l’aide qu’ils m’ont apportée dans la préparation de cet article et leur relecture attentive.

Je remercie également Fred Hardy pour ses précieux conseils.

--

--