C#.NETDesign PatternsArchitectureAPI Design

Le Fluent Builder Pattern peut clarifier la création d'objets complexes en C#, mais il devient vite une abstraction inutile s'il est applique sans discipline.

Yva Hajatiana
11 mars 2026
6 min de lecture
Partager :X / TwitterLinkedIn
Maquette modulaire représentant l’architecture logicielle

Fluent Builder en C# : usages, limites et exemple

Le Fluent Builder Pattern est souvent présenté comme une solution élégante dès qu'un objet possède plusieurs propriétés. Cette vision est trop simpliste. En pratique, le pattern est utile dans un nombre limité de cas: quand la construction est progressive, quand certaines combinaisons doivent être validées, ou quand on veut exposer une API expressive sans empiler les constructeurs.

Le sujet n'est donc pas "comment faire un builder ?", mais "dans quel contexte un builder améliore-t-il vraiment la conception ?".

Le vrai problème à résoudre

Dans une application .NET, la création d'un objet devient difficile lorsque plusieurs signaux se cumulent:

  • trop de paramètres dans le constructeur
  • plusieurs options facultatives
  • ordre des paramètres difficile à relire
  • validations conditionnelles
  • étapes de construction dépendantes les unes des autres

Un code comme celui-ci reste correct techniquement, mais il devient vite pénible à maintenir:

var invoice = new Invoice(
    customerId,
    currency,
    issueDate,
    dueDate,
    lines,
    discountRate,
    notes,
    paymentTerms,
    sendReminder,
    createdBy);

Le problème n'est pas seulement le nombre de paramètres. C'est aussi l'absence d'intention visible.

Ce que le Fluent Builder apporte réellement

Un bon builder rend la construction plus explicite:

var invoice = InvoiceBuilder
    .Create(customerId, currency)
    .WithIssueDate(issueDate)
    .WithDueDate(dueDate)
    .AddLines(lines)
    .WithDiscount(discountRate)
    .WithNotes(notes)
    .Build();

Le gain principal est la lisibilité. On comprend immédiatement ce qui est obligatoire, ce qui est optionnel et dans quel ordre conceptuel l'objet est prepare.

Les autres bénéfices sont réels, mais secondaires:

  • validation centralisée
  • API plus difficile à mal utiliser
  • tests plus lisibles
  • possibilité de réutiliser un schéma de construction

Le builder n'est pas une réponse universelle

Le Fluent Builder Pattern devient contre-productif quand il est applique à des objets simples.

Si une classe a trois ou quatre propriétés claires, un constructeur explicite ou un record suffit souvent largement:

public sealed record MoneyTransfer(
    Guid SourceAccountId,
    Guid DestinationAccountId,
    decimal Amount,
    string Currency);

Ajouter un builder ici ne simplifie rien. Il augmente seulement le nombre de types, le code boilerplate et la surface de maintenance.

Un bon signal: si le builder ne porte ni validation utile, ni progression logique, ni contrainte d'usage, il ne sert probablement à rien.

Les cas où il est pertinent

Le pattern devient défendable dans plusieurs situations.

1. Objet complexe avec étapes progressives

Quand la création suit un enchaînement métier, le builder clarifie le flux.

Exemple:

  • création d'une commande
  • ajout des lignes
  • application d'une remise
  • configuration de la livraison
  • validation finale

2. Construction avec combinaisons invalides possibles

Si certaines options sont incompatibles, le builder peut imposer des gardes explicites.

public InvoiceBuilder WithRecurringSchedule(RecurringSchedule schedule)
{
    if (_isOneShot)
    {
        throw new InvalidOperationException("A one-shot invoice cannot be recurring.");
    }

    _recurringSchedule = schedule;
    return this;
}

Ici, le builder joue un rôle de protection d'API, pas seulement de confort syntaxique.

3. Besoin d'une API lisible pour les appels fréquents

Certaines API internes sont appelées partout dans le base de code. Un style fluent peut réduire les erreurs et aider la lecture:

var query = SearchRequestBuilder
    .Create("distributed systems")
    .InLanguage("fr")
    .OnlyPublished()
    .PageSize(20)
    .Build();

Quand l'objet est souvent construit dans des variantes légèrement différentes, cette approche peut être plus claire qu'une accumulation de surcharge de constructeurs.

Builder mutable ou objet final immutable

La meilleure combinaison reste généralement:

  • builder mutable
  • objet final immutable

Le builder accumule l'état pendant la phase de construction. L'objet crée par Build() devient ensuite stable et sur.

public sealed class DeploymentConfiguration
{
    public string Environment { get; }
    public string Region { get; }
    public int Replicas { get; }

    internal DeploymentConfiguration(string environment, string region, int replicas)
    {
        Environment = environment;
        Region = region;
        Replicas = replicas;
    }
}

Cette séparation est importante. Le builder gère la flexibilité. L'objet final porte la stabilité.

Ou placer les validations

Un builder sérieux doit distinguer deux niveaux de validation:

  • validation immediate sur un paramètre invalide
  • validation globale au moment du Build()

Exemple:

public DeploymentConfiguration Build()
{
    if (string.IsNullOrWhiteSpace(_environment))
        throw new InvalidOperationException("Environment is required.");

    if (_replicas <= 0)
        throw new InvalidOperationException("Replicas must be greater than zero.");

    if (_environment == "production" && _replicas < 2)
        throw new InvalidOperationException("Production requires at least two replicas.");

    return new DeploymentConfiguration(_environment, _region, _replicas);
}

La validation locale évite les erreurs grossières. La validation globale protège les règles transverses.

Faut-il rendre l'API totalement fluide

Pas nécessairement. Beaucoup de builders deviennent caricaturaux parce qu'ils chaînent tout, même ce qui devrait rester simple.

Il vaut mieux garder ces principes:

  • méthodes au nom explicite
  • peu d'effets implicites
  • pas de magie cachée
  • Build() comme point de validation final

Une API fluide doit rester prévisible. Si elle devient "trop intelligente", elle devient aussi plus difficile à raisonner.

L'alternative moderne: records, object initializers et factories

En C# moderne, le builder partage l'espace avec d'autres outils:

  • record pour les objets de données simples
  • init pour les propriétés configurées une seule fois
  • méthodes de factory pour les cas de création standards

Exemple simple:

public sealed record NotificationOptions
{
    public required string Channel { get; init; }
    public required string Recipient { get; init; }
    public bool Urgent { get; init; }
    public string? Template { get; init; }
}

Si ce niveau de modélisation suffit, un builder n'apporte pas assez de valeur pour justifier son coût.

Une règle pratique pour choisir

Avant d'introduire un Fluent Builder, pose ces questions:

  • l'objet est-il vraiment complexe à construire ?
  • existe-t-il une progression logique de configuration ?
  • le constructeur actuel nuit-il à la lisibilité ?
  • y à-t-il des invariants difficiles à exprimer autrement ?
  • l'API sera-t-elle appelée souvent par d'autres développeurs ?

Si plusieurs réponses sont "non", il vaut mieux rester sur une solution plus simple.

Exemple complet

Voici un builder raisonnable, orienté intention:

public sealed class ApiClientOptionsBuilder
{
    private string? _baseUrl;
    private TimeSpan _timeout = TimeSpan.FromSeconds(30);
    private bool _enableRetries = true;

    public ApiClientOptionsBuilder WithBaseUrl(string baseUrl)
    {
        if (string.IsNullOrWhiteSpace(baseUrl))
            throw new ArgumentException("Base URL is required.", nameof(baseUrl));

        _baseUrl = baseUrl;
        return this;
    }

    public ApiClientOptionsBuilder WithTimeout(TimeSpan timeout)
    {
        if (timeout <= TimeSpan.Zero)
            throw new ArgumentOutOfRangeException(nameof(timeout));

        _timeout = timeout;
        return this;
    }

    public ApiClientOptionsBuilder EnableRetries(bool enabled = true)
    {
        _enableRetries = enabled;
        return this;
    }

    public ApiClientOptions Build()
    {
        if (_baseUrl is null)
            throw new InvalidOperationException("Base URL must be configured.");

        return new ApiClientOptions(_baseUrl, _timeout, _enableRetries);
    }
}

Ce builder reste utile parce qu'il:

  • encapsulé une validation réelle
  • expose des choix explicites
  • produit un objet final simple

Conclusion

Le Fluent Builder Pattern est un bon outil, mais c'est un outil de précision. Il est efficace quand il clarifie une construction complexe, impose des règles et rend une API plus sûre à utiliser. Il est mauvais quand il remplace mécaniquement des constructeurs déjà suffisants.

La meilleure décision de design n'est pas d'utiliser plus de patterns. C'est d'utiliser uniquement ceux qui réduisent la complexité réelle du code.

Mots-clés :C#.NETDesign PatternsArchitectureAPI Design
Y

Yva Hajatiana

Articles techniques sur l'ingénierie logicielle, .NET, le cloud et l'intelligence artificielle appliquée aux applications.