Patrón Builder
Construye objetos con muchos parámetros, varios opcionales, paso a paso y con nombres (builder.disco(2000).conWifi().build()), y los valida una sola vez al final: sin constructores de diez parámetros.
nivel básicoTambién: constructor, constructor paso a paso, fluent builder, interfaz fluida
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Builder
Escribe el procesador, la RAM y las opciones que se piden al builder, separadas por comas: disco 2000, grafica RTX 4070, wifi, sistema Windows 11.
Paso 1
Se crea el builder con los dos datos obligatorios. Los opcionales empiezan con su valor por defecto. Todavía no existe ningún Ordenador.
1Ordenador pc = Ordenador.builder("Core i7", 32) // llamadas = 4, objeto creado = no
2 .disco(2000)
3 .conWifi()
4 .build();
5
6Builder disco(int gb) {
7 discoGb = gb;
8 return this;
9}
10
11Ordenador build() {
12 if (grafica != null && ram < 16)
13 throw new IllegalStateException("con gráfica hacen falta 16 GB");
14 return new Ordenador(this);
15}Variables
- llamadas
- 4
- objeto creado
- no
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Un ordenador a medida tiene dos piezas obligatorias y media docena opcionales. Con constructores, o se escribe uno gigante (new Ordenador("Core i7", 32, 2000, "RTX 4070", true, "Windows 11"), donde nadie sabe qué es cada true) o uno por combinación, que es imposible de mantener. Con setters, el objeto puede quedar a medias y deja de poder ser inmutable.
El patrón Builder pone una clase intermedia, el builder, que recibe los obligatorios en su constructor y cada opcional en un método con nombre que devuelve el propio builder, para encadenar las llamadas. Al final, build() comprueba que la combinación es válida y crea el objeto de una vez, ya completo e inmutable.
En Java lo habitual es que el builder sea una clase anidada estática del objeto que construye y que el constructor del objeto sea privado: la única forma de crearlo es pasar por build(). StringBuilder, HttpRequest.newBuilder(), AlertDialog.Builder en Android o @Builder de Lombok siguen esta misma idea.
Cuándo usarlo
- Un objeto tiene muchos parámetros, sobre todo si varios son opcionales o del mismo tipo (dos
intseguidos se confunden fácilmente). - El objeto debe ser inmutable pero no se puede crear de un solo golpe con un constructor claro.
- Hay reglas que dependen de varios parámetros a la vez (una gráfica exige 16 GB de RAM): se comprueban juntas en
build(). - Se construye algo por partes en un orden libre: una consulta SQL, un correo, una petición HTTP, un informe.
Cuándo no
- Si el objeto tiene dos o tres parámetros obligatorios: un constructor normal es más claro.
- En lenguajes con parámetros con nombre y valores por defecto (Python, C#, Kotlin, PHP 8) muchas veces basta con eso.
Participantes
- Producto. La clase que se construye (
Ordenador), con sus atributosfinaly el constructor privado que recibe el builder. - Builder. La clase anidada con los mismos atributos: los obligatorios en su constructor y los opcionales con su valor por defecto.
- Métodos de construcción. Uno por opción (
disco,conWifi…), que guarda el valor y devuelvethispara encadenar. - build(). Valida la combinación y crea el producto; si algo no vale, lanza una excepción y el objeto no llega a existir.
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2class Ordenador {
3 -procesador : String
4 -ram : int
5 -discoGb : int
6 -grafica : String
7 -wifi : boolean
8 -sistema : String
9 -Ordenador(b : Builder)
10 +{static} builder(procesador : String, ram : int) : Builder
11}
12class Builder {
13 -procesador : String
14 -ram : int
15 -discoGb : int
16 -grafica : String
17 -wifi : boolean
18 -sistema : String
19 +disco(gb : int) : Builder
20 +grafica(modelo : String) : Builder
21 +conWifi() : Builder
22 +sistema(nombre : String) : Builder
23 +build() : Ordenador
24}
25class Cliente
26Builder ..> Ordenador : crea
27Cliente ..> Builder : usa
28@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Un ordenador a medida
Cada llamada se lee sola (.conWifi()), los opcionales que no se piden toman su valor por defecto y build() rechaza las combinaciones imposibles.
1import java.util.*;
2
3/** Un ordenador a medida: dos piezas obligatorias y muchas opcionales. Inmutable una vez creado. */
4class Ordenador {
5 private final String procesador;
6 private final int ram;
7 private final int discoGb;
8 private final String grafica;
9 private final boolean wifi;
10 private final String sistema;
11
12 private Ordenador(Builder b) { // solo el builder puede crearlo
13 procesador = b.procesador;
14 ram = b.ram;
15 discoGb = b.discoGb;
16 grafica = b.grafica;
17 wifi = b.wifi;
18 sistema = b.sistema;
19 }
20
21 static Builder builder(String procesador, int ram) { return new Builder(procesador, ram); }
22
23 @Override
24 public String toString() {
25 return procesador + ", " + ram + " GB de RAM, " + discoGb + " GB de disco"
26 + (grafica != null ? ", gráfica " + grafica : "")
27 + (wifi ? ", wifi" : "") + ", " + sistema;
28 }
29
30 static class Builder {
31 private final String procesador; // obligatorios: en el constructor del builder
32 private final int ram;
33 private int discoGb = 512; // opcionales, con su valor por defecto
34 private String grafica;
35 private boolean wifi;
36 private String sistema = "sin sistema operativo";
37
38 private Builder(String procesador, int ram) {
39 this.procesador = procesador;
40 this.ram = ram;
41 }
42
43 // Cada método devuelve el propio builder: así las llamadas se encadenan
44 Builder disco(int gb) { discoGb = gb; return this; }
45 Builder grafica(String modelo) { grafica = modelo; return this; }
46 Builder conWifi() { wifi = true; return this; }
47 Builder sistema(String nombre) { sistema = nombre; return this; }
48
49 /** Valida una sola vez, con todo ya elegido, y crea el objeto. */
50 Ordenador build() {
51 if (!List.of(8, 16, 32, 64).contains(ram)) throw new IllegalStateException("RAM no válida: " + ram + " GB");
52 if (grafica != null && ram < 16) throw new IllegalStateException("con gráfica dedicada hacen falta 16 GB de RAM");
53 return new Ordenador(this);
54 }
55 }
56}
57
58public class Main {
59 public static void main(String[] args) {
60 Ordenador oficina = Ordenador.builder("Ryzen 5", 16).build();
61 Ordenador juegos = Ordenador.builder("Core i7", 32)
62 .disco(2000)
63 .grafica("RTX 4070")
64 .conWifi()
65 .sistema("Windows 11")
66 .build();
67 System.out.println("Oficina: " + oficina);
68 System.out.println("Juegos: " + juegos);
69 try {
70 Ordenador.builder("Celeron", 8).grafica("RTX 4090").build();
71 } catch (IllegalStateException e) {
72 System.out.println("No se crea: " + e.getMessage());
73 }
74 }
75}class Ordenador:
"""Un ordenador a medida: dos piezas obligatorias y muchas opcionales."""
def __init__(self, builder):
self._procesador = builder.procesador
self._ram = builder.ram
self._disco_gb = builder.disco_gb
self._grafica = builder.grafica
self._wifi = builder.wifi
self._sistema = builder.sistema
@staticmethod
def builder(procesador, ram):
return Ordenador.Builder(procesador, ram)
def __str__(self):
return (f"{self._procesador}, {self._ram} GB de RAM, {self._disco_gb} GB de disco"
+ (f", gráfica {self._grafica}" if self._grafica else "")
+ (", wifi" if self._wifi else "") + f", {self._sistema}")
class Builder:
def __init__(self, procesador, ram):
self.procesador = procesador # obligatorios
self.ram = ram
self.disco_gb = 512 # opcionales, con su valor por defecto
self.grafica = None
self.wifi = False
self.sistema = "sin sistema operativo"
# Cada método devuelve el propio builder: así las llamadas se encadenan
def disco(self, gb):
self.disco_gb = gb
return self
def con_grafica(self, modelo):
self.grafica = modelo
return self
def con_wifi(self):
self.wifi = True
return self
def con_sistema(self, nombre):
self.sistema = nombre
return self
def build(self):
"""Valida una sola vez, con todo ya elegido, y crea el objeto."""
if self.ram not in (8, 16, 32, 64):
raise ValueError(f"RAM no válida: {self.ram} GB")
if self.grafica and self.ram < 16:
raise ValueError("con gráfica dedicada hacen falta 16 GB de RAM")
return Ordenador(self)
oficina = Ordenador.builder("Ryzen 5", 16).build()
juegos = (Ordenador.builder("Core i7", 32)
.disco(2000)
.con_grafica("RTX 4070")
.con_wifi()
.con_sistema("Windows 11")
.build())
print(f"Oficina: {oficina}")
print(f"Juegos: {juegos}")
try:
Ordenador.builder("Celeron", 8).con_grafica("RTX 4090").build()
except ValueError as e:
print(f"No se crea: {e}")/** Un ordenador a medida: dos piezas obligatorias y muchas opcionales. */
class Ordenador {
#procesador; #ram; #discoGb; #grafica; #wifi; #sistema;
constructor(builder) { // JavaScript no tiene constructores privados: se crea siempre con el builder
this.#procesador = builder.procesador;
this.#ram = builder.ram;
this.#discoGb = builder.discoGb;
this.#grafica = builder.grafica;
this.#wifi = builder.wifi;
this.#sistema = builder.sistema;
}
static builder(procesador, ram) { return new OrdenadorBuilder(procesador, ram); }
toString() {
return `${this.#procesador}, ${this.#ram} GB de RAM, ${this.#discoGb} GB de disco`
+ (this.#grafica ? `, gráfica ${this.#grafica}` : "")
+ (this.#wifi ? ", wifi" : "") + `, ${this.#sistema}`;
}
}
class OrdenadorBuilder {
constructor(procesador, ram) {
this.procesador = procesador; // obligatorios
this.ram = ram;
this.discoGb = 512; // opcionales, con su valor por defecto
this.grafica = null;
this.wifi = false;
this.sistema = "sin sistema operativo";
}
// Cada método devuelve el propio builder: así las llamadas se encadenan
disco(gb) { this.discoGb = gb; return this; }
conGrafica(modelo) { this.grafica = modelo; return this; }
conWifi() { this.wifi = true; return this; }
conSistema(nombre) { this.sistema = nombre; return this; }
/** Valida una sola vez, con todo ya elegido, y crea el objeto. */
build() {
if (![8, 16, 32, 64].includes(this.ram)) throw new Error(`RAM no válida: ${this.ram} GB`);
if (this.grafica && this.ram < 16) throw new Error("con gráfica dedicada hacen falta 16 GB de RAM");
return new Ordenador(this);
}
}
const oficina = Ordenador.builder("Ryzen 5", 16).build();
const juegos = Ordenador.builder("Core i7", 32)
.disco(2000)
.conGrafica("RTX 4070")
.conWifi()
.conSistema("Windows 11")
.build();
console.log(`Oficina: ${oficina}`);
console.log(`Juegos: ${juegos}`);
try {
Ordenador.builder("Celeron", 8).conGrafica("RTX 4090").build();
} catch (e) {
console.log(`No se crea: ${e.message}`);
}using System;
// Un ordenador a medida: dos piezas obligatorias y muchas opcionales. Inmutable una vez creado.
class Ordenador {
private readonly string procesador;
private readonly int ram;
private readonly int discoGb;
private readonly string? grafica;
private readonly bool wifi;
private readonly string sistema;
private Ordenador(Builder b) { // solo el builder (clase anidada) puede crearlo
procesador = b.procesador;
ram = b.ram;
discoGb = b.discoGb;
grafica = b.grafica;
wifi = b.wifi;
sistema = b.sistema;
}
public static Builder Crear(string procesador, int ram) => new(procesador, ram);
public override string ToString() =>
quot;{procesador}, {ram} GB de RAM, {discoGb} GB de disco"
+ (grafica != null ? quot;, gráfica {grafica}" : "")
+ (wifi ? ", wifi" : "") + quot;, {sistema}";
public class Builder {
internal readonly string procesador; // obligatorios
internal readonly int ram;
internal int discoGb = 512; // opcionales, con su valor por defecto
internal string? grafica;
internal bool wifi;
internal string sistema = "sin sistema operativo";
internal Builder(string procesador, int ram) {
this.procesador = procesador;
this.ram = ram;
}
// Cada método devuelve el propio builder: así las llamadas se encadenan
public Builder Disco(int gb) { discoGb = gb; return this; }
public Builder Grafica(string modelo) { grafica = modelo; return this; }
public Builder ConWifi() { wifi = true; return this; }
public Builder Sistema(string nombre) { sistema = nombre; return this; }
// Valida una sola vez, con todo ya elegido, y crea el objeto.
public Ordenador Build() {
if (ram is not (8 or 16 or 32 or 64)) throw new InvalidOperationException(quot;RAM no válida: {ram} GB");
if (grafica != null && ram < 16) throw new InvalidOperationException("con gráfica dedicada hacen falta 16 GB de RAM");
return new Ordenador(this);
}
}
}
class Program {
static void Main() {
Ordenador oficina = Ordenador.Crear("Ryzen 5", 16).Build();
Ordenador juegos = Ordenador.Crear("Core i7", 32)
.Disco(2000)
.Grafica("RTX 4070")
.ConWifi()
.Sistema("Windows 11")
.Build();
Console.WriteLine(quot;Oficina: {oficina}");
Console.WriteLine(quot;Juegos: {juegos}");
try {
Ordenador.Crear("Celeron", 8).Grafica("RTX 4090").Build();
} catch (InvalidOperationException e) {
Console.WriteLine(quot;No se crea: {e.Message}");
}
}
}<?php
/** Un ordenador a medida: dos piezas obligatorias y muchas opcionales. Inmutable una vez creado. */
final class Ordenador {
private function __construct(
private string $procesador,
private int $ram,
private int $discoGb,
private ?string $grafica,
private bool $wifi,
private string $sistema,
) {}
public static function builder(string $procesador, int $ram): OrdenadorBuilder {
return new OrdenadorBuilder($procesador, $ram);
}
/** Lo usa el builder: PHP no tiene clases anidadas que puedan llamar al constructor privado. */
public static function desde(OrdenadorBuilder $b): self {
return new self($b->procesador, $b->ram, $b->discoGb, $b->grafica, $b->wifi, $b->sistema);
}
public function __toString(): string {
return "{$this->procesador}, {$this->ram} GB de RAM, {$this->discoGb} GB de disco"
. ($this->grafica !== null ? ", gráfica {$this->grafica}" : "")
. ($this->wifi ? ", wifi" : "") . ", {$this->sistema}";
}
}
final class OrdenadorBuilder {
public int $discoGb = 512; // opcionales, con su valor por defecto
public ?string $grafica = null;
public bool $wifi = false;
public string $sistema = "sin sistema operativo";
public function __construct(public string $procesador, public int $ram) {} // obligatorios
// Cada método devuelve el propio builder: así las llamadas se encadenan
public function disco(int $gb): self { $this->discoGb = $gb; return $this; }
public function grafica(string $modelo): self { $this->grafica = $modelo; return $this; }
public function conWifi(): self { $this->wifi = true; return $this; }
public function sistema(string $nombre): self { $this->sistema = $nombre; return $this; }
/** Valida una sola vez, con todo ya elegido, y crea el objeto. */
public function build(): Ordenador {
if (!in_array($this->ram, [8, 16, 32, 64], true)) throw new LogicException("RAM no válida: {$this->ram} GB");
if ($this->grafica !== null && $this->ram < 16) throw new LogicException("con gráfica dedicada hacen falta 16 GB de RAM");
return Ordenador::desde($this);
}
}
$oficina = Ordenador::builder("Ryzen 5", 16)->build();
$juegos = Ordenador::builder("Core i7", 32)
->disco(2000)
->grafica("RTX 4070")
->conWifi()
->sistema("Windows 11")
->build();
echo "Oficina: $oficina\n";
echo "Juegos: $juegos\n";
try {
Ordenador::builder("Celeron", 8)->grafica("RTX 4090")->build();
} catch (LogicException $e) {
echo "No se crea: ", $e->getMessage(), "\n";
}Salida al ejecutarlo (la misma en los 5 lenguajes)
Oficina: Ryzen 5, 16 GB de RAM, 512 GB de disco, sin sistema operativo Juegos: Core i7, 32 GB de RAM, 2000 GB de disco, gráfica RTX 4070, wifi, Windows 11 No se crea: con gráfica dedicada hacen falta 16 GB de RAM
Los builders que ya usas
La biblioteca de Java, Android y Lombok están llenos de builders.
1// StringBuilder construye un String paso a paso (un String no se puede modificar)
2String linea = new StringBuilder().append("Ana").append(';').append(8.5).toString();
3
4// java.net.http: una petición con muchos parámetros opcionales
5HttpRequest peticion = HttpRequest.newBuilder(URI.create("https://api.ejemplo.com/alumnos"))
6 .header("Accept", "application/json")
7 .timeout(Duration.ofSeconds(10))
8 .GET()
9 .build();
10
11// Android: los diálogos se construyen igual
12new AlertDialog.Builder(this)
13 .setTitle("¿Borrar la nota?")
14 .setPositiveButton("Borrar", (d, which) -> borrar())
15 .setNegativeButton("Cancelar", null)
16 .show();
17
18// Con Lombok, @Builder escribe la clase Builder por ti
19@Builder
20class Alumno { String nombre; String email; int curso; }
21Alumno a = Alumno.builder().nombre("Ana").curso(1).build();# En Python se usan sobre todo parámetros con nombre y valores por defecto: cubren casi lo mismo
@dataclass(frozen=True)
class Alumno:
nombre: str
email: str = ""
curso: int = 1
a = Alumno("Ana", curso=2)
# Builders de verdad en las bibliotecas de consultas (SQLAlchemy):
consulta = select(Alumno).where(Alumno.nota >= 5).order_by(Alumno.nota.desc()).limit(10)
# Para montar textos largos, "".join(...) en lugar de sumar en un bucle
linea = ";".join(["Ana", "DAM", "8.5"])// En JavaScript lo habitual es un objeto de opciones con valores por defecto
function crearOrdenador({ procesador, ram, disco = 512, wifi = false, sistema = "sin sistema operativo" }) {
return { procesador, ram, disco, wifi, sistema };
}
crearOrdenador({ procesador: "Ryzen 5", ram: 16, wifi: true });
// La plataforma también tiene builders: URL y URLSearchParams montan una dirección paso a paso
const url = new URL("https://api.ejemplo.com/alumnos");
url.searchParams.append("ciclo", "DAM");
url.searchParams.append("orden", "nota");
// Y en Node, los query builders como Knex:
const aprobados = await knex("alumnos").where("nota", ">=", 5).orderBy("nota", "desc").limit(10);// StringBuilder, el builder de toda la vida
var sb = new StringBuilder().Append("Ana").Append(';').Append("DAM");
// ASP.NET Core arranca con un builder
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
// Para objetos sencillos, C# tiene inicializadores y argumentos con nombre
var alumno = new Alumno { Nombre = "Ana", Curso = 2 };
var pc = new Ordenador("Ryzen 5", ram: 16, wifi: true);<?php
// El query builder de Laravel es un builder: se encadena y get() hace de build()
$aprobados = DB::table("alumnos")
->where("nota", ">=", 5)
->orderBy("nota", "desc")
->limit(10)
->get();
// Desde PHP 8, los argumentos con nombre evitan muchos builders pequeños
$pc = new Ordenador(procesador: "Ryzen 5", ram: 16, wifi: true);En la práctica
StringBuilderyStringJoinerpara montar textos;Stream.builder()para streams.HttpRequest.newBuilder()yHttpClient.newBuilder()en Java;Request.Builderde OkHttp yRetrofit.Builderen Android.AlertDialog.BuilderyNotificationCompat.Builderen Android;WebApplication.CreateBuilder()en ASP.NET Core.- Los constructores de consultas: el query builder de Laravel, el
CriteriaBuilderde JPA o SQLAlchemy encadenanwhere,orderByylimit.
Errores típicos
- Validar en cada método del builder en vez de en
build(): las reglas que dependen de varios valores solo se pueden comprobar al final. - Dejar públicos los setters o el constructor del producto: el objeto se puede crear o cambiar sin pasar por las comprobaciones.
- Reutilizar el mismo builder para dos objetos sin darse cuenta: lo que quedó del primero se cuela en el segundo.
- Usar un builder para clases de dos atributos: añade una clase entera sin aportar nada.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Un constructor de consultas SQL
El programa lee una consulta línea a línea y, al llegar a fin, la construye con el builder de Consulta y escribe el SQL. Los métodos del builder no guardan nada y build() solo sabe escribir SELECT * FROM tabla. Complétalos.
consulta tabla,select a, b,where condición(varias se unen conAND),orden campo [asc|desc](varios se separan con coma;descse escribeDESCyascno se escribe),limite nyfin.- Salida:
SELECT columnas FROM tabla WHERE c1 AND c2 ORDER BY o1, o2 LIMIT n, poniendo solo las partes que se han pedido; sinselect, las columnas son*. Después definempieza una consulta nueva. build()lanza unaIllegalStateExceptionconFalta la tablaoLIMIT no válido: n(menor que 1), y elmainescribeError: mensaje.
Ejemplo
consulta alumnos select nombre, nota where nota >= 5 where ciclo = 'DAM' orden nota desc orden nombre limite 10 fin consulta ciclos fin
SELECT nombre, nota FROM alumnos WHERE nota >= 5 AND ciclo = 'DAM' ORDER BY nota DESC, nombre LIMIT 10 SELECT * FROM ciclos
Ver la solución explicada
1import java.util.*;
2
3/** Una consulta SELECT ya construida (y validada). */
4class Consulta {
5 private final String sql;
6 private Consulta(String sql) { this.sql = sql; }
7 String sql() { return sql; }
8
9 static Builder builder() { return new Builder(); }
10
11 static class Builder {
12 private String tabla;
13 private final List<String> columnas = new ArrayList<>();
14 private final List<String> condiciones = new ArrayList<>();
15 private final List<String> orden = new ArrayList<>();
16 private Integer limite;
17
18 Builder from(String tabla) { this.tabla = tabla; return this; }
19 Builder select(List<String> cols) { columnas.addAll(cols); return this; }
20 Builder where(String condicion) { condiciones.add(condicion); return this; }
21 Builder orderBy(String campo, boolean descendente) { orden.add(descendente ? campo + " DESC" : campo); return this; }
22 Builder limit(int n) { limite = n; return this; }
23
24 Consulta build() {
25 if (tabla == null) throw new IllegalStateException("Falta la tabla");
26 if (limite != null && limite < 1) throw new IllegalStateException("LIMIT no válido: " + limite);
27 StringBuilder sb = new StringBuilder("SELECT ");
28 sb.append(columnas.isEmpty() ? "*" : String.join(", ", columnas));
29 sb.append(" FROM ").append(tabla);
30 if (!condiciones.isEmpty()) sb.append(" WHERE ").append(String.join(" AND ", condiciones));
31 if (!orden.isEmpty()) sb.append(" ORDER BY ").append(String.join(", ", orden));
32 if (limite != null) sb.append(" LIMIT ").append(limite);
33 return new Consulta(sb.toString());
34 }
35 }
36}
37
38public class Main {
39 public static void main(String[] args) {
40 Scanner sc = new Scanner(System.in);
41 Consulta.Builder b = Consulta.builder();
42 while (sc.hasNextLine()) {
43 String linea = sc.nextLine().trim();
44 if (linea.isEmpty()) continue;
45 int esp = linea.indexOf(' ');
46 String orden = esp < 0 ? linea : linea.substring(0, esp);
47 String resto = esp < 0 ? "" : linea.substring(esp + 1).trim();
48 switch (orden) {
49 case "consulta" -> b.from(resto);
50 case "select" -> {
51 List<String> cols = new ArrayList<>();
52 for (String c : resto.split(",")) if (!c.isBlank()) cols.add(c.trim());
53 if (cols.isEmpty()) System.out.println("Orden no válida: " + linea);
54 else b.select(cols);
55 }
56 case "where" -> b.where(resto);
57 case "orden" -> {
58 String[] p = resto.split("\\s+");
59 if (p.length == 1 && !p[0].isEmpty()) b.orderBy(p[0], false);
60 else if (p.length == 2 && (p[1].equals("asc") || p[1].equals("desc"))) b.orderBy(p[0], p[1].equals("desc"));
61 else System.out.println("Orden no válida: " + linea);
62 }
63 case "limite" -> {
64 try {
65 b.limit(Integer.parseInt(resto));
66 } catch (NumberFormatException e) {
67 System.out.println("Orden no válida: " + linea);
68 }
69 }
70 case "fin" -> {
71 try {
72 System.out.println(b.build().sql());
73 } catch (IllegalStateException e) {
74 System.out.println("Error: " + e.getMessage());
75 }
76 b = Consulta.builder();
77 }
78 default -> System.out.println("Orden no válida: " + linea);
79 }
80 }
81 }
82}El orden de las líneas no importa: el builder guarda las piezas y build() las coloca en el orden que exige SQL.
Las comprobaciones están en un solo sitio, build(), y una consulta que no vale no llega a crearse.
2. Billetes de tren con reglas
Los billetes se construyen con un builder: el origen y el destino al empezar y el resto con un método por opción. Completa en el builder precio() y build(), que debe rechazar los billetes que no cumplen las reglas. La lectura de órdenes y la salida ya están.
- Un billete empieza con
billete origen destinoy termina confin; entre medias,clase turista|preferente,asiento 5A,maleta(una por línea),mascota,bicicletayjoven. - Precio: 20 € en turista y 35 € en preferente; la primera maleta va incluida y cada una más cuesta 5 €; la mascota, 10 €; la bicicleta, 8 €; y
jovendescuenta un 20 % del total. - Reglas (en este orden), con
IllegalStateException:el origen y el destino son el mismo(sin distinguir mayúsculas),la clase X no existe,el asiento X no existe(fila del 1 al 20 y letra de la A a la D),se admiten 3 maletas como muchoylas bicicletas solo van en turista.
Ejemplo
billete Madrid Sevilla fin billete Barcelona Valencia clase preferente asiento 5A maleta maleta mascota joven fin billete Bilbao Burgos maleta bicicleta fin
Madrid → Sevilla · turista: 20,00 € Barcelona → Valencia · preferente · asiento 5A · 2 maletas · mascota · joven: 40,00 € Bilbao → Burgos · turista · 1 maleta · bicicleta: 28,00 €
Ver la solución explicada
1import java.util.*;
2
3/** Un billete de tren, inmutable: solo se crea con su builder. */
4class Billete {
5 private final String origen, destino, clase, asiento;
6 private final int maletas;
7 private final boolean mascota, bicicleta, joven;
8 private final double precio;
9
10 private Billete(Builder b) {
11 origen = b.origen;
12 destino = b.destino;
13 clase = b.clase;
14 asiento = b.asiento;
15 maletas = b.maletas;
16 mascota = b.mascota;
17 bicicleta = b.bicicleta;
18 joven = b.joven;
19 precio = b.precio();
20 }
21
22 static Builder de(String origen, String destino) { return new Builder(origen, destino); }
23
24 @Override
25 public String toString() {
26 StringBuilder sb = new StringBuilder(origen + " → " + destino + " · " + clase);
27 if (asiento != null) sb.append(" · asiento ").append(asiento);
28 if (maletas > 0) sb.append(" · ").append(maletas).append(maletas == 1 ? " maleta" : " maletas");
29 if (mascota) sb.append(" · mascota");
30 if (bicicleta) sb.append(" · bicicleta");
31 if (joven) sb.append(" · joven");
32 return sb + ": " + Main.euros(precio);
33 }
34
35 static class Builder {
36 private final String origen, destino;
37 private String clase = "turista";
38 private String asiento;
39 private int maletas;
40 private boolean mascota, bicicleta, joven;
41
42 private Builder(String origen, String destino) {
43 this.origen = origen;
44 this.destino = destino;
45 }
46
47 String trayecto() { return origen + " → " + destino; }
48
49 Builder clase(String clase) { this.clase = clase; return this; }
50 Builder asiento(String asiento) { this.asiento = asiento; return this; }
51 Builder maleta() { maletas++; return this; }
52 Builder mascota() { mascota = true; return this; }
53 Builder bicicleta() { bicicleta = true; return this; }
54 Builder joven() { joven = true; return this; }
55
56 double precio() {
57 double p = clase.equals("preferente") ? 35 : 20;
58 if (maletas > 1) p += 5 * (maletas - 1); // la primera maleta va incluida
59 if (mascota) p += 10;
60 if (bicicleta) p += 8;
61 if (joven) p *= 0.8; // el descuento, al final
62 return p;
63 }
64
65 Billete build() {
66 if (origen.equalsIgnoreCase(destino)) throw new IllegalStateException("el origen y el destino son el mismo");
67 if (!clase.equals("turista") && !clase.equals("preferente")) throw new IllegalStateException("la clase " + clase + " no existe");
68 if (asiento != null && !asiento.matches("([1-9]|1[0-9]|20)[A-D]")) throw new IllegalStateException("el asiento " + asiento + " no existe");
69 if (maletas > 3) throw new IllegalStateException("se admiten 3 maletas como mucho");
70 if (bicicleta && clase.equals("preferente")) throw new IllegalStateException("las bicicletas solo van en turista");
71 return new Billete(this);
72 }
73 }
74}
75
76public class Main {
77 static String euros(double x) {
78 return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
79 }
80
81 public static void main(String[] args) {
82 Scanner sc = new Scanner(System.in);
83 Billete.Builder b = null;
84 while (sc.hasNextLine()) {
85 String[] p = sc.nextLine().trim().split("\\s+");
86 if (p[0].isEmpty()) continue;
87 if (p[0].equals("billete")) {
88 if (b != null) System.out.println("Billete sin terminar: " + b.trayecto());
89 b = p.length == 3 ? Billete.de(p[1], p[2]) : null;
90 if (b == null) System.out.println("Orden no válida: " + String.join(" ", p));
91 continue;
92 }
93 if (b == null) {
94 System.out.println("Empieza con billete origen destino");
95 continue;
96 }
97 switch (p.length == 1 ? p[0] : p[0] + ":") {
98 case "clase:" -> b.clase(p[1]);
99 case "asiento:" -> b.asiento(p[1]);
100 case "maleta" -> b.maleta();
101 case "mascota" -> b.mascota();
102 case "bicicleta" -> b.bicicleta();
103 case "joven" -> b.joven();
104 case "fin" -> {
105 try {
106 System.out.println(b.build());
107 } catch (IllegalStateException e) {
108 System.out.println("Billete no válido: " + e.getMessage());
109 }
110 b = null;
111 }
112 default -> System.out.println("Orden no válida: " + String.join(" ", p));
113 }
114 }
115 if (b != null) System.out.println("Billete sin terminar: " + b.trayecto());
116 }
117}Las reglas que mezclan opciones (bicicleta y clase) solo se pueden comprobar cuando ya se ha elegido todo: por eso van en build() y no en cada método.
Como el constructor de Billete es privado y sus campos final, no puede existir un billete sin validar ni cambiar después.
Test
Test: Builder
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Qué devuelve cada método de opción de un builder (disco, conWifi…)?
2.¿Dónde se comprueban las reglas que dependen de varios parámetros?
3.¿Qué problema resuelve frente a un constructor con muchos parámetros?
4.¿Cuál de estas clases de Java es un builder?
5.¿Por qué el constructor del producto suele ser privado?