Builder Pattern

Intención y problema

¿Cómo separar la construcción progresiva de un objeto complejo de su representación final? Builder organiza pasos de ensamblaje para que el proceso y la forma del producto puedan evolucionar con responsabilidades distintas. Pertenece a creational-patterns.

La formulación GoF enfatiza esa separación y la posibilidad de obtener diferentes representaciones con un proceso común. La síntesis de CMU sobre Builder también contrasta usos modernos del término. Los escenarios siguientes son propios, no implementaciones de ese material.

Construir un paquete de reporte puede requerir encabezado, secciones, anexos y configuración de salida. Mezclar lectura de datos, orden de ensamblaje y detalles de cada representación vuelve difícil cambiar una parte sin entender las demás. Builder puede ofrecer una conversación significativa para construir, en lugar de dispersar ese proceso entre callers.

Forces y proceso de construcción

Queremos claridad sobre partes opcionales, etapas dependientes e invariantes de construcción. También queremos evitar una abstracción extensa para objetos que un constructor expresa bien.

La complejidad puede venir de combinar opciones, repetir una configuración coherente o crear distintas representaciones. Tener muchos campos no prueba por sí solo la necesidad. Si todos son independientes y el lenguaje ofrece argumentos nombrados, el costo adicional puede no justificarse.

Otra fuerza es distinguir estado de construcción y producto utilizable. El primero puede estar incompleto; el segundo necesita cumplir su contrato cuando se entrega. La facilidad para configurar no debería permitir que un consumidor reciba un objeto que solo funciona bajo una secuencia implícita de setters.

Estructura clásica y colaboración

Client selects ConcreteBuilder
    ↓
ReportDirector → ReportBuilder → Product
                        ↑
                        ├── HtmlReportBuilder → HtmlReport
                        └── ArchiveBuilder → ExportArchive

Builder ofrece pasos de construcción. ConcreteBuilder decide cómo materializar partes y conserva el ensamblaje necesario. Product es el resultado. Un Director puede organizar el proceso común y el orden de pasos, sin conocer todos los detalles de representación.

En el ejemplo, el Director recorre información del reporte y solicita agregar encabezado, secciones y anexos. Un builder construye HTML; otro prepara un archivo de exportación con metadatos y contenido. El proceso reutiliza conversaciones significativas, mientras cada representación conserva decisiones propias.

Un Director separado no es obligatorio en implementaciones modernas. El caller puede organizar los pasos directamente cuando extraer esa secuencia no aporta valor. Tampoco todos los productos necesitan ser del mismo tipo: deben definirse contratos de pasos y obtención de resultado que sean honestos para cada representación.

Si una variante no puede representar un anexo, hace falta una política explícita. Ignorarlo silenciosamente no preserva necesariamente la intención del proceso. La separación no elimina incompatibilidades entre capacidades de los builders.

Builder clásico y Fluent Builder

En uso moderno, Builder también suele nombrar una API de construcción configurable:

OrderBuilder
  .ForCustomer(customer)
  .WithItem(item)
  .WithShipping(shipping)
  .Build()

Puede concentrar configuración opcional y validación antes de producir Order, aunque no exista un Director ni múltiples representaciones. Es una evolución terminológica útil que debe describirse sin igualar todos los mecanismos.

AspectoBuilder clásicoUso habitual de Fluent Builder
FocoSeparar proceso y representación.Configurar y producir un objeto mediante una conversación legible.
Variación posibleDiferentes builders y productos.Opciones, etapas o combinaciones de un producto.
Forma de APINo exige encadenamiento.Frecuentemente encadena operaciones.

Fluent API no demuestra automáticamente GoF Builder. Encadenar setters puede modificar un objeto ya existente sin separar construcción y representación. Fowler trata la fluidez como diseño de una conversación legible, con costos propios, en Fluent Interface.

El criterio no es contar métodos que devuelven this. Hay que entender qué proceso se separa, quién conserva estado intermedio y cuándo existe un producto válido.

Frente a constructores extensos

new Order(
  customer, address, items,
  discount, shipping, currency, ...
)

Un constructor extenso puede dificultar lectura y combinaciones opcionales. Constructor telescópico describe una familia de sobrecargas para distintas combinaciones; Builder puede reducir esa proliferación y hacer visibles elecciones significativas.

No conviene inferir que todo constructor largo necesita Builder. A veces el objeto mezcla responsabilidades, o algunos argumentos forman un valor cohesivo que debería expresarse directamente. La API de construcción no corrige por sí sola un modelo mal delimitado.

Builder ayuda cuando los pasos, configuraciones dependientes y validación combinada constituyen un proceso real. Un objeto inmutable sencillo con tres valores claros puede ser más comprensible mediante un constructor o función.

Builder y Factory

Factory decide u oculta creación de un producto. Builder expone una construcción progresiva. Una factory puede elegir un builder para la representación requerida; este después realiza los pasos acordados.

Factory selects Builder
    → Builder constructs Product

También una factory puede usar un builder internamente y entregar directamente el producto, sin exponer ese proceso al consumidor. Las intenciones pueden combinarse; llamar Create a un método o Build a otro no las demuestra.

factory-method-pattern especializa una operación de creación; eso no implica construcción progresiva. abstract-factory-pattern hace explícita una familia de productos compatibles. Puede proporcionar un builder adecuado, mientras este conserva la responsabilidad de ensamblaje.

creational-patterns mantiene el contexto de factories y otras soluciones. Builder no exige una jerarquía Factory Method ni una infraestructura de registro.

Estado intermedio e invariantes

Un builder temporalmente incompleto puede ser aceptable si todavía no se entrega como Product. Build debe decidir qué ocurre cuando falta Customer, hay elementos incompatibles o una combinación de Shipping y configuración resulta inválida.

Las opciones incluyen validar y devolver un error explícito, exigir datos iniciales mediante constructor seed o usar un staged/typed builder que limite operaciones disponibles. Son decisiones con costos distintos; más tipos no siempre mejoran comprensión.

La validación debe tener un propietario coherente. Duplicar reglas diferentes en Builder y Product permite divergencia; delegar en una validación compartida o en la construcción protegida del Product puede conservar autoridad. Validaciones tempranas pueden mejorar feedback, pero no deberían permitir omitir la comprobación final de la combinación.

También hay que definir reuse: Build consume el builder, lo reinicia o permite obtener otro resultado. Conservar accidentalmente un anexo de una construcción anterior produce un producto equivocado aunque cada paso individual sea válido.

Inmutabilidad y alternativas de lenguaje

mutable construction state
    ↓ Build()
immutable Product

Builder puede facilitar una fase mutable seguida de un producto inmutable. Si promete esa propiedad, posteriores cambios del builder no deberían modificar el producto entregado: colecciones y referencias compartidas necesitan una política adecuada.

Builder no es un requisito para immutability. Constructores de records, funciones y valores inmutables pueden expresar directamente una creación válida. Named arguments, optional parameters, object initializers e immutable copy/update syntax pueden reducir la necesidad de otra API.

Esas alternativas también requieren invariantes. Un initializer que permite publicar una combinación inválida no resuelve el problema de construcción solo por ser conciso. pattern-selection orienta la comparación según dependencias entre pasos, representaciones y costo de validación.

Consecuencias, pruebas y misuses

Builder puede concentrar configuración y procesos repetibles, mejorar lectura y separar representación. Agrega estado intermedio, tipos o funciones, navegación y políticas de cierre/reutilización. El beneficio debe superar esas obligaciones.

Las pruebas relevantes comprueban producto completo, combinaciones rechazadas, independencia de resultados y representaciones coherentes. Verificar solo llamadas encadenadas no demuestra invariantes ni aislamiento del estado temporal.

Son misuses un Builder para valores triviales, uno gigante que refleja cada setter y uno que deja escapar internals de construcción. También lo son producir objetos inválidos, duplicar validación inconsistente y clasificar cualquier API fluent como el patrón clásico.

design-pattern recuerda que intención, fuerzas y consecuencias definen la colaboración. design-patterns permite comparar otras entradas. Builder se justifica cuando explica un proceso significativo; mantener creación directa puede ser la respuesta más clara.