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.