Quando useresti il ​​Builder Pattern? [chiuso]


531

Quali sono alcuni comuni , esempi reali di utilizzo del builder? Cosa ti compra? Perché non usare semplicemente un modello di fabbrica?


Lo stackoverflow.com/questions/35238292/... menzionato alcune API che l'uso builder modello
Alireza Fattahi

Le risposte di Aaron e Tetha sono davvero istruttive. Ecco l' articolo completo relativo a tali risposte.
Diablo,

Risposte:


262

La differenza chiave tra un costruttore e un IMHO di fabbrica è che un costruttore è utile quando è necessario fare molte cose per costruire un oggetto. Ad esempio, immagina un DOM. Devi creare molti nodi e attributi per ottenere il tuo oggetto finale. Una factory viene utilizzata quando la factory può facilmente creare l'intero oggetto all'interno di una chiamata di metodo.

Un esempio di utilizzo di un builder è la costruzione di un documento XML, ho usato questo modello per costruire frammenti HTML, ad esempio potrei avere un Builder per costruire un tipo specifico di tabella e potrebbe avere i seguenti metodi (i parametri non sono mostrati) :

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

Questo costruttore avrebbe quindi sputato il codice HTML per me. È molto più facile da leggere che non seguire un metodo procedurale di grandi dimensioni.

Dai un'occhiata al Builder Pattern su Wikipedia .


1021

Di seguito sono riportati alcuni motivi che sostengono l'uso del pattern e del codice di esempio in Java, ma è un'implementazione del Builder Pattern coperto da Gang of Four in Design Patterns . Le ragioni per cui lo useresti in Java sono applicabili anche ad altri linguaggi di programmazione.

Come afferma Joshua Bloch in Effective Java, 2nd Edition :

Il modello del costruttore è una buona scelta quando si progettano classi i cui costruttori o fabbriche statiche avrebbero più di una manciata di parametri.

Ad un certo punto tutti abbiamo incontrato una classe con un elenco di costruttori in cui ogni aggiunta aggiunge un nuovo parametro di opzione:

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

Questo è chiamato il modello del costruttore telescopico. Il problema con questo modello è che una volta che i costruttori sono lunghi 4 o 5 parametri, diventa difficile ricordare l' ordine richiesto dei parametri e quale costruttore specifico si potrebbe desiderare in una data situazione.

Un'alternativa è necessario il modello telescopica Constructor è il modello JavaBean cui si chiama un costruttore con i parametri obbligatori e quindi chiamare qualsiasi setter opzionali dopo:

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

Il problema qui è che, poiché l'oggetto viene creato su più chiamate, potrebbe trovarsi in uno stato incoerente durante la sua costruzione. Ciò richiede anche uno sforzo supplementare per garantire la sicurezza della filettatura.

La migliore alternativa è utilizzare il Builder Pattern.

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;
  }
}

Si noti che la pizza è immutabile e che i valori dei parametri sono tutti in un'unica posizione . Poiché i metodi setter del Builder restituiscono l'oggetto Builder, possono essere concatenati .

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

Ciò si traduce in un codice facile da scrivere e molto facile da leggere e comprendere. In questo esempio, il metodo di generazione potrebbe essere modificato per controllare i parametri dopo che sono stati copiati dal builder nell'oggetto Pizza e generare un'eccezione IllegalStateException se è stato fornito un valore di parametro non valido. Questo modello è flessibile ed è facile aggiungere ulteriori parametri in futuro. È davvero utile solo se hai più di 4 o 5 parametri per un costruttore. Detto questo, potrebbe essere utile in primo luogo se sospetti di poter aggiungere altri parametri in futuro.

Ho preso molto in prestito questo argomento dal libro Effective Java, 2nd Edition di Joshua Bloch. Per saperne di più su questo modello e altre pratiche Java efficaci, lo consiglio vivamente.


24
Diverso dal costruttore GOF originale giusto? Perché non esiste Director Class. Sembra quasi un altro modello per me, ma sono d'accordo che è molto utile.
Lino Rosa,

194
Per questo esempio particolare, non sarebbe meglio rimuovere i parametri booleani ed essere in grado di direnew Pizza.Builder(12).cheese().pepperoni().bacon().build();
Fabian Steeg,

46
Sembra più un'interfaccia fluida che un modello di generatore .
Robert Harvey,

21
@Fabian Steeg, penso che le persone stiano reagendo in modo eccessivo ai setter booleani dall'aspetto più bello, tieni presente che questo tipo di setter non consente cambiamenti di runtime:, Pizza.Builder(12).cheese().pepperoni().bacon().build();dovresti ricompilare il tuo codice o avere una logica non necessaria se hai solo bisogno di peperoni pizze. Per lo meno, dovresti anche fornire versioni parametrizzate come suggerito originariamente da @Kamikaze Mercenary. Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();. Inoltre, non effettuiamo mai test unitari, vero?
egallardo,

23
@JasonC Giusto, e a che serve una pizza immutabile?
Maarten Bodewes,

325

Prendi in considerazione un ristorante. La creazione del "pasto di oggi" è un modello di fabbrica, perché dici alla cucina "prendimi il pasto di oggi" e la cucina (fabbrica) decide quale oggetto generare, in base a criteri nascosti.

Il builder appare se ordini una pizza personalizzata. In questo caso, il cameriere dice allo chef (costruttore) "Ho bisogno di una pizza; aggiungi formaggio, cipolle e pancetta!" Pertanto, il builder espone gli attributi che l'oggetto generato dovrebbe avere, ma nasconde come impostarli.


Nitin ha esteso l'analogia della cucina in un'altra risposta a questa domanda .
apre dal

19

La classe .NET StringBuilder è un ottimo esempio di modello di generatore. Viene utilizzato principalmente per creare una stringa in una serie di passaggi. Il risultato finale ottenuto facendo ToString () è sempre una stringa ma la creazione di quella stringa varia in base alle funzioni utilizzate nella classe StringBuilder. Per riassumere, l'idea di base è costruire oggetti complessi e nascondere i dettagli di implementazione di come viene costruito.


9
Non penso che questo sia il modello del costruttore. StringBuilder è solo un'altra implementazione di una classe di array di caratteri (ovvero stringa), ma tiene conto delle prestazioni e della gestione della memoria, poiché le stringhe sono immutabili.
Charles Graham,

23
È assolutamente il modello builder, così come la classe StringBuilder in Java. Si noti come il metodo append () di entrambe queste classi restituisca StringBuilder stesso, in modo che si possa concatenare b.append(...).append(...)prima di chiamare finalmente toString(). Citazione: infoq.com/articles/internal-dsls-java
pohl

3
@pohl Ya Non penso che sia davvero un modello di costruttore, direi che è più semplicemente un'interfaccia fluida.
Didier A.

"Nota come il metodo append () di entrambe queste classi restituisce StringBuilder stesso" che non è il modello Builder, ma solo un'interfaccia fluida. È solo che spesso un Builder utilizzerà ANCHE un'interfaccia fluida. Un Builder non deve avere un'interfaccia fluida.
bytedev,

Ma nota che StringBuilder per natura non è sincronizzato, a differenza di StringBuffer che è sincronizzato.
Alan Deep,

11

Per un problema multi-thread, avevamo bisogno di costruire un oggetto complesso per ogni thread. L'oggetto rappresentava i dati in elaborazione e potrebbe cambiare in base all'input dell'utente.

Potremmo usare una fabbrica invece? sì

Perché non l'abbiamo fatto? Il costruttore ha più senso immagino.

Le fabbriche vengono utilizzate per creare diversi tipi di oggetti dello stesso tipo di base (implementare la stessa interfaccia o classe di base).

I costruttori costruiscono ripetutamente lo stesso tipo di oggetto, ma la costruzione è dinamica, quindi può essere modificata in fase di esecuzione.


9

Lo usi quando hai molte opzioni da affrontare. Pensa a cose come jmock:

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

Sembra molto più naturale ed è ... possibile.

C'è anche xml building, string building e molte altre cose. Immagina se java.util.Mapfosse stato messo come costruttore. Potresti fare cose come questa:

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

3
Ho dimenticato di leggere la mappa "if" implementato un modello di generatore ed è stato sorpreso di vedere lì il costrutto .. :)
sethu

3
:) Mi dispiace per quello. È comune in molte lingue restituire se stessi anziché il vuoto. Puoi farlo in Java, ma non è molto comune.
Dustin,

7
L'esempio della mappa è semplicemente un esempio di concatenamento di metodi.
nogridbag,

@nogridbag In realtà sarebbe più vicino al metodo a cascata. Sebbene utilizzi il concatenamento in un modo che simula il collegamento in cascata, quindi è ovviamente un concatenamento, ma semanticamente si comporta come un collegamento in cascata.
Didier A.

9

Mentre attraversavo il framework Microsoft MVC, ho pensato al modello di generatore. Mi sono imbattuto nel modello nella classe ControllerBuilder. Questa classe restituisce la classe di fabbrica del controller, che viene quindi utilizzata per costruire un controller concreto.

Il vantaggio che vedo nell'uso del pattern builder è che puoi creare una tua fabbrica e collegarla al framework.

@Tetha, può esserci un ristorante (Framework) gestito da un ragazzo italiano che serve pizza. Per preparare la pizza un ragazzo italiano (Object Builder) usa Owen (Factory) con una base per pizza (classe base).

Ora un ragazzo indiano prende il posto del ristorante da un ragazzo italiano. Server indiano (Framework) server dosa invece di pizza. Per preparare il ragazzo indiano dosa (costruttore di oggetti) usa la padella (fabbrica) con un Maida (classe base)

Se si guarda allo scenario, il cibo è diverso, il modo in cui viene preparato il cibo è diverso, ma nello stesso ristorante (sotto lo stesso quadro). Il ristorante dovrebbe essere costruito in modo tale da supportare cinese, messicano o qualsiasi cucina. Object builder all'interno di framework facilita il plug-in del tipo di cucina che desideri. per esempio

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

Non ho sempre apprezzato il modello Builder come qualcosa di ingombrante, invadente e molto spesso abusato da programmatori meno esperti. È un modello che ha senso solo se è necessario assemblare l'oggetto da alcuni dati che richiedono una fase di post-inizializzazione (cioè una volta raccolti tutti i dati - fare qualcosa con esso). Al contrario, nel 99% dei casi i costruttori vengono semplicemente utilizzati per inizializzare i membri della classe.

In tali casi è meglio dichiarare semplicemente i withXyz(...)setter di tipi all'interno della classe e farli restituire un riferimento a se stesso.

Considera questo:

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");

Ora abbiamo una singola classe ordinata che gestisce la propria inizializzazione e fa praticamente lo stesso lavoro del costruttore, tranne che è molto più elegante.


Ho appena deciso che volevo creare il mio complesso documento XML come JSON. In primo luogo, come faccio a sapere che la classe "Complesso" è in grado di fornire un prodotto XMLable in primo luogo e come lo cambierei per produrre un oggetto JSONable? Risposta rapida: non posso perché ho bisogno di usare i costruttori. E torniamo al punto di partenza ...
David Barker,

1
bs totale, Builder è progettato per costruire oggetti immutabili e con abilità cambia il modo di costruire in futuro senza toccare la classe di prodotto
Mickey Tin

6
hmmm? Hai letto da qualche parte nella risposta che mi dice che cosa è progettato Builder? È un punto di vista alternativo alla domanda in alto "Quando useresti il ​​Builder Pattern?", Che si basa sull'esperienza di innumerevoli abusi del pattern, in cui qualcosa di molto più semplice fa meglio il lavoro. Tutti i motivi sono utili se sai quando e come usarli - questo è il punto centrale della documentazione dei motivi in ​​primo luogo! Quando il pattern è abusato o peggio - abusato - allora diventa un anti-pattern nel contesto del codice. Purtroppo ...
Pavel Lechev,

6

Un altro vantaggio del builder è che se hai una Factory, c'è ancora qualche accoppiamento nel tuo codice, perché affinché Factory funzioni, deve conoscere tutti gli oggetti che può eventualmente creare . Se aggiungi un altro oggetto che potrebbe essere creato, dovrai modificare la classe di fabbrica per includerlo. Questo succede anche nella Fabbrica astratta.

Con il costruttore, d'altra parte, devi solo creare un nuovo costruttore concreto per questa nuova classe. La classe director rimarrà la stessa, perché riceve il builder nel costruttore.

Inoltre, ci sono molti gusti di builder. Kamikaze Mercenary`s ne dà un altro.


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
Puoi migliorare le tue risposte in due modi: 1) Renderlo SSCCE. 2) Spiegare come questo risponde alla domanda.
james.garriss,


3

Ho usato builder nella libreria di messaggistica di casa. Il core della libreria stava ricevendo i dati dal filo, raccogliendoli con l'istanza di Builder, quindi, una volta che Builder aveva deciso di avere tutto il necessario per creare un'istanza di messaggio, Builder.GetMessage () stava costruendo un'istanza di messaggio utilizzando i dati raccolti dal filo.



2

Quando volevo usare lo standard XMLGregorianCalendar per il mio XML per obiettare al marshalling di DateTime in Java, ho sentito molti commenti su quanto fosse pesante e ingombrante usarlo. Stavo cercando di controllare i campi XML in xs: strutture datetime per gestire fuso orario, millisecondi, ecc.

Quindi ho progettato un'utilità per creare un calendario XMLGregorian da un GregorianCalendar o java.util.Date.

A causa del luogo in cui lavoro, non sono autorizzato a condividerlo online senza restrizioni legali, ma ecco un esempio di come un client lo utilizza. Estrae i dettagli e filtra parte dell'implementazione di XMLGregorianCalendar che sono meno utilizzati per xs: datetime.

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

Concesso questo modello è più di un filtro poiché imposta i campi in xmlCalendar come indefinito in modo che siano esclusi, lo "costruisce" ancora. Ho facilmente aggiunto altre opzioni al builder per creare una struttura xs: date e xs: time e anche per manipolare gli offset del fuso orario quando necessario.

Se hai mai visto il codice che crea e utilizza XMLGregorianCalendar, vedresti come questo ha reso molto più facile la manipolazione.


0

Un ottimo esempio del mondo reale è quello di utilizzare quando testate le unità delle vostre classi. Si usano costruttori sut (System Under Test).

Esempio:

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;

}

Test:

    [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();
    }
}

Perché in questo caso avresti bisogno di un costruttore invece di aggiungere solo setter alla tua CustomAuthenticationServiceclasse?
Laur Ivan

Questa è una buona domanda @LaurIvan! Forse il mio esempio è stato un po 'scarso, ma immagina di non poter cambiare la classe CustomAuthenticationService, il costruttore sarebbe un modo attraente per migliorare la lettura dei test unitari. E penso che creare getter e setter esporrà i tuoi campi, che verranno utilizzati solo per i tuoi test. Se vuoi saperne di più su Sut Builder puoi leggere Test Data Builder , che è praticamente lo stesso, ma per Suts.
Rafael Miceli,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.