Quand utiliseriez-vous le modèle Builder? [fermé]


531

Quels sont les exemples courants et réels d'utilisation du modèle de générateur? Qu'est-ce que ça t'achète? Pourquoi ne pas simplement utiliser un motif d'usine?


Le stackoverflow.com/questions/35238292/… a mentionné certaines API qui utilisent le modèle de générateur
Alireza Fattahi

Les réponses d' Aaron et de Tetha sont vraiment informatives. Voici l' article complet relatif à ces réponses.
Diablo

Réponses:


262

La principale différence entre un constructeur et une IMHO d'usine, c'est qu'un constructeur est utile lorsque vous devez faire beaucoup de choses pour construire un objet. Imaginez par exemple un DOM. Vous devez créer de nombreux nœuds et attributs pour obtenir votre objet final. Une fabrique est utilisée lorsque la fabrique peut facilement créer l'objet entier dans un appel de méthode.

Un exemple d'utilisation d'un générateur est la construction d'un document XML, j'ai utilisé ce modèle lors de la création de fragments HTML par exemple, je pourrais avoir un générateur pour construire un type spécifique de table et il pourrait avoir les méthodes suivantes (les paramètres ne sont pas affichés) :

BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()

Ce constructeur cracherait alors le HTML pour moi. C'est beaucoup plus facile à lire que de parcourir une grande méthode procédurale.

Découvrez Builder Pattern sur Wikipedia .


1021

Voici quelques raisons plaidant en faveur de l'utilisation du modèle et de l'exemple de code en Java, mais il s'agit d'une implémentation du modèle de générateur couvert par le gang des quatre dans les modèles de conception . Les raisons pour lesquelles vous l'utiliseriez en Java s'appliquent également à d'autres langages de programmation.

Comme l'indique Joshua Bloch dans Effective Java, 2nd Edition :

Le modèle de générateur est un bon choix lors de la conception de classes dont les constructeurs ou les usines statiques auraient plus d'une poignée de paramètres.

Nous avons tous à un moment donné rencontré une classe avec une liste de constructeurs où chaque ajout ajoute un nouveau paramètre d'option:

Pizza(int size) { ... }        
Pizza(int size, boolean cheese) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) { ... }

C'est ce qu'on appelle le modèle de constructeur télescopique. Le problème avec ce modèle est qu'une fois que les constructeurs ont 4 ou 5 paramètres, il devient difficile de se souvenir de l' ordre requis des paramètres ainsi que du constructeur particulier que vous pourriez souhaiter dans une situation donnée.

Une alternative que vous avez au modèle de constructeur télescopique est le modèle JavaBean où vous appelez un constructeur avec les paramètres obligatoires, puis appelez tous les paramètres facultatifs après:

Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

Le problème ici est que, parce que l'objet est créé sur plusieurs appels, il peut être dans un état incohérent au cours de sa construction. Cela nécessite également beaucoup d'efforts supplémentaires pour garantir la sécurité du fil.

La meilleure alternative est d'utiliser le modèle Builder.

public class Pizza {
  private int size;
  private boolean cheese;
  private boolean pepperoni;
  private boolean bacon;

  public static class Builder {
    //required
    private final int size;

    //optional
    private boolean cheese = false;
    private boolean pepperoni = false;
    private boolean bacon = false;

    public Builder(int size) {
      this.size = size;
    }

    public Builder cheese(boolean value) {
      cheese = value;
      return this;
    }

    public Builder pepperoni(boolean value) {
      pepperoni = value;
      return this;
    }

    public Builder bacon(boolean value) {
      bacon = value;
      return this;
    }

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

  private Pizza(Builder builder) {
    size = builder.size;
    cheese = builder.cheese;
    pepperoni = builder.pepperoni;
    bacon = builder.bacon;
  }
}

Notez que Pizza est immuable et que les valeurs des paramètres sont toutes au même endroit . Étant donné que les méthodes de définition du générateur renvoient l'objet générateur, elles peuvent être chaînées .

Pizza pizza = new Pizza.Builder(12)
                       .cheese(true)
                       .pepperoni(true)
                       .bacon(true)
                       .build();

Il en résulte un code facile à écrire et très facile à lire et à comprendre. Dans cet exemple, la méthode de génération peut être modifiée pour vérifier les paramètres après qu'ils ont été copiés du générateur vers l'objet Pizza et lever une IllegalStateException si une valeur de paramètre non valide a été fournie. Ce modèle est flexible et il est facile d'y ajouter plus de paramètres à l'avenir. Il n'est vraiment utile que si vous allez avoir plus de 4 ou 5 paramètres pour un constructeur. Cela dit, cela pourrait être utile en premier lieu si vous pensez que vous ajouterez peut-être plus de paramètres à l'avenir.

J'ai emprunté massivement à ce sujet dans le livre Effective Java, 2nd Edition de Joshua Bloch. Pour en savoir plus sur ce modèle et d'autres pratiques Java efficaces, je le recommande vivement.


24
Différent du constructeur GOF d'origine, n'est-ce pas? Parce qu'il n'y a pas de classe Director. Semble presque comme un autre modèle pour moi, mais je conviens que c'est très utile.
Lino Rosa

194
Pour cet exemple particulier, ne serait-il pas plus agréable de supprimer les paramètres booléens et de pouvoir direnew Pizza.Builder(12).cheese().pepperoni().bacon().build();
Fabian Steeg

46
Cela ressemble plus à une interface fluide qu'à un modèle de générateur .
Robert Harvey

21
@Fabian Steeg, je pense que les gens réagissent de manière excessive aux setters booléens plus beaux, gardez à l'esprit que ce genre de setters n'autorise pas les changements d'exécution:, Pizza.Builder(12).cheese().pepperoni().bacon().build();vous auriez besoin de recompiler votre code ou d'avoir une logique inutile si vous n'avez besoin que de pepperoni pizzas. À tout le moins, vous devez également fournir des versions paramétrées comme @Kamikaze Mercenary initialement suggéré. Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();. Là encore, nous ne faisons jamais de tests unitaires, n'est-ce pas?
egallardo

23
@JasonC D'accord, et à quoi sert une pizza immuable de toute façon?
Maarten Bodewes

325

Considérez un restaurant. La création du «repas d'aujourd'hui» est un modèle d'usine, car vous dites à la cuisine «donnez-moi le repas d'aujourd'hui» et la cuisine (l'usine) décide quel objet générer, en fonction de critères cachés.

Le constructeur apparaît si vous commandez une pizza personnalisée. Dans ce cas, le serveur dit au chef (constructeur) "J'ai besoin d'une pizza; ajoutez-y du fromage, des oignons et du bacon!" Ainsi, le générateur expose les attributs que l'objet généré doit avoir, mais cache comment les définir.


Nitin a étendu l'analogie avec la cuisine dans une autre réponse à cette question .
Pops

19

La classe .NET StringBuilder est un excellent exemple de modèle de générateur. Il est principalement utilisé pour créer une chaîne en une série d'étapes. Le résultat final que vous obtenez sur ToString () est toujours une chaîne mais la création de cette chaîne varie en fonction des fonctions de la classe StringBuilder qui ont été utilisées. Pour résumer, l'idée de base est de construire des objets complexes et de masquer les détails d'implémentation de la façon dont il est construit.


9
Je ne pense pas que ce soit le modèle du constructeur. Le StringBuilder n'est qu'une autre implémentation d'une classe de tableau de caractères (c'est-à-dire une chaîne), mais il tient compte des performances et de la gestion de la mémoire, car les chaînes sont immuables.
Charles Graham

23
C'est absolument le modèle de générateur, tout comme la classe StringBuilder en Java. Remarquez comment la méthode append () de ces deux classes renvoie StringBuilder lui-même, afin que l'on puisse enchaîner b.append(...).append(...)avant de finalement appeler toString(). Citation: infoq.com/articles/internal-dsls-java
pohl

3
@pohl Ya Je ne pense pas que ce soit vraiment un modèle de constructeur, je dirais que c'est plus juste une interface fluide.
Didier A.

"Remarquez comment la méthode append () de ces deux classes renvoie StringBuilder lui-même" ce n'est pas le modèle Builder, c'est juste une interface fluide. C'est juste que souvent un Builder utilisera AUSSI une interface fluide. Un constructeur n'a pas besoin d'avoir une interface fluide.
bytedev

Mais notez que StringBuilder par nature n'est pas synchronisé, contrairement au StringBuffer qui est synchronisé.
Alan Deep

11

Pour un problème multi-thread, nous avions besoin d'un objet complexe à construire pour chaque thread. L'objet représentait les données en cours de traitement et pouvait changer en fonction de l'entrée utilisateur.

Pourrions-nous utiliser une usine à la place? Oui

Pourquoi pas nous? Le constructeur a plus de sens, je suppose.

Les usines sont utilisées pour créer différents types d'objets qui sont du même type de base (implémenter la même interface ou classe de base).

Les constructeurs construisent le même type d'objet à plusieurs reprises, mais la construction est dynamique et peut donc être modifiée au moment de l'exécution.


9

Vous l'utilisez lorsque vous avez beaucoup d'options à gérer. Pensez à des choses comme jmock:

m.expects(once())
    .method("testMethod")
    .with(eq(1), eq(2))
    .returns("someResponse");

Cela semble beaucoup plus naturel et c'est ... possible.

Il y a aussi la construction xml, la construction de chaînes et bien d'autres choses. Imaginez si vous java.util.Mapaviez mis en tant que constructeur. Vous pouvez faire des trucs comme ça:

Map<String, Integer> m = new HashMap<String, Integer>()
    .put("a", 1)
    .put("b", 2)
    .put("c", 3);

3
J'ai oublié de lire la carte "si" implémenté un modèle de constructeur et j'ai été surpris de voir la construction là-bas .. :)
sethu

3
:) Désolé pour ça. Il est courant dans de nombreuses langues de retourner soi au lieu de vide. Vous pouvez le faire en java, mais ce n'est pas très courant.
Dustin

7
L'exemple de carte est simplement un exemple de chaînage de méthode.
nogridbag

@nogridbag Ce serait en fait plus proche de la méthode en cascade. Bien qu'il utilise le chaînage d'une manière qui simule la cascade, il s'agit évidemment de chaînage, mais sémantiquement, il se comporte comme une cascade.
Didier A.

9

En passant par le framework Microsoft MVC, j'ai pensé au modèle de générateur. Je suis tombé sur le modèle dans la classe ControllerBuilder. Cette classe doit renvoyer la classe d'usine du contrôleur, qui est ensuite utilisée pour construire le contrôleur concret.

L'avantage que je vois dans l'utilisation du modèle de générateur est que vous pouvez créer votre propre usine et la connecter au framework.

@Tetha, il peut y avoir un restaurant (Framework) géré par un italien, qui sert de la pizza. Afin de préparer une pizza, un gars italien (Object Builder) utilise Owen (Factory) avec une base de pizza (classe de base).

Maintenant, un Indien prend le relais d'un italien. Restaurant indien (Framework) serveurs dosa au lieu de pizza. Afin de préparer dosa Indian guy (constructeur d'objet) utilise Frying Pan (Factory) avec un Maida (classe de base)

Si vous regardez le scénario, la nourriture est différente, la façon dont la nourriture est préparée est différente, mais dans le même restaurant (dans le même cadre). Le restaurant doit être construit de manière à pouvoir prendre en charge la cuisine chinoise, mexicaine ou toute autre cuisine. Le générateur d'objets à l'intérieur du cadre facilite le plugin du type de cuisine que vous souhaitez. par exemple

class RestaurantObjectBuilder
{
   IFactory _factory = new DefaultFoodFactory();

   //This can be used when you want to plugin the 
   public void SetFoodFactory(IFactory customFactory)
   {
        _factory = customFactory;
   }

   public IFactory GetFoodFactory()
   {
      return _factory;
   }
}

7

J'ai toujours détesté le modèle Builder comme quelque chose de lourd, de gênant et très souvent abusé par des programmeurs moins expérimentés. C'est un modèle qui n'a de sens que si vous devez assembler l'objet à partir de certaines données qui nécessitent une étape de post-initialisation (c'est-à-dire une fois que toutes les données sont collectées - faites quelque chose avec). Au lieu de cela, dans 99% du temps, les générateurs sont simplement utilisés pour initialiser les membres de la classe.

Dans de tels cas, il est de loin préférable de simplement déclarer withXyz(...)les sélecteurs de type à l'intérieur de la classe et de les renvoyer une référence à lui-même.

Considère ceci:

public class Complex {

    private String first;
    private String second;
    private String third;

    public String getFirst(){
       return first; 
    }

    public void setFirst(String first){
       this.first=first; 
    }

    ... 

    public Complex withFirst(String first){
       this.first=first;
       return this; 
    }

    public Complex withSecond(String second){
       this.second=second;
       return this; 
    }

    public Complex withThird(String third){
       this.third=third;
       return this; 
    }

}


Complex complex = new Complex()
     .withFirst("first value")
     .withSecond("second value")
     .withThird("third value");

Nous avons maintenant une classe unique soignée qui gère sa propre initialisation et fait à peu près le même travail que le constructeur, sauf que c'est beaucoup plus élégant.


Je viens de décider que je voulais construire mon document XML complexe en JSON. Premièrement, comment puis-je savoir que la classe 'Complexe' est capable de fournir un produit XMLable en premier lieu et comment pourrais-je le changer pour produire un objet JSONable? Réponse rapide: je ne peux pas parce que j'ai besoin d'utiliser des constructeurs. Et nous bouclons la boucle ...
David Barker

1
total bs, Builder est conçu pour construire des objets immuables et avec la capacité de changer la façon de construire à l'avenir sans toucher à la classe de produits
Mickey Tin

6
hmmm? Avez-vous lu quelque part dans la réponse me disant à quoi Builder est conçu? C'est un point de vue alternatif à la question du haut "Quand utiliseriez-vous le modèle Builder?", Qui est basée sur l'expérience d'innombrables abus du modèle, où quelque chose de beaucoup plus simple fait mieux le travail. Tous les modèles sont utiles si vous savez quand et comment les utiliser - c'est tout l'intérêt de documenter les modèles en premier lieu! Lorsque le motif est surutilisé ou pire - mal utilisé - il devient alors un anti-motif dans le contexte de votre code. Hélas ...
Pavel Lechev

6

Un autre avantage du constructeur est que si vous avez une Factory, il y a encore du couplage dans votre code, car pour que la Factory fonctionne, elle doit connaître tous les objets qu'elle peut éventuellement créer . Si vous ajoutez un autre objet qui pourrait être créé, vous devrez modifier la classe d'usine pour l'inclure. Cela se produit également dans l'usine abstraite.

Avec le constructeur, en revanche, il vous suffit de créer un nouveau constructeur de béton pour cette nouvelle classe. La classe directrice restera la même, car elle reçoit le constructeur dans le constructeur.

En outre, il existe de nombreuses saveurs de constructeur. Kamikaze Mercenary`s en donne un autre.


6
/// <summary>
/// Builder
/// </summary>
public interface IWebRequestBuilder
{
    IWebRequestBuilder BuildHost(string host);

    IWebRequestBuilder BuildPort(int port);

    IWebRequestBuilder BuildPath(string path);

    IWebRequestBuilder BuildQuery(string query);

    IWebRequestBuilder BuildScheme(string scheme);

    IWebRequestBuilder BuildTimeout(int timeout);

    WebRequest Build();
}

/// <summary>
/// ConcreteBuilder #1
/// </summary>
public class HttpWebRequestBuilder : IWebRequestBuilder
{
    private string _host;

    private string _path = string.Empty;

    private string _query = string.Empty;

    private string _scheme = "http";

    private int _port = 80;

    private int _timeout = -1;

    public IWebRequestBuilder BuildHost(string host)
    {
        _host = host;
        return this;
    }

    public IWebRequestBuilder BuildPort(int port)
    {
        _port = port;
        return this;
    }

    public IWebRequestBuilder BuildPath(string path)
    {
        _path = path;
        return this;
    }

    public IWebRequestBuilder BuildQuery(string query)
    {
        _query = query;
        return this;
    }

    public IWebRequestBuilder BuildScheme(string scheme)
    {
        _scheme = scheme;
        return this;
    }

    public IWebRequestBuilder BuildTimeout(int timeout)
    {
        _timeout = timeout;
        return this;
    }

    protected virtual void BeforeBuild(HttpWebRequest httpWebRequest) {
    }

    public WebRequest Build()
    {
        var uri = _scheme + "://" + _host + ":" + _port + "/" + _path + "?" + _query;

        var httpWebRequest = WebRequest.CreateHttp(uri);

        httpWebRequest.Timeout = _timeout;

        BeforeBuild(httpWebRequest);

        return httpWebRequest;
    }
}

/// <summary>
/// ConcreteBuilder #2
/// </summary>
public class ProxyHttpWebRequestBuilder : HttpWebRequestBuilder
{
    private string _proxy = null;

    public ProxyHttpWebRequestBuilder(string proxy)
    {
        _proxy = proxy;
    }

    protected override void BeforeBuild(HttpWebRequest httpWebRequest)
    {
        httpWebRequest.Proxy = new WebProxy(_proxy);
    }
}

/// <summary>
/// Director
/// </summary>
public class SearchRequest
{

    private IWebRequestBuilder _requestBuilder;

    public SearchRequest(IWebRequestBuilder requestBuilder)
    {
        _requestBuilder = requestBuilder;
    }

    public WebRequest Construct(string searchQuery)
    {
        return _requestBuilder
        .BuildHost("ajax.googleapis.com")
        .BuildPort(80)
        .BuildPath("ajax/services/search/web")
        .BuildQuery("v=1.0&q=" + HttpUtility.UrlEncode(searchQuery))
        .BuildScheme("http")
        .BuildTimeout(-1)
        .Build();
    }

    public string GetResults(string searchQuery) {
        var request = Construct(searchQuery);
        var resp = request.GetResponse();

        using (StreamReader stream = new StreamReader(resp.GetResponseStream()))
        {
            return stream.ReadToEnd();
        }
    }
}

class Program
{
    /// <summary>
    /// Inside both requests the same SearchRequest.Construct(string) method is used.
    /// But finally different HttpWebRequest objects are built.
    /// </summary>
    static void Main(string[] args)
    {
        var request1 = new SearchRequest(new HttpWebRequestBuilder());
        var results1 = request1.GetResults("IBM");
        Console.WriteLine(results1);

        var request2 = new SearchRequest(new ProxyHttpWebRequestBuilder("localhost:80"));
        var results2 = request2.GetResults("IBM");
        Console.WriteLine(results2);
    }
}

1
Vous pouvez améliorer vos réponses de deux manières: 1) Faites-en SSCCE. 2) Expliquez comment cela répond à la question.
james.garriss


3

J'ai utilisé Builder dans une bibliothèque de messagerie locale. Le noyau de la bibliothèque recevait des données du fil, les collectait avec l'instance Builder, puis, une fois que Builder avait décidé qu'il avait tout ce dont il avait besoin pour créer une instance de Message, Builder.GetMessage () construisait une instance de message en utilisant les données collectées à partir du câble.



2

Quand j'ai voulu utiliser le XMLGregorianCalendar standard pour mon XML pour le marshaling d'objet de DateTime en Java, j'ai entendu beaucoup de commentaires sur le poids et la lourdeur de son utilisation. J'essayais de contrôler les champs XML dans les structures xs: datetime pour gérer le fuseau horaire, les millisecondes, etc.

J'ai donc conçu un utilitaire pour construire un calendrier XMLGregorian à partir d'un GregorianCalendar ou java.util.Date.

En raison de l'endroit où je travaille, je ne suis pas autorisé à le partager en ligne sans autorisation légale, mais voici un exemple de la façon dont un client l'utilise. Il résume les détails et filtre certaines des implémentations de XMLGregorianCalendar qui sont moins utilisées pour xs: datetime.

XMLGregorianCalendarBuilder builder = XMLGregorianCalendarBuilder.newInstance(jdkDate);
XMLGregorianCalendar xmlCalendar = builder.excludeMillis().excludeOffset().build();

Étant donné que ce modèle est plus un filtre car il définit les champs du xmlCalendar comme non définis afin qu'ils soient exclus, il le "construit" toujours. J'ai facilement ajouté d'autres options au générateur pour créer une structure xs: date et xs: time et également pour manipuler les décalages de fuseau horaire en cas de besoin.

Si vous avez déjà vu du code qui crée et utilise XMLGregorianCalendar, vous verriez comment cela a rendu la manipulation beaucoup plus facile.


0

Un excellent exemple dans le monde réel est à utiliser lors des tests unitaires de vos classes. Vous utilisez des générateurs sut (System Under Test).

Exemple:

Classe:

public class CustomAuthenticationService
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationService(ICloudService cloudService, IDatabaseService databaseService)
    {
        _cloudService = cloudService;
        _databaseService = databaseService;
    }

    public bool IsAuthorized(User user)
    {            
        //Implementation Details
        return true;

}

Tester:

    [Test]
    public void Given_a_User_With_Permission_When_Verifying_If_Authorized_Then_Authorize_It_Returning_True()
    {
        CustomAuthenticationService sut = new CustomAuthenticationServiceBuilder();
        User userWithAuthorization = null;

        var result = sut.IsAuthorized(userWithAuthorization);

        Assert.That(result, Is.True);
    }

sut Builder:

public class CustomAuthenticationServiceBuilder
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationServiceBuilder()
    {
        _cloudService = new AwsService();
        _databaseService = new SqlServerService();
    }

    public CustomAuthenticationServiceBuilder WithAzureService(AzureService azureService)
    {
        _cloudService = azureService;

        return this;
    }

    public CustomAuthenticationServiceBuilder WithOracleService(OracleService oracleService)
    {
        _databaseService = oracleService;

        return this;
    }

    public CustomAuthenticationService Build()
    {
        return new CustomAuthenticationService(_cloudService, _databaseService);
    }

    public static implicit operator CustomAuthenticationService (CustomAuthenticationServiceBuilder builder)
    {
        return builder.Build();
    }
}

Pourquoi auriez-vous besoin d'un constructeur dans ce cas au lieu d'ajouter simplement des setters à votre CustomAuthenticationServiceclasse?
Laur Ivan

C'est une bonne question @LaurIvan! Peut-être que mon exemple était un peu pauvre, mais imaginez que vous ne pouvez pas changer la classe CustomAuthenticationService, le constructeur serait un moyen attrayant d'améliorer la lecture de vos tests unitaires. Et je pense que créer des getters et setters exposera vos champs, qui seront utilisés uniquement pour vos tests. Si vous voulez en savoir plus sur Sut Builder, vous pouvez lire sur Test Data Builder , qui est à peu près la même chose, mais pour suts.
Rafael Miceli
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.