Quantcast
Channel: Publicis Sapient Engineering – Engineering Done Right
Viewing all articles
Browse latest Browse all 1865

Design pattern : Builder et Builder sont dans un bateau

$
0
0

On ne présente plus le patron builder, l’un des plus connus des patrons de conception…

Mais parle-t-on toujours de la même chose?

Qu’est-ce que le design pattern builder ?

Le Gang Of Four nous donne la définition suivante du design pattern « builder » :

 

« Le pattern Builder est utilisé pour la création d’objets complexes dont les différentes parties doivent être créées suivant un certain ordre ou algorithme spécifique. Une classe externe contrôle l’algorithme de construction »

On a également le rôle de ce design pattern :

« Separate the construction of a complex object from its representation so that the same construction process can create different representations. »

« dissocier la construction d’un objet de sa représentation, afin que le même processus de construction ait la possibilité de créer des représentations différentes »

Typiquement, ce design pattern sera préconisé dans les situations où au moins :

  • l’objet final est imposant, et sa création complexe ;
  • beaucoup d’arguments doivent être passés à la construction de l’objet, afin d’avoir un design lisible ;
  • certains de ces arguments sont optionnels (par ex. : un bateau n’a pas forcément de capitaine, mais toujours une taille), ou ont plusieurs variations (la taille peut par exemple être passée en mètres ou en pieds).

Le builder fluent : beaucoup de méthodes chainées, un seul build

C’est l’image que l’on se fait le plus souvent du builder :
Une suite de méthodes chainées, suivie d’un build final qui agrège les données, généralement dans une innerClass

new BoatBuilder().withSize(2).build();
class Boat{
 int size;
 Boat(int size){ ...}

 class BoatBuilder(){
  int size;
  BoatBuilder withSize(int size){
   this.size=size
   return this;
  }
  Boat build(){
   return new Boat(size);
  }
 }
}

Ce builder permet ainsi facilement d’avoir des paramètres optionnels, à la différence d’un constructeur de base. Mais les paramètres passés sont injectés tels quels dans les attributs de classes, sans traitements.

Le fluent interface

Ce n’est pas un builder à proprement parler, mais le principe est le même :

Il s’agit en fait de l’utilisation du design pattern fluent lors de la construction d’un objet mutable (également appelée désignation chaînée : les setters préfixés par ‘with‘ renvoient également l’instance à la place de void)

Boat erika = new Boat().withCaptain(hadock).withSize(2);

Pourquoi utiliser le fluent interface comme builder?

Le fluent interface permet d’éviter efficacement l’anti-pattern « Telescoping Constructor Pattern », en particulier sur les instances immutables :

Boat(int size) { ... }        
Boat(int size, Human captain) { ... }    
Boat(int size, Human captain, boolean inWood, boolean motor) { ... }    
Boat(int size, Human captain, boolean inWood, boolean motor, boolean sail) { ... }
 
Boat aurore = new Boat(hadock , false, true, false);

Le builder Command : beaucoup de setter, un seul build() (mais qui fait beaucoup)

Relativement similaire au builder-fluent décrit ci-dessus, la principale différence est qu’un traitement est effectué au sein de la méthode build.(transformer un nom en Human)

class Boat(){
 Human captain;
 ...
}

class BoatBuilder(){
 String ownerName;
 BoatBuilder  withOwnerName(String ownerName){
  ...
 }
 
 Boat build(){
  Boat boat = new Boat();
  boat.setCaptain(new Human(ownerName));
  return boat;
 }
}
new BoatBuilder().withOwnerName("Hadock").build();

Ce pattern permet ainsi des traitement plus complexes lors de la conception: l’objet généré est plus que la somme des paramètres passés en entrée.
L’avantage principal est que nous n’avons pas à construire d’objets complexes avant de les passer en entrée : ces traitements peuvent être faits au sein du build.

Le design pattern command

Il s’agit en fait d’un design pattern command, dont la méthode void execute() a été sournoisement transformée en build().

Le rôle du design pattern command est en effet très similaire à celui du pattern builder :

« Encapsuler toutes les informations nécessaires pour effectuer une action séparément à une date ultérieure« 

Cas particulier : les classes tiers et le inner-builder command

Si votre builder-command doit faire appel à des classes services tiers, il peut-être nécessaire d’en faire une inner-class d’une classe service

Class Boat(){ 
 Human captain;
 ...
}
//outer class
class BoatService{
 @Autowired
 Dao dao;
 
 //inner class
 class BoatBuilder(){
  ...
  Boat build(){
   ...
   //l'inner classe accès aux attribut de l'outer class 
   boat.setCaptain(dao.findHuman(ownerName));
   return boat;
  }
 }
 
  ...
 Boat buildBoat(String ownerName){
  return new BoatBuilder().withOwnerName("Hadock").build();
 }
 
    Boat buildBoatWithoutCaptain(String ownerName){
  return new BoatBuilder().build();
 }
 
}

Le builder officiel : beaucoup de (part) build, une seule class Director.

Lorsque le Gang Of Four nous définit le builder, il nous définit en fait l’implémentation d’un monteur, qui repose sur un duo de classe builder-director :

 

xebia-builder-officiel

Étape par étape, le Director va demander au builder de construire les différentes parties d’un objet composé (voir design pattern composite), la récupération des données et l’ordonnancement des différentes étapes de construction étant gérée par une class « Director ».

Idéalement, la classe Builder est abstraite, afin de permettre différentes versions du montage/implémentation de la classe finale.

Cela correspond à cette implémentation:

class LicenceBuilder{
     License buildPartLicense(Boat boat, String ownerName){
  boat.setLicenseOwner(licenseService.find(ownerName));
  
 };
}
abstract class AbstractBoatBuilder<TBoat extends Boat>{
 abstract TBoat buildPartHull();
    abstract void buildPartPropulsion(TBoat boat);
    void buildAdministration(TBoat boat){
  if(boat.getOwnerLicense()!=null){
   boat.setCaptain(boat.getOwnerLicense().getNavigator());
   boad.setAuthorizedToNavigate(true);
  }  
 };
}

class BoatDirector{
 AbstractBoatBuilder boatBuilder;
 LicenseBuilder licenseBuilder;
 BoatDirector(AbstractBoatBuilder boatBuilder, LicenseBuilder licenseBuilder){
  this.builder = builder;
  this.licenseBuilder = licenseBuilder;
 }
 
 public Boat build(){
  Boat boat = builder.buildPartHull();
  builder.buildPartPropulsion(boat);
  licenseBuilder.buildPartLicense(boat, "Rackam LeRouge");
  boatBuilder.buildPartAdministation(boat)
  return boat;
 }
}

 
new BoatDirector(boatBuilder, licenseBuilder).build();

L’objectif du builder monteur est double :

  • D’une part, décomposer le build en phases distinctes,
  • D’autre part, autoriser différentes implémentation du monteur (à l’image des abstract Factory).
class Gallion extends Boat{...}
class GallionBuilder extends AbstractBuilder<Gallion>{
 Boat buildPartHull(){
  return new Gallion(); 
 };
 void buildPartPropulsion(Gallion boat){
  boat.setPropulsion(new Sail());
 }
}
class SubMarine extends Boat{...}
class SubMarineBuilder extends AbstractBuilder<SubMarine>{
 Boat buildPartHull(){
  return new SubMarine(); 
 }

 void buildPartPropulsion(SubMarine boat){
  boat.setPropulsion(new AtomicMotor());
 }

 @Overide
  void buildAdministration(Submarine boat, String ownerName){
  super.buildAdministration(boat, ownerName);
  boat.setNuclearCode("0000");
 };

}
 

La manière dont les méthodes builder.buildPart incorporent les modifications à l’objet composite est entièrement au choix du développeur. On aurait également pu écrire :

De même, pour le passage des arguments: la définition du Gang of Four ne précise pas s’ils doivent être passés aux builders ou au director.

Quels sont les avantages du builder-monteur ?

Ce pattern permet une séparation claire entre l’ordonnancement de la création, et la construction des différentes parties de l’objet.

Pourquoi séparer le builder du director?

Dans la pratique, on en arrive souvent à fusionner la classe director et la classe builder (par exemple dans la class StringBuilder)

Et les factories dans tout-ça?

Pour rappel :

  • Une factory est une méthode statique permettant une alternative au constructeur pour l’instanciation d’un objet.
  • Une abstract factory est une classe qui permet d’instancier des objets pour lesquels on n’a pas (encore) l’implémentation fournie.

Techniquement, les deux design pattern remplissent le même rôle. Les builders sont à préférer lorsque de nombreux paramètres entrent en jeu, ou lorsque certains paramètres sont optionnels.

Pourquoi tant d’implémentations différentes du builder?

Il existe un gap important entre la définition donnée par le Gang of Four, qui précise une implémentation, et le rôle auquel répond le pattern builder : « dissocier la construction d’un objet de sa représentation. » De plus, le design UML « officiel » ne répond pas à la problématique du passage des arguments.

A voile et à moteur: le builder-fluent-command-monteur

En théorie, si l’on voulait réaliser un builder  remplissant à la fois le rôle et la définition du Gang Of Four, on aboutirait à ceci :

class Boat{
 Human captain;
 Propulsion propulsion;
 License licenseOwner;
}
 
class LicenceBuilder{
     License buildPartLicense(Boat boat, String ownerName){
  boat.setLicenseOwner(licenseService.find(ownerName));
  
 };
}
abstract class AbstractBoatBuilder<TBoat extends Boat>{
 abstract TBoat buildPartHull();
    abstract void buildPartPropulsion(TBoat boat);
    void buildAdministration(TBoat boat){
  if(boat.getOwnerLicense()!=null){
   boat.setCaptain(boat.getOwnerLicense().getNavigator());
   boad.setAuthorizedToNavigate(true);

  }  
 };
}

class BoatDirector{
 AbstractBoatBuilder boatBuilder;
 LicenseBuilder licenseBuilder;
 
 BoatDirector(AbstractBoatBuilder boatBuilder, LicenseBuilder licenseBuilder){
  this.builder = builder;
  this.licenseBuilder = licenseBuilder;
 }
 
 String ownerName;
 BoatDirector withOwnerName(String ownerName){
  this.ownerName = ownerName;
  return this;
 }
 public Boat build(){
  Boat boat = builder.buildPartHull();
  builder.buildPartPropulsion(boat);
  licenseBuilder.buildPartLicense(boat, ownerName);
  boatBuilder.buildPartAdministation(boat)
  return boat;
 }
}

class Kayak extends Boat{...}
class KayakBuilder extends AbstractBuilder<Kayak>{

  Boat buildPartHull(){
  return new Kayak(); 
 };
 void buildPartPropulsion(Boat boat){
  boat.setPropulsion(new Paddle());
 }
}
new Director(new KayakBuilder(), new LicenseBuilder()).withOwnerName("Tintin").build();

Pourquoi ne l’utilise-t-on pas toujours (voir pas du tout) ?

L’inconvénient de ce design est qu’il devient rapidement très volumineux, et rajoute des complexités pas toujours justifiées: en effet suivant les cas, on n’aura pas besoin d’une construction ordonnée, de traitements complexes, de différentes implémentations du(des) builder(s), voir même d’injecter des paramètres. Il peut alors être pertinent d’utiliser une version « plus souple », allégée du design pattern builder.

Et au final, comment je choisis mon patron de conception?

Et pour conclure…

A chaque cas d’utilisation correspond son pattern builder. Encore faut-il choisir le bon !

Sources :

https://jlordiales.me/2012/12/13/the-builder-pattern-in-practice/
http://www.beabetterdeveloper.com/2013/03/use-builder-pattern-to-avoid-methods.html
http://rpouiller.developpez.com/tutoriel/java/design-patterns-gang-of-four/?page=page_2#LIV-B
http://www.blackwasp.co.uk/gofpatterns.aspx
http://martinfowler.com/bliki/FluentInterface.html
http://cheliou.developpez.com/tutoriels/software-craftsmanship/object-building/Du Telescoping Constructor au Builder Pattern 


Viewing all articles
Browse latest Browse all 1865

Trending Articles