POO & Design Patterns

Les design patterns Builder

Convenience Builder

À son niveau le plus simple, le patron builder est un moyen pratique (convenience builder) de construire un objet dont la construction demande de nombreux paramètres, dont certains peuvent être optionnels.

Exemple

public class Client{
    
    public Client(String firstName,
                  String lastName,
                  List<Email> email,
                  List<PhoneNumber>){
        ...
    }
}
Client client = new Client("Arnaud",
                           "Carayol",
                           List.of("arnaud.carayol@u-pem.fr","carayol@u-mlv.fr"),
                           List.of());

Peu lisible, List.of() vs null, pas de valeur par défaut.

Convenience builder pour Client (1/3)

Idée: On va introduire une classe mutable qui a pour fonction créer l'objet. C'est le convenience builder.

public ClientBuilder {
    private String firstName;
    private String lastName;
    private ArrayList<PhoneNumber> phones = new ArrayList<>();
    private ArrayList<Email> emails = new ArrayList<>();
    
    public ClientBuilder firstName(String firstName){
        this.firstName=Objects.requireNonNull(firstName);
        return this;
    }    

    public ClientBuilder lastName(String lastName){
        this.lastName=Objects.requireNonNull(lastName);
        return this;
    }

    public ClientBuilder phoneNumber(PhoneNumber phoneNumber){
        phones.add(Objects.requireNonNull(phoneNumber));
        return this;
    }

    public ClientBuilder email(Email email){
        emails.add(Objects.requireNonNull(email));
        return this;
    }

    public Client build(){
        if (firstName == null || lastName == null){
            throw new IllegalStateException();
        }
        return new Client(firstName,lastName,emails,phones);
    }
}

Convenience builder pour Client (2/3)


public class Client {

   public static class Builder {
       private String firstName;
       private String lastName;
       private ArrayList<PhoneNumber> phones = new ArrayList<>();
       private ArrayList<Email> emails = new ArrayList<>();
       
       public ClientBuilder firstName(String firstName){
           this.firstName=Objects.requireNonNull(firstName);
           return this;
       }    
   
       public ClientBuilder lastName(String lastName){
           this.lastName=Objects.requireNonNull(lastName);
           return this;
       }
   
       public ClientBuilder phoneNumber(PhoneNumber phoneNumber){
           phones.add(Objects.requireNonNull(phoneNumber));
           return this;
       }
   
       public ClientBuilder email(Email email){
           emails.add(Objects.requireNonNull(email));
           return this;
       }
   
       public Client build(){
           if (firstName == null || lastName == null){
               throw new IllegalStateException();
           }
           return new Client(firstName,lastName,emails,phones);
       }
   }
}

Convenience builder pour Client (2/3)

Client client = new ClientBuilder().firstName("Arnaud")
                                   .lastName("Carayol")
                                   .email("arnaud.carayol@u-pem.fr")
                                   .email("carayol@univ-mlv.fr")
                                   .build();

Les méthodes du builder retournent this pour pouvoir chaîner les appels.

La méthode build effectue les vérifications sur la consistence de l'état de l'objet.

Convenience builder pour Client (3/3)

public class Client{
	
	public static class ClientBuilder{
		...
	}

	private Client(ClientBuilder clientBuilder){
		this.firstName = clientBuilder.firstName;
		...
	}

}

Le builder est souvent le seul moyen de construire l'objet.
On peut aussi utiliser le builder pour fournir des paramètres par défaut
Pas obligatoire mais c'est une possibilité.

Convenience builder pour Client (3bis/3)

Plutôt que de faire des new Client.Builder, on peut faire un methode static qui renvoie un Builder.


public class Client{

}

Client client = Client.with().firstName("Arnaud")
                                   .lastName("Carayol")
                                   .email("arnaud.carayol@u-pem.fr")
                                   .email("carayol@univ-mlv.fr")
                                   .build();

Les méthodes du builder retournent this pour pouvoir chaîner les appels.

La méthode build effectue les vérifications sur la consistence de l'état de l'objet.

Dans l'API ...

  • StringBuilder
  • HttpRequest.Builder pour construire des requêtes HTTP.
  • Locale.Builder pour construire les paramètres de localisation.

Le Builder pattern du GoF

Le patron builder du GoF est plus ambitieux. Il s'applique lorsque l'on a un produit avec plusieurs implémentations mais que toutes les implémentations ont le même schéma de construction.

Par exemple, un rapport qui peut soit être au format HTML, Markdown, XML.

Quelque soit l'implémentation, le schéma de construction est le même.

  • titre
  • paragraphes
  • signature

Le Builder pattern du GoF

Exemple Report

public interface ReportBuilder{
	public ReportBuilder addTitle(String title);
	public ReportBuilder addParagraph(String paragraph);
	public ReportBuilder addSignature(String signature);
	public Report build();
}
public class HTMLReportBuilder implements ReportBuilder{
    private StringBuilder sb = new StringBuilder();

    @Override
    public ReportBuilder addTitle(String title){
        sb.add("<h1>"+title+"</h1>");
        return this;
    }

    @Override
    public ReportBuilder addParagraph(String paragraph){
        sb.add("<p>"+paragraph+"</p>");
        return this;
    }

       ...

    @Override
    public ReportBuilder build(){
        return new HTMLReport(sb.toString());
    }

}