¿Cuándo usarías el patrón de construcción? [cerrado]

531

¿Cuáles son algunos comunes , ejemplos del mundo real de utilizar el Builder? ¿Qué te compra? ¿Por qué no solo usar un patrón de fábrica?

Charles Graham
fuente
El stackoverflow.com/questions/35238292/... mencionó algunas APIs que el patrón de uso constructor
Alireza Fattahi
Las respuestas de Aaron y Tetha son realmente informativas. Aquí está el artículo completo relacionado con esas respuestas.
Diablo

Respuestas:

262

La diferencia clave entre un constructor y una IMHO de fábrica, es que un constructor es útil cuando necesita hacer muchas cosas para construir un objeto. Por ejemplo, imagine un DOM. Tienes que crear muchos nodos y atributos para obtener tu objeto final. Se utiliza una fábrica cuando la fábrica puede crear fácilmente todo el objeto dentro de una llamada de método.

Un ejemplo de uso de un generador es la creación de un documento XML. He utilizado este modelo al crear fragmentos HTML, por ejemplo, podría tener un generador para crear un tipo específico de tabla y podría tener los siguientes métodos (no se muestran los parámetros) :

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

Este constructor entonces escupió el HTML para mí. Esto es mucho más fácil de leer que recorrer un gran método de procedimiento.

Echa un vistazo a Builder Pattern en Wikipedia .

JoshBerke
fuente
1021

A continuación se presentan algunas razones para argumentar el uso del patrón y el código de ejemplo en Java, pero es una implementación del Patrón de construcción cubierto por la Banda de cuatro en los patrones de diseño . Las razones por las que lo usaría en Java también son aplicables a otros lenguajes de programación.

Como Joshua Bloch dice en Effective Java, 2nd Edition :

El patrón de construcción es una buena opción cuando se diseñan clases cuyos constructores o fábricas estáticas tendrían más de un puñado de parámetros.

En algún momento, todos hemos encontrado una clase con una lista de constructores donde cada adición agrega un nuevo parámetro de opción:

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

Esto se llama el patrón de constructor telescópico. El problema con este patrón es que una vez que los constructores tienen 4 o 5 parámetros de largo, se hace difícil recordar el orden requerido de los parámetros , así como también qué constructor particular puede desear en una situación dada.

Una alternativa que tiene al Patrón de constructor telescópico es el Patrón de JavaBean donde llama a un constructor con los parámetros obligatorios y luego llama a cualquier configurador opcional después de:

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

El problema aquí es que debido a que el objeto se crea a través de varias llamadas, puede estar en un estado inconsistente a la mitad de su construcción. Esto también requiere mucho esfuerzo adicional para garantizar la seguridad del hilo.

La mejor alternativa es usar el Patrón de generador.

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

Tenga en cuenta que Pizza es inmutable y que los valores de los parámetros están todos en una sola ubicación . Debido a que los métodos de establecimiento del generador devuelven el objeto generador, pueden encadenarse .

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

Esto da como resultado un código fácil de escribir y muy fácil de leer y comprender. En este ejemplo, el método de compilación podría modificarse para verificar los parámetros después de que se hayan copiado del generador al objeto Pizza y arrojar una IllegalStateException si se ha proporcionado un valor de parámetro no válido. Este patrón es flexible y es fácil agregarle más parámetros en el futuro. Realmente solo es útil si va a tener más de 4 o 5 parámetros para un constructor. Dicho esto, podría valer la pena en primer lugar si sospecha que puede agregar más parámetros en el futuro.

He tomado prestado mucho sobre este tema del libro Effective Java, 2nd Edition de Joshua Bloch. Para obtener más información sobre este patrón y otras prácticas eficaces de Java, lo recomiendo encarecidamente.

Aaron
fuente
24
Diferente del generador GOF original, ¿verdad? Porque no hay clase de director. Me parece casi otro patrón, pero estoy de acuerdo en que es muy útil.
Lino Rosa
194
Para este ejemplo en particular, ¿no sería mejor eliminar los parámetros booleanos y poder decirnew Pizza.Builder(12).cheese().pepperoni().bacon().build();
Fabian Steeg el
46
Esto se parece más a una interfaz fluida que a un patrón de construcción .
Robert Harvey
21
@Fabian Steeg, creo que la gente está reaccionando de forma exagerada a los setters booleanos más bonitos, tenga en cuenta que este tipo de setters no permiten cambios en el tiempo de ejecución: Pizza.Builder(12).cheese().pepperoni().bacon().build();necesitaría recompilar su código o tener una lógica innecesaria si solo necesita un poco de pepperoni pizzas Como mínimo, también debe proporcionar versiones parametrizadas como @Kamikaze Mercenary originalmente sugerido. Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();. Por otra parte, nunca hacemos pruebas unitarias, ¿verdad?
egallardo
23
@JasonC Correcto, ¿y de qué sirve una pizza inmutable?
Maarten Bodewes
325

Considera un restaurante. La creación de la "comida de hoy" es un patrón de fábrica, porque usted le dice a la cocina "consígueme la comida de hoy" y la cocina (fábrica) decide qué objeto generar, en base a criterios ocultos.

El generador aparece si solicita una pizza personalizada. En este caso, el camarero le dice al chef (constructor) "¡Necesito una pizza; agréguele queso, cebolla y tocino!" Por lo tanto, el constructor expone los atributos que debe tener el objeto generado, pero oculta cómo configurarlos.

Tetha
fuente
Nitin amplió la analogía de la cocina en otra respuesta a esta pregunta .
Aparece el
19

La clase .NET StringBuilder es un gran ejemplo de patrón de generador. Se utiliza principalmente para crear una cadena en una serie de pasos. El resultado final que obtienes al hacer ToString () siempre es una cadena, pero la creación de esa cadena varía de acuerdo con las funciones que se usaron en la clase StringBuilder. En resumen, la idea básica es construir objetos complejos y ocultar los detalles de implementación de cómo se está construyendo.


fuente
99
No creo que ese sea el patrón de construcción. StringBuilder es solo otra implementación de una clase de matriz de caracteres (es decir, cadena), pero tiene en cuenta el rendimiento y la administración de memoria, porque las cadenas son inmutables.
Charles Graham el
23
Es absolutamente el patrón generador, como lo es la clase StringBuilder en Java. Observe cómo el método append () de estas dos clases devuelve el propio StringBuilder, de modo que se puede encadenar b.append(...).append(...)antes de llamar finalmente toString(). Cita: infoq.com/articles/internal-dsls-java
pohl
3
@pohl Ya no creo que sea realmente un patrón de construcción, diría que es más que una interfaz fluida.
Didier A.
"Observe cómo el método append () de ambas clases devuelve el propio StringBuilder", que no es el patrón Builder, es solo una interfaz fluida. Es solo que a menudo un Constructor TAMBIÉN usará una interfaz fluida. Un generador no tiene que tener una interfaz fluida.
bytedev
Pero tenga en cuenta que StringBuilder por naturaleza no está sincronizado, a diferencia del StringBuffer que está sincronizado.
Alan Deep
11

Para un problema de subprocesos múltiples, necesitábamos construir un objeto complejo para cada subproceso. El objeto representaba los datos que se procesaban y podía cambiar según la entrada del usuario.

¿Podríamos usar una fábrica en su lugar? si

¿Por qué no lo hicimos nosotros? Constructor tiene más sentido, supongo.

Las fábricas se utilizan para crear diferentes tipos de objetos que son del mismo tipo básico (implementar la misma interfaz o clase base).

Los constructores construyen el mismo tipo de objeto una y otra vez, pero la construcción es dinámica, por lo que se puede cambiar en tiempo de ejecución.

Cameron MacFarland
fuente
9

Lo usa cuando tiene muchas opciones con las que lidiar. Piensa en cosas como jmock:

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

Se siente mucho más natural y es ... posible.

También hay construcción xml, construcción de cadenas y muchas otras cosas. Imagina si java.util.Maphubiera puesto como constructor. Podrías hacer cosas como esta:

Map<String, Integer> m = new HashMap<String, Integer>()
    .put("a", 1)
    .put("b", 2)
    .put("c", 3);
Dustin
fuente
3
Olvidé leer el mapa "if" implementado un patrón de construcción y me sorprendió ver la construcción allí .. :)
sethu
3
:) Lo siento por eso. Es común en muchos idiomas regresar a sí mismo en lugar de anular. Puedes hacerlo en Java, pero no es muy común.
Dustin
77
El ejemplo del mapa es simplemente un ejemplo de encadenamiento de métodos.
nogridbag
@nogridbag En realidad, estaría más cerca del método en cascada. Aunque está usando el encadenamiento de una manera que simula la conexión en cascada, obviamente está encadenando, pero semánticamente se comporta como una cascada.
Didier A.
9

Mientras revisaba el marco de Microsoft MVC, pensé en el patrón del generador. Encontré el patrón en la clase ControllerBuilder. Esta clase es para devolver la clase de fábrica del controlador, que luego se utiliza para construir un controlador concreto.

La ventaja que veo al usar el patrón de construcción es que puede crear una fábrica propia y conectarla al marco.

@Tetha, puede haber un restaurante (Framework) dirigido por un chico italiano que sirve pizza. Para preparar pizza, el tipo italiano (Object Builder) usa Owen (Factory) con una base de pizza (clase base).

Ahora el chico indio se hace cargo del restaurante del italiano. El restaurante indio (Framework) sirve dosa en lugar de pizza. Para preparar dosa, el tipo indio (constructor de objetos) usa Sartén (Fábrica) con una Maida (clase base)

Si observa el escenario, la comida es diferente, la forma en que se prepara la comida es diferente, pero en el mismo restaurante (bajo el mismo marco). El restaurante debe construirse de tal manera que pueda soportar cocina china, mexicana o de cualquier tipo. El creador de objetos dentro del marco facilita agregar el tipo de cocina que desee. por ejemplo

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

Siempre me disgustó el patrón Builder como algo difícil de manejar, molesto y muy a menudo abusado por programadores menos experimentados. Es un patrón que solo tiene sentido si necesita ensamblar el objeto a partir de algunos datos que requieren un paso posterior a la inicialización (es decir, una vez que se recopilan todos los datos, haga algo con ellos). En cambio, en el 99% del tiempo, los constructores simplemente se usan para inicializar a los miembros de la clase.

En tales casos, es mucho mejor simplemente declarar los establecedores de withXyz(...)tipo dentro de la clase y hacer que devuelvan una referencia a sí mismo.

Considera esto:

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

Ahora tenemos una clase única ordenada que gestiona su propia inicialización y hace casi el mismo trabajo que el constructor, excepto que es mucho más elegante.

Pavel Lechev
fuente
Simplemente decidí que quería construir mi complejo documento XML como JSON. En primer lugar, ¿cómo sé que la clase 'Complex' es capaz de entregar un producto XMLable en primer lugar y cómo lo cambiaría para producir un objeto JSONable? Respuesta rápida: No puedo porque necesito usar constructores. Y cerramos el círculo ...
David Barker
1
total bs, Builder está diseñado para construir objetos inmutables y con la capacidad de cambiar la forma de construir en el futuro sin tocar la clase de producto
Mickey Tin
66
hmmm? ¿Leíste en alguna parte de la respuesta que me dice para qué está diseñado Builder? Es un punto de vista alternativo a la pregunta en la parte superior "¿Cuándo usarías el Patrón de construcción?", Que se basa en la experiencia de innumerables abusos del patrón, donde algo mucho más simple hace el trabajo mejor. Todos los patrones son útiles si sabe cuándo y cómo usarlos: ¡ese es el objetivo de documentar los patrones en primer lugar! Cuando el patrón se usa en exceso o peor, se usa incorrectamente, se convierte en un antipatrón dentro del contexto de su código. Por desgracia ...
Pavel Lechev
6

Otra ventaja del generador es que si tiene una Fábrica, todavía hay algo de acoplamiento en su código, porque para que la Fábrica funcione, tiene que conocer todos los objetos que puede crear . Si agrega otro objeto que podría crearse, deberá modificar la clase de fábrica para incluirlo. Esto también sucede en Abstract Factory.

Con el constructor, por otro lado, solo tiene que crear un nuevo constructor concreto para esta nueva clase. La clase de director permanecerá igual, porque recibe al constructor en el constructor.

Además, hay muchos sabores de constructor. Kamikaze Mercenary`s da otro.

Lino Rosa
fuente
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);
    }
}
Raman Zhylich
fuente
1
Puede mejorar sus respuestas de dos maneras: 1) Conviértalo en SSCCE. 2) Explica cómo responde esto a la pregunta.
james.garriss
3

Utilicé el constructor en la biblioteca de mensajería local. El núcleo de la biblioteca estaba recibiendo datos del cable, recopilándolos con la instancia de Builder, luego, una vez que Builder decidió que tenía todo lo necesario para crear una instancia de Mensaje, Builder.GetMessage () estaba construyendo una instancia de mensaje utilizando los datos recopilados del cable.

wasker
fuente
3

Echa un vistazo a InnerBuilder, un complemento IntelliJ IDEA que agrega una acción 'Generador' al menú Generar (Alt + Insertar) que genera una clase de generador interno como se describe en Java efectivo

https://github.com/analytically/innerbuilder

analíticamente
fuente
2

Cuando quise usar el XMLGregorianCalendar estándar para mi XML para objetar la clasificación de DateTime en Java, escuché muchos comentarios sobre lo pesado y engorroso que era usarlo. Estaba tratando de controlar los campos XML en las estructuras xs: datetime para administrar la zona horaria, milisegundos, etc.

Así que diseñé una utilidad para construir un calendario XMLGregorian desde un GregorianCalendar o java.util.Date.

Debido a dónde trabajo, no puedo compartirlo en línea sin contenido legal, pero aquí hay un ejemplo de cómo lo usa un cliente. Resume los detalles y filtra parte de la implementación de XMLGregorianCalendar que se usa menos para xs: datetime.

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

Por supuesto, este patrón es más un filtro, ya que establece campos en el xmlCalendar como indefinidos para que se excluyan, todavía lo "construye". He agregado fácilmente otras opciones al generador para crear una estructura xs: date y xs: time y también para manipular las compensaciones de zona horaria cuando sea necesario.

Si alguna vez has visto código que crea y usa XMLGregorianCalendar, verás cómo esto hizo que sea mucho más fácil de manipular.

John Brown
fuente
0

Un gran ejemplo del mundo real es usar cuando la unidad prueba sus clases. Utiliza constructores sut (sistema bajo prueba).

Ejemplo:

Clase:

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;

}

Prueba:

    [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();
    }
}
Rafael Miceli
fuente
¿Por qué necesitarías un constructor en este caso en lugar de solo agregar setters a tu CustomAuthenticationServiceclase?
Laur Ivan
Esa es una buena pregunta @LaurIvan! Tal vez mi ejemplo fue un poco pobre, pero imagina que no puedes cambiar la clase CustomAuthenticationService, el generador sería una forma atractiva de mejorar la lectura de tus pruebas unitarias. Y creo que crear captadores y establecedores expondrán sus campos, que se utilizarán solo para sus pruebas. Si desea obtener más información sobre Sut Builder, puede leer sobre Test Data Builder , que es más o menos lo mismo, pero para suts.
Rafael Miceli