Builder – Serie de patrones de diseño en Java

Builder Java

Continuando con la serie sobre patrones de diseño en Java, hoy veremos un patrón que es especialmente útil cuando se necesita construir objetos complejos de forma controlada y flexible, el patrón Builder.

Qué es el patrón Builder?

Builder es un patrón de diseño creacional que separa la construcción de un objeto complejo de su representación final. Esto significa que puedes crear diferentes configuraciones del mismo objeto sin cambiar su estructura interna.

A diferencia de los anteriores patrones creacionales como Singleton o Factory Method, el patrón de diseño Builder se enfoca en construir objetos paso a paso. Esto lo hace ideal para objetos con muchas propiedades o configuraciones opcionales, manteniendo el código limpio y legible.

Esto es útil para hacer cosas como:

  • Objetos con múltiples configuraciones opcionales: Por ejemplo, un formulario de registro con campos opcionales.
  • Construcción paso a paso: Cuando el objeto necesita ensamblarse en ciertas etapas del proceso.
  • Evitar constructores con muchos parámetros: Especialmente cuando algunos parámetros son opcionales o tienen valores por defecto.

Y se puede aplicar en casos de uso como:

  • Creación de formularios: Configuración paso a paso de formularios con campos opcionales.
  • Conexiones HTTP: Librerías como OkHttp o Retrofit usan Builders para configurar solicitudes.
  • Generación de reportes: Configurar reportes con múltiples opciones como formato, contenido y diseño.
  • Juegos: Crear personajes o niveles con configuraciones personalizadas.
  • Objetos de configuración: Configurar conexiones a bases de datos o servicios en la nube.

Cómo implementar Builder en Java

Supongamos que estamos desarrollando un sistema de construcción de pedidos de pizza. Cada pizza puede tener diferentes configuraciones como tamaño, tipo de masa y una lista de ingredientes, adicionales, etc.

1. Clase Producto: Pizza

Crearemos una clase llamada Pizza, esta clase será la clase compleja que queremos contruir:

Nota: Usaremos un constructor privado en la clase Pizza para restringir la creación directa de objetos desde fuera de la clase y garantizar que siempre se utilice el Builder para construirlos.

public class Pizza {
    private String tamaño;
    private String masa;
    private String salsa;
    private String[] ingredientes;

    // Constructor privado
    private Pizza(Builder builder) {
        this.tamaño = builder.tamaño;
        this.masa = builder.masa;
        this.salsa = builder.salsa;
        this.ingredientes = builder.ingredientes;
    }

    @Override
    public String toString() {
        return "Pizza [tamaño=" + tamaño + ", masa=" + masa + 
               ", salsa=" + salsa + ", ingredientes=" + String.join(", ", ingredientes) + "]";
    }

    // Clase Builder interna
    public static class Builder {
        private String tamaño;
        private String masa;
        private String salsa;
        private String[] ingredientes;

        public Builder tamaño(String tamaño) {
            this.tamaño = tamaño;
            return this;
        }

        public Builder masa(String masa) {
            this.masa = masa;
            return this;
        }

        public Builder salsa(String salsa) {
            this.salsa = salsa;
            return this;
        }

        public Builder ingredientes(String... ingredientes) {
            this.ingredientes = ingredientes;
            return this;
        }

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

2. Uso de Builder para crear objetos

El cliente utiliza el Builder para configurar y construir diferentes instancias de la clase Pizza.

public class Main {
    public static void main(String[] args) {
        // Crear una pizza con configuración personalizada
        Pizza pizza1 = new Pizza.Builder()
                .tamaño("Grande")
                .masa("Corteza delgada")
                .salsa("Tomate")
                .ingredientes("Queso", "Pepperoni", "Aceitunas")
                .build();

        System.out.println(pizza1);

        // Crear otra pizza con diferentes configuraciones
        Pizza pizza2 = new Pizza.Builder()
                .tamaño("Mediana")
                .masa("Corteza gruesa")
                .salsa("Barbacoa")
                .ingredientes("Pollo", "Maíz", "Piña")
                .build();

        System.out.println(pizza2);
    }
}

Salida:

Pizza [tamaño=Grande, masa=Corteza delgada, salsa=Tomate, ingredientes=Queso, Pepperoni, Aceitunas]  
Pizza [tamaño=Mediana, masa=Corteza gruesa, salsa=Barbacoa, ingredientes=Pollo, Maíz, Piña]  

Ventajas y desventajas del patrón

Ventajas

  • Manejo de objetos complejos:
    Permite construir objetos con múltiples propiedades (especialmente opcionales) sin confundir al desarrollador con constructores largos o difíciles de manejar.
  • Legibilidad y mantenibilidad:
    El uso de métodos encadenados hace que el código sea fácil de entender y mantener, eliminando el riesgo de errores por desorden de parámetros.
  • Inmutabilidad del objeto:
    Los objetos creados son inmutables, lo que garantiza mayor seguridad y evita modificaciones accidentales después de la creación.

Desventajas

  • Mayor cantidad de código:
    Requiere escribir más clases y métodos, lo que puede ser innecesario para objetos simples y aumentar la complejidad.
  • No siempre es necesario:
    En objetos con pocas propiedades o configuraciones simples, el Builder puede ser excesivo y poco práctico.
  • Duplicación de lógica:
    Puede ser necesario duplicar validaciones o valores por defecto tanto en el Builder como en la clase principal, complicando el mantenimiento.

Fusionando patrones

Ahora que hemos visto 4 patrones de diseño Singleton, Prototype, Factory Method y Builder, cómo podríamos fusionarlos todos en un proyecto, aprovechando la ventajas que aporta cada uno?

La fusión de estos 4 patrones haría muy eficiente el funcionamiento de un sistema de configuración de vehículos en una fábrica.

Imaginemos una fábrica de automóviles donde:

  1. Solo debe haber una única instancia de la configuración global de producción, de modo que un vehículo solo tendrá un único proceso de fabricación.
  2. Los automóviles tienen muchas opciones de personalización, como color, tipo de motor, y equipamiento.
  3. Se pueden clonar modelos ya configurados para acelerar el proceso de creación.
  4. Al estar listo, el vehículo se envía a distintos países, por medio de diferentes medios de transporte.

Con el uso de los 4 patrones podemos focalizar el uso de cada una de las necesidades de modo que:

  1. Singleton: Garantizaría una configuración única para la el proceso de fábricación.
  2. Builder: Permitiría crear vehículos con la personalización requerida paso a paso.
  3. Prototype: Clonaría modelos existentes para ahorrar tiempo en la creación de vehículos similares o con configuraciones previamente creadas.
  4. Factory Method: Crearía instancias específicas de pedidos según el tipo de encargo, medio de transporte y tipo de envío ya sea nacional o internacional.

Este enfoque es modular, escalable y se adaptaría muy bien a sistemas complejos como fábricas o sistemas de marketplace.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *