Le Pattern Builder (Constructeur d'Objets)

Introduction

Le pattern Builder est un design pattern de création qui permet de construire des objets complexes étape par étape. Il sépare la construction d'un objet complexe de sa représentation, permettant ainsi au même processus de construction de créer différentes représentations.

Avantages du Pattern Builder

  • Objets immutables : Permet de créer des objets immuables sans setters
  • Clarté du code : Rend le code plus lisible et expressif
  • Validation : Permet de valider les paramètres avant la création
  • Flexibilité : Paramètres optionnels facilement gérables
  • Consistance : Garantit que l'objet créé est dans un état valide

Diagramme UML du Pattern

@startuml
class Product {
  -String parametre1
  -String parametre2
  -String parametre3
  -String parametre4
  -Product(Builder)
  +getParametre1(): String
  +getParametre2(): String
  +getParametre3(): String
  +getParametre4(): String
}

class Builder {
  -String parametre1
  -String parametre2
  -String parametre3
  -String parametre4
  +Builder(String parametre1)
  +parametre2(String): Builder
  +parametre3(String): Builder
  +parametre4(String): Builder
  +build(): Product
}

Product +-- Builder
Builder ..> Product : <<creates>>

note right of Builder
  Le Builder permet de construire
  l'objet étape par étape avec
  une interface fluide
end note
@enduml

À Quelles Problématiques Répond-il ?

Problème 1 : Trop d'Arguments dans le Constructeur

public class Parameters {
   private String parametre1;
   private String parametre2;
   private String parametre3;
   private String parametre4;

   public Parameters(String parametre1, String parametre2, String parametre3, String parametre4) {
    this.parametre1 = parametre1;
    this.parametre2 = parametre2;
    this.parametre3 = parametre3;
    this.parametre4 = parametre4;
   }
}

Problèmes identifiés : - Le nombre d'arguments est supérieur à ce qui est conseillé (< 2 ou 3 maximum) - Ambiguïté de l'ordre des arguments - Difficulté de lecture et de maintenance - Impossible de rendre certains paramètres optionnels

Utilisation problématique :

// Quel paramètre correspond à quoi ?
Parameters params = new Parameters("valeur1", "valeur2", "valeur3", "valeur4");

Pour éviter tous ces paramètres, on peut passer par des setters, mais cela crée d'autres problèmes.

Problème 2 : Utilisation des Setters

public class Parameters {
   private String parametre1;
   private String parametre2;
   private String parametre3;
   private String parametre4;

   public Parameters() {
   }

   public void setParametre1(String parametre1) {
       this.parametre1 = parametre1;
   }

   public void setParametre2(String parametre2) {
       this.parametre2 = parametre2;
   }

   public void setParametre3(String parametre3) {
       this.parametre3 = parametre3;
   }

   public void setParametre4(String parametre4) {
       this.parametre4 = parametre4;
   }
}

Utilisation :

Parameters params = new Parameters();
params.setParametre1("valeur1");
params.setParametre2("valeur2");
params.setParametre3("valeur3");
params.setParametre4("valeur4");

Problèmes identifiés : - Pas d'immutabilité : L'objet peut être modifié après sa création - Pas de garantie de consistance : Impossible de savoir si tous les paramètres obligatoires sont renseignés - État intermédiaire invalide : L'objet existe dans un état incomplet entre les appels de setters - Thread-safety : Problèmes potentiels en environnement multi-thread

@startuml
participant Client
participant Parameters

Client -> Parameters: new Parameters()
activate Parameters
Parameters --> Client: objet incomplet
deactivate Parameters

Client -> Parameters: setParametre1("val1")
note right: Objet dans un état\nintermédiaire invalide

Client -> Parameters: setParametre2("val2")
note right: Toujours invalide

Client -> Parameters: setParametre3("val3")
note right: Toujours invalide

Client -> Parameters: setParametre4("val4")
note right: Enfin valide !
@enduml

Solution : Le Pattern Builder

Implémentation Complète

public class Parameters {
    // Attributs finaux (immutables)
    private final String parametre1;
    private final String parametre2;
    private final String parametre3;
    private final String parametre4;

    // Classe Builder interne statique
    public static class Builder {
        // Attributs du builder (mutables)
        private String parametre1;
        private String parametre2;
        private String parametre3;
        private String parametre4;

        // Constructeur avec paramètres obligatoires
        public Builder(String parametre1) { 
            this.parametre1 = parametre1;
        }

        // Méthodes fluent pour paramètres optionnels
        public Builder parametre2(String parametre2) {
            this.parametre2 = parametre2;
            return this;  // Retourne this pour chaînage
        }

        public Builder parametre3(String parametre3) {
            this.parametre3 = parametre3;
            return this;
        }

        public Builder parametre4(String parametre4) {
            this.parametre4 = parametre4;
            return this;
        }

        // Méthode de construction finale
        public Parameters build() {
            // Validation avant construction
            validate();
            return new Parameters(this);
        }

        private void validate() {
            if (parametre1 == null || parametre1.isEmpty()) {
                throw new IllegalStateException("parametre1 est obligatoire");
            }
            // Autres validations...
        }
    }

    // Constructeur privé accessible uniquement par le Builder
    private Parameters(Builder builder) {
        this.parametre1 = builder.parametre1;
        this.parametre2 = builder.parametre2;
        this.parametre3 = builder.parametre3;
        this.parametre4 = builder.parametre4;
    }

    // Getters uniquement (pas de setters = immutable)
    public String getParametre1() { return parametre1; }
    public String getParametre2() { return parametre2; }
    public String getParametre3() { return parametre3; }
    public String getParametre4() { return parametre4; }
}

Utilisation

// Création avec interface fluide
Parameters parametres = new Parameters.Builder("valeur1")
    .parametre2("valeur2")
    .parametre3("valeur3")
    .parametre4("valeur4")
    .build();

// Paramètres optionnels
Parameters parametres2 = new Parameters.Builder("valeur1")
    .parametre2("valeur2")
    .build();  // parametre3 et parametre4 sont null

Diagramme de Séquence

@startuml
participant Client
participant Builder
participant Parameters

Client -> Builder: new Builder("param1")
activate Builder
Builder --> Client: builder

Client -> Builder: parametre2("param2")
Builder --> Client: builder

Client -> Builder: parametre3("param3")
Builder --> Client: builder

Client -> Builder: parametre4("param4")
Builder --> Client: builder

Client -> Builder: build()
Builder -> Builder: validate()
Builder -> Parameters: new Parameters(builder)
activate Parameters
Parameters --> Builder: parameters (immutable)
deactivate Parameters
Builder --> Client: parameters
deactivate Builder

note right of Parameters
  L'objet est créé en une seule fois
  dans un état valide et immutable
end note
@enduml

Avantages de Cette Solution

Immutabilité : L'objet créé est immutable (attributs final, pas de setters)

Lisibilité : Le code est expressif et facile à comprendre

Validation : Les paramètres sont validés avant la création dans build()

Flexibilité : Paramètres optionnels gérés naturellement

Consistance : L'objet est toujours dans un état valide

Thread-safe : L'immutabilité garantit la sécurité en multi-thread

Exemples Réels

Exemple 1 : Configuration d'Application

public class AppConfig {
    private final String host;
    private final int port;
    private final String database;
    private final int timeout;
    private final boolean sslEnabled;
    private final int maxConnections;

    public static class Builder {
        // Obligatoires
        private final String host;
        private final int port;

        // Optionnels avec valeurs par défaut
        private String database = "default";
        private int timeout = 30;
        private boolean sslEnabled = false;
        private int maxConnections = 10;

        public Builder(String host, int port) {
            this.host = host;
            this.port = port;
        }

        public Builder database(String database) {
            this.database = database;
            return this;
        }

        public Builder timeout(int timeout) {
            this.timeout = timeout;
            return this;
        }

        public Builder sslEnabled(boolean sslEnabled) {
            this.sslEnabled = sslEnabled;
            return this;
        }

        public Builder maxConnections(int maxConnections) {
            this.maxConnections = maxConnections;
            return this;
        }

        public AppConfig build() {
            if (port < 0 || port > 65535) {
                throw new IllegalArgumentException("Port invalide");
            }
            if (timeout < 0) {
                throw new IllegalArgumentException("Timeout invalide");
            }
            return new AppConfig(this);
        }
    }

    private AppConfig(Builder builder) {
        this.host = builder.host;
        this.port = builder.port;
        this.database = builder.database;
        this.timeout = builder.timeout;
        this.sslEnabled = builder.sslEnabled;
        this.maxConnections = builder.maxConnections;
    }

    // Getters...
}

Utilisation :

// Configuration minimale
AppConfig config1 = new AppConfig.Builder("localhost", 8080)
    .build();

// Configuration complète
AppConfig config2 = new AppConfig.Builder("prod.example.com", 443)
    .database("production")
    .timeout(60)
    .sslEnabled(true)
    .maxConnections(100)
    .build();

Exemple 2 : Requête HTTP

public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final String body;
    private final int timeout;

    public static class Builder {
        private final String url;
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private int timeout = 30000;

        public Builder(String url) {
            this.url = url;
        }

        public Builder method(String method) {
            this.method = method;
            return this;
        }

        public Builder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

        public Builder headers(Map<String, String> headers) {
            this.headers.putAll(headers);
            return this;
        }

        public Builder body(String body) {
            this.body = body;
            return this;
        }

        public Builder timeout(int timeout) {
            this.timeout = timeout;
            return this;
        }

        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }

    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
        this.body = builder.body;
        this.timeout = builder.timeout;
    }

    // Getters...
}

Utilisation :

HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
    .method("POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token123")
    .body("{\"name\":\"John\"}")
    .timeout(5000)
    .build();

Builder avec Lombok

Lombok simplifie grandement l'écriture du pattern Builder avec l'annotation @Builder.

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class User {
    private final String firstName;
    private final String lastName;
    private final String email;
    private final int age;
    private final String phone;
}

Utilisation :

User user = User.builder()
    .firstName("John")
    .lastName("Doe")
    .email("john.doe@example.com")
    .age(30)
    .phone("+33612345678")
    .build();

Builder Lombok avec Valeurs par Défaut

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ServerConfig {
    private final String host;
    private final int port;

    @Builder.Default
    private final int timeout = 30;

    @Builder.Default
    private final boolean sslEnabled = false;

    @Builder.Default
    private final int maxConnections = 10;
}

Variantes du Pattern Builder

Builder avec Étapes Obligatoires (Step Builder)

Cette variante force l'ordre et la présence de certains paramètres.

public class User {
    private final String username;
    private final String password;
    private final String email;

    // Interfaces pour forcer l'ordre
    public interface UsernameStep {
        PasswordStep username(String username);
    }

    public interface PasswordStep {
        EmailStep password(String password);
    }

    public interface EmailStep {
        BuildStep email(String email);
    }

    public interface BuildStep {
        User build();
    }

    // Builder implémentant les interfaces
    public static class Builder implements UsernameStep, PasswordStep, EmailStep, BuildStep {
        private String username;
        private String password;
        private String email;

        private Builder() {}

        @Override
        public PasswordStep username(String username) {
            this.username = username;
            return this;
        }

        @Override
        public EmailStep password(String password) {
            this.password = password;
            return this;
        }

        @Override
        public BuildStep email(String email) {
            this.email = email;
            return this;
        }

        @Override
        public User build() {
            return new User(this);
        }
    }

    public static UsernameStep builder() {
        return new Builder();
    }

    private User(Builder builder) {
        this.username = builder.username;
        this.password = builder.password;
        this.email = builder.email;
    }
}

Utilisation :

// L'ordre est forcé par le compilateur
User user = User.builder()
    .username("john")      // Doit être appelé en premier
    .password("secret")    // Doit être appelé en second
    .email("john@example.com")  // Doit être appelé en troisième
    .build();

// Ceci ne compile pas :
// User user = User.builder().password("secret"); // Erreur de compilation !

@startuml
interface UsernameStep {
  +username(String): PasswordStep
}

interface PasswordStep {
  +password(String): EmailStep
}

interface EmailStep {
  +email(String): BuildStep
}

interface BuildStep {
  +build(): User
}

class Builder {
  -String username
  -String password
  -String email
}

Builder ..|> UsernameStep
Builder ..|> PasswordStep
Builder ..|> EmailStep
Builder ..|> BuildStep

note right of Builder
  Le Builder implémente toutes
  les interfaces pour forcer
  l'ordre des appels
end note
@enduml

Bonnes Pratiques

1. Rendre le Constructeur Privé

public class Product {
    // Constructeur privé - accessible uniquement par le Builder
    private Product(Builder builder) {
        // ...
    }
}

2. Utiliser final pour l'Immutabilité

public class Product {
    private final String name;  // Immuable
    private final int price;    // Immuable
}

3. Valider dans build()

public Product build() {
    // Validation avant construction
    if (name == null || name.isEmpty()) {
        throw new IllegalStateException("Le nom est obligatoire");
    }
    if (price < 0) {
        throw new IllegalStateException("Le prix doit être positif");
    }
    return new Product(this);
}

4. Retourner this pour le Chaînage

public Builder name(String name) {
    this.name = name;
    return this;  // Permet le chaînage
}

5. Copie Défensive pour les Collections

private Product(Builder builder) {
    // Copie défensive pour éviter les modifications externes
    this.tags = Collections.unmodifiableList(new ArrayList<>(builder.tags));
}

Quand Utiliser le Pattern Builder ?

✅ Utilisez le Builder quand :

  • Vous avez plus de 3-4 paramètres dans le constructeur
  • Certains paramètres sont optionnels
  • Vous voulez créer des objets immutables
  • L'ordre des paramètres n'est pas évident
  • Vous avez besoin de validation complexe
  • Vous voulez une API fluide et lisible

❌ N'utilisez pas le Builder quand :

  • L'objet est simple (1-2 paramètres)
  • Tous les paramètres sont obligatoires et leur ordre est évident
  • L'objet doit être mutable
  • La performance est critique (le Builder ajoute un léger overhead)

Comparaison avec d'Autres Patterns

@startuml
package "Patterns de Création" {
  [Builder] as builder
  [Factory Method] as factory
  [Abstract Factory] as abstractFactory
  [Prototype] as prototype
  [Singleton] as singleton
}

note right of builder
  Construction étape par étape
  Objets complexes
  Immutabilité
end note

note right of factory
  Création via méthode
  Polymorphisme
  Sous-types
end note

note right of abstractFactory
  Familles d'objets
  Cohérence
end note

note right of prototype
  Clonage
  Performance
end note

note right of singleton
  Instance unique
  Accès global
end note
@enduml

Conclusion

Le pattern Builder est une solution élégante pour créer des objets complexes de manière lisible et maintenable. Il offre :

  • Immutabilité : Objets thread-safe et prévisibles
  • Lisibilité : Code expressif et auto-documenté
  • Flexibilité : Gestion facile des paramètres optionnels
  • Validation : Garantie de consistance de l'objet
  • Maintenabilité : Ajout facile de nouveaux paramètres

Utilisez-le systématiquement pour les objets avec de nombreux paramètres, et considérez Lombok pour réduire le boilerplate.

Ressources