9. Builder (Constructor) - Patrón Creacional

El patrón Builder separa la construcción de un objeto complejo de su representación final. En lugar de un constructor monolítico con numerosos parámetros, descompone el proceso en pasos controlados, permitiendo crear distintas representaciones del mismo objeto usando el mismo flujo de construcción.

Builder resulta clave cuando los objetos tienen muchas combinaciones opcionales, deben armarse siguiendo un orden estricto o dependen de datos provenientes de fuentes heterogéneas. El patrón incrementa la legibilidad del código y reduce errores al evitar constructores con firmas difíciles de recordar.

9.1 Problema que resuelve

Los constructores con largas listas de parámetros son propensos a errores: el orden importa, muchos valores pueden ser opcionales y, si se utilizan setters, existe el riesgo de dejar el objeto en un estado inválido. Además, cuando se necesita crear distintas variantes (por ejemplo, versiones para exportar y para visualizar), duplicar el código de construcción se vuelve inviable.

Builder encapsula el proceso en una entidad dedicada, que conoce cada paso necesario y valida los datos antes de producir el objeto final. El cliente invoca solo los pasos que necesita, lo que mejora la claridad y evita estados inconsistentes.

9.2 Intención y motivación

La intención del patrón es aislar la lógica de armado del producto, ofreciendo una interfaz fluida para configurar sus partes. Se busca:

  • Controlar el orden de construcción y asegurar que se ejecuten los pasos obligatorios.
  • Evitar constructores telescópicos que combinan numerosas variantes.
  • Permitir múltiples representaciones del mismo producto compartiendo el mismo algoritmo de montaje.

En frameworks de persistencia y generación de documentos es común encontrar builders encadenables que simplifican la declaración de objetos complejos y favorecen la inmutabilidad.

9.3 Estructura y participantes

Los elementos principales del patrón Builder son:

  • Builder: interfaz que declara los pasos de construcción.
  • Builders concretos: implementan los pasos para generar una representación específica del producto.
  • Director: opcional; define el orden de los pasos y coordina la construcción.
  • Producto: objeto complejo resultante.

El director es útil cuando se desea reutilizar la secuencia de creación en distintos contextos. No obstante, muchos builders modernos permiten que el cliente llame directamente a los pasos mediante una interfaz fluida.

9.4 Implementación básica en Java

El siguiente ejemplo en Java exhibe un builder tradicional para construir un menú de restaurante, donde algunos componentes son opcionales.

public class Menu {
    private final String platoPrincipal;
    private final String bebida;
    private final String postre;

    private Menu(String platoPrincipal, String bebida, String postre) {
        this.platoPrincipal = platoPrincipal;
        this.bebida = bebida;
        this.postre = postre;
    }

    public String resumen() {
        return "Menú: " + platoPrincipal
                + (bebida != null ? ", bebida: " + bebida : "")
                + (postre != null ? ", postre: " + postre : "");
    }

    public static class Builder {
        private String platoPrincipal;
        private String bebida;
        private String postre;

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

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

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

        public Menu construir() {
            if (platoPrincipal == null || platoPrincipal.isBlank()) {
                throw new IllegalStateException("El plato principal es obligatorio");
            }
            return new Menu(platoPrincipal, bebida, postre);
        }
    }
}

El cliente obtiene un objeto consistente utilizando una API encadenable:

public class Aplicacion {
    public static void main(String[] args) {
        Menu menuEjecutivo = new Menu.Builder()
                .conPlatoPrincipal("Pasta al pesto")
                .conBebida("Agua mineral")
                .conPostre("Tiramisú")
                .construir();

        System.out.println(menuEjecutivo.resumen());
    }
}
Builder

9.5 Ejemplo completo en Java: generador de reportes JSON y XML

Consideremos un generador de reportes que puede exportar en JSON o XML. Cada formato requiere pasos similares (encabezados, cuerpo, pie), pero la representación final difiere. Implementaremos builders concretos y un director que orquesta la secuencia.

public class Reporte {
    private final String contenido;

    public Reporte(String contenido) {
        this.contenido = contenido;
    }

    public String contenido() {
        return contenido;
    }
}

public interface ReporteBuilder {
    void iniciarDocumento();
    void agregarEncabezado(String titulo);
    void agregarCuerpo(String dato, String valor);
    void agregarPie(String autor);
    Reporte obtenerResultado();
}

public class ReporteJsonBuilder implements ReporteBuilder {
    private final StringBuilder json = new StringBuilder();
    private boolean primerElemento = true;

    @Override
    public void iniciarDocumento() {
        json.append("{\n");
    }

    @Override
    public void agregarEncabezado(String titulo) {
        json.append("  \"titulo\": \"").append(titulo).append("\",\n");
        json.append("  \"datos\": [\n");
    }

    @Override
    public void agregarCuerpo(String dato, String valor) {
        if (!primerElemento) {
            json.append(",\n");
        }
        json.append("    { \"").append(dato).append("\": \"").append(valor).append("\" }");
        primerElemento = false;
    }

    @Override
    public void agregarPie(String autor) {
        json.append("\n  ],\n");
        json.append("  \"autor\": \"").append(autor).append("\"\n");
        json.append("}\n");
    }

    @Override
    public Reporte obtenerResultado() {
        return new Reporte(json.toString());
    }
}

public class ReporteXmlBuilder implements ReporteBuilder {
    private final StringBuilder xml = new StringBuilder();

    @Override
    public void iniciarDocumento() {
        xml.append("<reporte>\n");
    }

    @Override
    public void agregarEncabezado(String titulo) {
        xml.append("  <titulo>").append(titulo).append("</titulo>\n");
        xml.append("  <datos>\n");
    }

    @Override
    public void agregarCuerpo(String dato, String valor) {
        xml.append("    <dato nombre=\"").append(dato).append("\">")
                .append(valor).append("</dato>\n");
    }

    @Override
    public void agregarPie(String autor) {
        xml.append("  </datos>\n");
        xml.append("  <autor>").append(autor).append("</autor>\n");
        xml.append("</reporte>\n");
    }

    @Override
    public Reporte obtenerResultado() {
        return new Reporte(xml.toString());
    }
}

public class DirectorReporte {
    private final ReporteBuilder builder;

    public DirectorReporte(ReporteBuilder builder) {
        this.builder = builder;
    }

    public Reporte construirReporte(String titulo, Map<String, String> datos, String autor) {
        builder.iniciarDocumento();
        builder.agregarEncabezado(titulo);
        datos.forEach(builder::agregarCuerpo);
        builder.agregarPie(autor);
        return builder.obtenerResultado();
    }
}

El cliente selecciona el builder adecuado y utiliza el director para generar el reporte deseado:

public class Ejecucion {
    public static void main(String[] args) {
        Map<String, String> datos = Map.of("ventas", "1500", "devoluciones", "3");

        DirectorReporte directorJson = new DirectorReporte(new ReporteJsonBuilder());
        Reporte reporteJson = directorJson.construirReporte("Informe mensual", datos, "Equipo Comercial");
        System.out.println(reporteJson.contenido());

        DirectorReporte directorXml = new DirectorReporte(new ReporteXmlBuilder());
        Reporte reporteXml = directorXml.construirReporte("Informe mensual", datos, "Equipo Comercial");
        System.out.println(reporteXml.contenido());
    }
}

Gracias al director, la secuencia de pasos se mantiene consistente, mientras que los builders concretos controlan la representación final. Nuevos formatos (YAML, texto plano) solo requieren agregar otro builder.

Builder

9.6 Variantes del patrón

Existen diversas adaptaciones del patrón:

  • Builders encadenables: retornan la instancia del builder en cada paso, permitiendo una API fluida (como el ejemplo del menú).
  • Builders inmutables: cada paso crea un nuevo builder con el estado actualizado, favoreciendo la seguridad en contextos concurrentes.
  • Builders reutilizables: permiten reiniciar el estado para generar múltiples productos con instancias de builder recicladas.
  • Builder directorless: el cliente invoca los pasos directamente cuando el orden no es estricto o es evidente.

9.7 Buenas prácticas

  • Definir claramente qué pasos son obligatorios y validar su presencia antes de construir el producto.
  • Proveer valores por defecto razonables para simplificar la construcción en escenarios comunes.
  • Evitar exponer el objeto parcialmente construido hasta que el proceso haya finalizado.
  • Documentar los efectos de cada paso, especialmente si alteran dependencias externas o recursos compartidos.

9.8 Desventajas y riesgos

El patrón agrega capas extra (builders, directores) y puede resultar innecesario si el objeto es simple o tiene pocos parámetros opcionales. Además, cuando se deben introducir nuevos atributos obligatorios, es necesario actualizar todos los builders concretos, lo que requiere disciplina para mantenerlos sincronizados.

Para situaciones donde los objetos son pequeños, un constructor clásico asistido por un objeto de configuración puede resultar suficiente. Builder cobra sentido al mejorar la claridad y la reutilización ante escenarios complejos.

9.9 Cuándo elegir Builder

Builder es especialmente valioso cuando:

  • El objeto destino tiene muchas combinaciones de atributos opcionales u obligatorios.
  • Se desea separar la lógica de construcción de la representación final (por ejemplo, para generar diferentes formatos).
  • El proceso requiere validaciones intermedias o depende de recursos externos.
  • Se busca una API legible que minimice errores al configurar objetos complejos.

Si se utiliza en conjunto con otros patrones creacionales, Builder puede apoyarse en Prototype para clonar configuraciones base o en Abstract Factory para seleccionar el builder adecuado según la plataforma.