cv.cm

Programación · Patrones de diseño

Patrón Builder explicado con ejemplos

5 min de lectura

Respuesta corta

Builder es un patrón de diseño creacional que separa la construcción de un objeto complejo de su representación final, de modo que el mismo proceso paso a paso puede crear resultados distintos. En el código moderno suele aparecer como una API fluida: llamas a métodos como .url(), .header() y .timeout() sobre un builder, y después .build() devuelve un objeto terminado y validado. Sustituye a los constructores largos con muchos parámetros opcionales.

Intención

Separar la construcción de un objeto complejo de su representación, de modo que el mismo proceso de construcción pueda crear representaciones distintas. El patrón Builder es uno de los cinco patrones creacionales del libro de la Banda de los Cuatro (GoF).

El problema que resuelve

Imagina un objeto de petición HTTP con una URL, un método, cualquier número de cabeceras, un cuerpo opcional, un tiempo de espera y un número de reintentos. Un constructor para todo esto es difícil de llamar correctamente:

class HttpRequest {
  constructor(
    readonly url: string,
    readonly method: string,
    readonly headers: Record<string, string>,
    readonly body?: string,
    readonly timeoutMs?: number,
    readonly retries?: number,
  ) {}
}

// Which argument is which? Easy to swap 3 and 5000 by mistake.
const req = new HttpRequest("https://api.example.com", "POST", {}, undefined, 3, 5000);

Esto se llama constructor telescópico. Además, no tiene dónde poner reglas como «una petición GET no puede tener cuerpo», y no puede construirse poco a poco, por ejemplo cuando las cabeceras se añaden en varios lugares antes de enviar.

Estructura y participantes

ParticipanteResponsabilidadEn el ejemplo
BuilderDeclara los pasos para crear las partes del producto.Métodos de RequestBuilder
ConcreteBuilderImplementa los pasos, mantiene el estado parcial y devuelve el resultado.RequestBuilder
DirectorOpcional. Ejecuta los pasos en un orden fijo para producir configuraciones habituales.jsonPost()
ProductEl objeto complejo que se construye.HttpRequest

Implementación en TypeScript

Cada paso guarda un valor y devuelve this, de modo que las llamadas pueden encadenarse. build() valida la combinación y devuelve un producto inmutable:

interface HttpRequest {
  readonly url: string;
  readonly method: "GET" | "POST";
  readonly headers: Readonly<Record<string, string>>;
  readonly body?: string;
  readonly timeoutMs: number;
}

class RequestBuilder {
  private method: "GET" | "POST" = "GET";
  private headers: Record<string, string> = {};
  private body?: string;
  private timeoutMs = 10_000;

  constructor(private readonly url: string) {}

  post(body: string): this { this.method = "POST"; this.body = body; return this; }
  header(name: string, value: string): this { this.headers[name] = value; return this; }
  timeout(ms: number): this { this.timeoutMs = ms; return this; }

  build(): HttpRequest {
    if (this.timeoutMs <= 0) throw new Error("timeout must be positive");
    return Object.freeze({
      url: this.url, method: this.method, headers: { ...this.headers },
      body: this.body, timeoutMs: this.timeoutMs,
    });
  }
}

const request = new RequestBuilder("https://api.example.com/orders")
  .header("Accept", "application/json")
  .timeout(5_000)
  .build();
console.log(request.method, request.timeoutMs, request.headers);
// GET 5000 { Accept: 'application/json' }

Un Director es simplemente código que conoce una receta habitual. En TypeScript suele ser una función normal que maneja el builder:

interface Steps {
  header(name: string, value: string): this;
  post(body: string): this;
}

// Director: a reusable recipe that drives any builder with these steps.
function jsonPost<B extends Steps>(builder: B, data: unknown): B {
  return builder
    .header("Content-Type", "application/json")
    .post(JSON.stringify(data));
}

class LogBuilder implements Steps {
  log: string[] = [];
  header(name: string, value: string): this { this.log.push(`${name}=${value}`); return this; }
  post(body: string): this { this.log.push(`POST ${body}`); return this; }
}

console.log(jsonPost(new LogBuilder(), { id: 7 }).log);
// [ 'Content-Type=application/json', 'POST {"id":7}' ]

Esto muestra la idea original de GoF: los mismos pasos de construcción, manejados por el mismo director, pueden alimentar a builders distintos y producir representaciones distintas, aquí un registro en lugar de una petición.

Cuándo usarlo y cuándo no

Usa el patrón Builder cuando:

  • Un objeto tenga muchas partes opcionales, o partes que deban añadirse de una en una.
  • La construcción tenga reglas que involucren varios campos y deban comprobarse una sola vez, en build().
  • Quieras que el objeto terminado sea inmutable, pero aun así montarlo paso a paso.
  • El mismo proceso de construcción deba producir representaciones distintas, como un documento exportado a HTML, Markdown o PDF.

Evítalo cuando:

  • El objeto tenga pocos campos. Un constructor es más corto y igual de claro.
  • Tu lenguaje ya tenga parámetros con nombre u opcionales. En TypeScript, un único objeto de opciones suele eliminar por completo la necesidad de un builder.
interface RequestOptions {
  url: string;
  method?: "GET" | "POST";
  headers?: Record<string, string>;
  timeoutMs?: number;
}

function createRequest({ url, method = "GET", headers = {}, timeoutMs = 10_000 }: RequestOptions) {
  return { url, method, headers, timeoutMs };
}

console.log(createRequest({ url: "/orders", timeoutMs: 5_000 }).method); // GET

Usos en el mundo real

  • Java: java.net.http.HttpRequest.newBuilder() devuelve un HttpRequest.Builder; encadenas uri(), header() y timeout() y luego llamas a build().
  • Java: Locale.Builder ensambla y valida una configuración regional a partir de partes de idioma, región y escritura.
  • Java y C#: StringBuilder ensambla una cadena a partir de muchas partes añadidas antes de producir la cadena final inmutable.
  • Muchos constructores de consultas SQL y de datos de prueba usan el mismo estilo fluido.

Patrones relacionados

  • El patrón Abstract Factory crea familias enteras de productos y devuelve cada uno de inmediato; el patrón Builder construye un producto complejo paso a paso y lo devuelve al final.
  • Los árboles del patrón Composite suelen crearse con un builder, porque construir un árbol nodo a nodo es una serie natural de pasos.
  • La interfaz fluida es el estilo de API de encadenar llamadas que devuelven this. Builder suele usarla, pero no son lo mismo.
  • El patrón Prototype puede ser una alternativa cuando la mayoría de los objetos nuevos son pequeñas variaciones de una plantilla ya hecha.

Preguntas frecuentes

¿Por qué cada método del builder devuelve this?

Devolver el builder permite encadenar llamadas en una sola expresión, como .header(...).timeout(...).build(). Devolver this en TypeScript también conserva el tipo correcto en las subclases.

¿Necesito una clase Director?

No. El Director es opcional en la descripción de GoF. En la práctica, normalmente basta con una función que aplique una secuencia habitual de pasos.

¿Sigue siendo útil el patrón Builder en TypeScript?

A veces. Un objeto de opciones con valores por defecto cubre la mayoría de los casos simples. Builder sigue siendo útil cuando los objetos se ensamblan poco a poco, cuando hace falta validación entre campos o cuando quieres un resultado inmutable.

¿Cuál es la diferencia entre el patrón Builder y el patrón Factory Method?

Factory Method decide qué clase instanciar y devuelve el objeto en una sola llamada. Builder permite al cliente controlar cómo se ensambla un objeto a lo largo de varias llamadas.

Todos los tutoriales de programación