Patrones de Diseño: Builder Pattern

3 minuto de lectura

El patrón builder está en la categoría en los patrones de diseño de creación (object creation) y según GoF (Gang of Four) el patrón Builder es usando para “Separar o abstraer la construcción de un objeto complejo de su representación”.

Veamos un ejemplo práctico, haremos un pizza builder paso a paso

Paso 1: Crear el Objeto a Construir - Pizza

public class Pizza {

  private final List<Topping> _toppings;

  private final BreadType _breadType;

  private final Souce _souce;

  public Pizza(List<Topping> toppings, BreadType breadType, Souce souce) {
    super();
    _toppings = toppings;
    _breadType = breadType;
    _souce = souce;
  }

  public final List<Topping> getToppings() {
    return _toppings;
  }

  public final BreadType getBreadType() {
    return _breadType;
  }

  public final Souce getSouce() {
    return _souce;
  }

  // Algunas enums para los ingredientes
  public enum Topping {
    PARMESAN_CHEESE, MOZZARELLA_CHEESE, 
    FETA_CHEESE, MUSHROOMS, 
    PESTO, PEPPERONI, 
    ONIONS, HAM;
  }

  // Algunas enums para salsas
  public enum Souce {
    TOMATO, CREAM;
  }

  // Algunas enums para tipos de masa
  public enum BreadType {
    FLAT_BREAD, PIZZA_PAN;
  }
}

Paso 2: Crear el Builder de Pizza

public final class PizzaBuilder {

  // DEFAULT VALUES
  private List<Topping> _toppings = new ArrayList<Topping>();

  private BreadType _breadType = BreadType.PIZZA_PAN;

  private Souce _souce = Souce.TOMATO;

  public PizzaBuilder withTopping(Topping topping) {
    _toppings.add(topping);
    return this;
  }

  public PizzaBuilder withBread(BreadType breadType) {
    _breadType = breadType;
    return this;
  }

  public PizzaBuilder withSouce(Souce souce) {
    _souce = souce;
    return this;
  }

  public Pizza buildPizza() {
    return new Pizza(_toppings, _breadType, _souce);
  }
}

Paso 3: Usar el Builder

public class PizzaBuilderTest {

  @Test
  public void testPizzaBuilder() {
    PizzaBuilder pizzaBuilder = new PizzaBuilder();
    pizzaBuilder.withBread(BreadType.FLAT_BREAD).withSouce(Souce.TOMATO)
        .withTopping(Topping.HAM)
        .withTopping(Topping.MOZZARELLA_CHEESE)
        .withTopping(Topping.ONIONS);

    Pizza pizzaWithoutPepperoni = pizzaBuilder.buildPizza();

    System.out.println("-------- Pizza Without Pepperoni ------");
    showBuildedPizza(pizzaWithoutPepperoni);

    Pizza pizzaWithPepperoni = pizzaBuilder.withTopping(Topping.PEPPERONI)
        .buildPizza();

    System.out.println("-------- Pizza With Pepperoni ------");
    showBuildedPizza(pizzaWithPepperoni);
  }

  private void showBuildedPizza(Pizza pizza) {
    System.out.println(pizza.getBreadType());
    System.out.println(pizza.getSouce());

    for (Topping topping : pizza.getToppings()) {
      System.out.println("Topping [" + topping + "]");
    }
  }
}

Como podrán apreciar, la instancia de pizzaBuilder sirve para crear mas de un tipo de pizza sin perder los atributos ya creados (Ver líneas 11 y 16). La única diferencia entre las dos instancias de pizza, es que a la segunda se le agregó pepperoni.

Algunas características del patrón builder:

  • Contiene valores por omisión (defaults): Es altamente recomendable que el Builder contenga valores por omisión (en lo posible), de no ser así, controlar cuando se construye la instancia del Objeto para que no existan problemas de integridad. En este caso los valores por omisión son Salsa de Tomates, Masa tipo Pan Pizza y sin ingredientes.
  • No contiene atributos con el modificador final: Por diseño los atributos de un builder no pueden ser final ya que van asignando a medida que se construye el objeto. Por lo tanto los builders en general son Not thread-safe. Al usarlos se debe tener conciencia que los datos almacenados en el builder se mantienen y se pueden crear múltiples instancias del Objeto a Construir (Pizza) pero siempre con los datos que se mantienen en el builder (es una ventaja y desventaja a la vez, según el caso). Se recomienda crear nuevas instancias del builder para cada caso.
  • El orden en que se ejecutan los métodos no es determinante: El orden en que se ejecutan los métodos de un builder no modifican la instancia del Objeto a Construir (siempre y cuando contengan los mismos atributos). Por ejemplo, en el caso del PizzaBuilder, si se ejecuta withBread y withSouce en distinto orden, no implica que la instancia de Pizza cambie. Podemos sobre-escribir el método equals() y al realizar la comparación en base a los ingredientes, tipo de masa y salsa, al ejecutar dicho método debería retornar siempre true.
PizzaBuilder builder1 = new PizzaBuilder()
builder.withBread(BreadType.FLAT_BREAD).withSouce(Souce.TOMATO);
pizza1 = builder.buildPizza();

PizzaBuilder builder2 = new PizzaBuilder()
builder.withSouce(Souce.TOMATO).withBread(BreadType.FLAT_BREAD);
pizza2 = builder.buildPizza();

pizza1.equals(pizza2); // -----> esto debería ser true
  • Es fácil de leer

No hay mucho que decir al respecto, comparen:

// Con Builder
Pizza pizza = new PizzaBuilder()
  .withBread(BreadType.FLAT_BREAD)
  .withSouce(Souce.TOMATO)
  .withTopping(Topping.HAM)
  .withTopping(Topping.MOZZARELLA_CHEESE)
  .buildPizza();

// Sin builder
List<Topping> toppings = new ArrayList<Topping>();
toppings.add(Topping.HAM);
toppings.add(Topping.MOZZARELLA_CHEESE);

Pizza pizza2 = new Pizza(toppings, BreadType.FLAT_BREAD, Souce.TOMATO);

Más información en:

Comentar