Patrón Repository
Una interfaz que se usa como una colección de objetos del dominio (guardar, buscar, consultas del negocio) y esconde dónde y cómo se guardan. Es lo que dan Spring Data, Room o Entity Framework.
nivel intermedioTambién: repositorio, Spring Data, JpaRepository
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.
Repository
Escribe las consultas al repositorio separadas por comas: pendientes cliente, caros importe o enviar id (que guarda el pedido cambiado).
Paso 1
El servicio pregunta en términos del negocio: pendientesDe("Ana"). No hay ni SQL ni tablas a la vista.
1default List<Pedido> pendientesDe(String cliente) { // consulta = pendientesDe("Ana")
2 return buscar(p -> p.cliente().equals(cliente)
3 && p.estado() == Estado.PENDIENTE);
4}
5
6public List<Pedido> buscar(Predicate<Pedido> criterio) {
7 List<Pedido> r = new ArrayList<>();
8 for (Pedido p : pedidos.values())
9 if (criterio.test(p)) r.add(p);
10 return r;
11}
12
13public void guardar(Pedido p) { pedidos.put(p.id(), p); }Variables
- consulta
- pendientesDe("Ana")
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
El DAO piensa en tablas: una operación por cada cosa que se hace con las filas. El repositorio piensa en el dominio: se comporta como una colección de pedidos o de reservas a la que se le añaden objetos y se le piden los que cumplen algo que tiene sentido en el negocio, como «los pedidos pendientes de Ana» o «las habitaciones ocupadas esta noche».
Un repositorio suele tener una parte genérica, igual para todas las entidades (guardar, porId, buscar con un criterio), y una parte propia de cada una, con consultas que se llaman como las nombraría un usuario (pendientesDe(cliente), masCarosQue(importe)). El servicio usa esas consultas y no sabe si debajo hay SQL, JPA, un fichero o una lista en memoria.
En los frameworks modernos el repositorio ya viene hecho: en Spring Data JPA basta con declarar la interfaz PedidoRepository extends JpaRepository<Pedido, Long> y Spring escribe la implementación, incluidas las consultas que se deducen del nombre del método (findByClienteAndEstado). Room en Android y Entity Framework en .NET siguen la misma idea.
Cuándo usarlo
- Aplicaciones con un dominio rico (pedidos, reservas, matrículas) donde las consultas tienen significado de negocio.
- Proyectos con un ORM (JPA, Hibernate, Room, Entity Framework, Doctrine), que ya trabajan con objetos y no con filas.
- Quieres probar los servicios con un repositorio en memoria.
Cuándo no
- Si la aplicación es poco más que un CRUD de tablas: un DAO o el acceso directo del framework es más simple.
- Si las consultas son informes complejos con muchas tablas: mejor una consulta específica que forzarlas a parecer una colección.
Participantes
- Entidad. El objeto del dominio con su identidad (
Pedidocon suid). - Repositorio genérico. Las operaciones de colección comunes a todas las entidades (
Repositorio<T, ID>). - Repositorio de la entidad. Añade las consultas del negocio (
RepositorioPedidos.pendientesDe). - Implementación. En memoria, con JDBC o generada por el framework (
RepositorioPedidosMemoria, Spring Data). - Servicio. Usa el repositorio para aplicar las reglas del negocio.
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2interface Repositorio {
3 +guardar(entidad : T) : void
4 +porId(id : ID) : Optional<T>
5 +buscar(criterio : Predicate<T>) : List<T>
6}
7interface RepositorioPedidos {
8 +pendientesDe(cliente : String) : List<Pedido>
9 +masCarosQue(importe : double) : List<Pedido>
10}
11class RepositorioPedidosMemoria {
12 -pedidos : Map<Integer, Pedido>
13}
14class Pedido {
15 +id : int
16 +cliente : String
17 +total : double
18 +estado : Estado
19}
20class ServicioPedidos
21Repositorio <|-- RepositorioPedidos
22RepositorioPedidos <|.. RepositorioPedidosMemoria
23RepositorioPedidosMemoria o-- "0..*" Pedido : pedidos
24ServicioPedidos --> "1" RepositorioPedidos : repo
25@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Un repositorio de pedidos
Las consultas del negocio son métodos por defecto de la interfaz que se apoyan en buscar: cualquier implementación las tiene gratis.
1import java.util.*;
2import java.util.function.Predicate;
3
4enum Estado { PENDIENTE, ENVIADO, ENTREGADO }
5
6record Pedido(int id, String cliente, double total, Estado estado) {}
7
8/** Un repositorio genérico: se usa como una colección de objetos del dominio. */
9interface Repositorio<T, ID> {
10 void guardar(T entidad); // inserta o, si ya existe, sustituye
11 Optional<T> porId(ID id);
12 List<T> buscar(Predicate<T> criterio);
13}
14
15/** El de pedidos añade consultas con nombre del negocio, no de tablas ni de SQL. */
16interface RepositorioPedidos extends Repositorio<Pedido, Integer> {
17 default List<Pedido> pendientesDe(String cliente) {
18 return buscar(p -> p.cliente().equals(cliente) && p.estado() == Estado.PENDIENTE);
19 }
20
21 default List<Pedido> masCarosQue(double importe) {
22 return buscar(p -> p.total() > importe);
23 }
24}
25
26/** En memoria para el ejemplo; con JPA, Spring Data escribiría esta clase por ti. */
27class RepositorioPedidosMemoria implements RepositorioPedidos {
28 private final Map<Integer, Pedido> pedidos = new TreeMap<>();
29
30 public void guardar(Pedido p) { pedidos.put(p.id(), p); }
31 public Optional<Pedido> porId(Integer id) { return Optional.ofNullable(pedidos.get(id)); }
32
33 public List<Pedido> buscar(Predicate<Pedido> criterio) {
34 List<Pedido> r = new ArrayList<>();
35 for (Pedido p : pedidos.values()) if (criterio.test(p)) r.add(p);
36 return r;
37 }
38}
39
40public class Main {
41 static String ids(List<Pedido> pedidos) {
42 List<String> r = new ArrayList<>();
43 for (Pedido p : pedidos) r.add("#" + p.id());
44 return r.isEmpty() ? "ninguno" : String.join(", ", r);
45 }
46
47 public static void main(String[] args) {
48 RepositorioPedidos repo = new RepositorioPedidosMemoria();
49 repo.guardar(new Pedido(1, "Ana", 45.0, Estado.PENDIENTE));
50 repo.guardar(new Pedido(2, "Luis", 120.5, Estado.ENVIADO));
51 repo.guardar(new Pedido(3, "Ana", 230.0, Estado.PENDIENTE));
52 repo.guardar(new Pedido(4, "Ana", 15.0, Estado.ENTREGADO));
53 System.out.println("Pendientes de Ana: " + ids(repo.pendientesDe("Ana")));
54 System.out.println("Más de 100 €: " + ids(repo.masCarosQue(100)));
55 repo.guardar(new Pedido(1, "Ana", 45.0, Estado.ENVIADO)); // mismo id: se actualiza
56 System.out.println("Pendientes de Ana tras enviar el #1: " + ids(repo.pendientesDe("Ana")));
57 System.out.println("Pedido #2: " + repo.porId(2).map(p -> p.cliente() + ", " + p.estado()).orElse("no existe"));
58 }
59}from dataclasses import dataclass
from enum import Enum
class Estado(Enum):
PENDIENTE = "PENDIENTE"
ENVIADO = "ENVIADO"
ENTREGADO = "ENTREGADO"
@dataclass(frozen=True)
class Pedido:
id: int
cliente: str
total: float
estado: Estado
class RepositorioPedidos:
"""Se usa como una colección de pedidos, con consultas que se llaman como las del negocio."""
def __init__(self):
self._pedidos = {}
def guardar(self, p): # inserta o, si ya existe, sustituye
self._pedidos[p.id] = p
def por_id(self, id):
return self._pedidos.get(id)
def buscar(self, criterio):
return [p for _, p in sorted(self._pedidos.items()) if criterio(p)]
def pendientes_de(self, cliente):
return self.buscar(lambda p: p.cliente == cliente and p.estado == Estado.PENDIENTE)
def mas_caros_que(self, importe):
return self.buscar(lambda p: p.total > importe)
def ids(pedidos):
return ", ".join(f"#{p.id}" for p in pedidos) or "ninguno"
repo = RepositorioPedidos()
repo.guardar(Pedido(1, "Ana", 45.0, Estado.PENDIENTE))
repo.guardar(Pedido(2, "Luis", 120.5, Estado.ENVIADO))
repo.guardar(Pedido(3, "Ana", 230.0, Estado.PENDIENTE))
repo.guardar(Pedido(4, "Ana", 15.0, Estado.ENTREGADO))
print(f"Pendientes de Ana: {ids(repo.pendientes_de('Ana'))}")
print(f"Más de 100 €: {ids(repo.mas_caros_que(100))}")
repo.guardar(Pedido(1, "Ana", 45.0, Estado.ENVIADO)) # mismo id: se actualiza
print(f"Pendientes de Ana tras enviar el #1: {ids(repo.pendientes_de('Ana'))}")
p = repo.por_id(2)
print(f"Pedido #2: {p.cliente + ', ' + p.estado.value if p else 'no existe'}")/** Se usa como una colección de pedidos, con consultas que se llaman como las del negocio. */
class RepositorioPedidos {
#pedidos = new Map();
guardar(p) { this.#pedidos.set(p.id, p); } // inserta o, si ya existe, sustituye
porId(id) { return this.#pedidos.get(id) ?? null; }
buscar(criterio) {
return [...this.#pedidos.values()].sort((a, b) => a.id - b.id).filter(criterio);
}
pendientesDe(cliente) { return this.buscar((p) => p.cliente === cliente && p.estado === "PENDIENTE"); }
masCarosQue(importe) { return this.buscar((p) => p.total > importe); }
}
const ids = (pedidos) => pedidos.map((p) => `#${p.id}`).join(", ") || "ninguno";
const repo = new RepositorioPedidos();
repo.guardar({ id: 1, cliente: "Ana", total: 45.0, estado: "PENDIENTE" });
repo.guardar({ id: 2, cliente: "Luis", total: 120.5, estado: "ENVIADO" });
repo.guardar({ id: 3, cliente: "Ana", total: 230.0, estado: "PENDIENTE" });
repo.guardar({ id: 4, cliente: "Ana", total: 15.0, estado: "ENTREGADO" });
console.log(`Pendientes de Ana: ${ids(repo.pendientesDe("Ana"))}`);
console.log(`Más de 100 €: ${ids(repo.masCarosQue(100))}`);
repo.guardar({ id: 1, cliente: "Ana", total: 45.0, estado: "ENVIADO" }); // mismo id: se actualiza
console.log(`Pendientes de Ana tras enviar el #1: ${ids(repo.pendientesDe("Ana"))}`);
const p = repo.porId(2);
console.log(`Pedido #2: ${p ? `${p.cliente}, ${p.estado}` : "no existe"}`);using System;
using System.Collections.Generic;
using System.Linq;
enum Estado { PENDIENTE, ENVIADO, ENTREGADO }
record Pedido(int Id, string Cliente, decimal Total, Estado Estado);
// Un repositorio genérico: se usa como una colección de objetos del dominio.
interface IRepositorio<T, TId> {
void Guardar(T entidad);
T? PorId(TId id);
List<T> Buscar(Func<T, bool> criterio);
}
// El de pedidos añade consultas con nombre del negocio (métodos por defecto de la interfaz).
interface IRepositorioPedidos : IRepositorio<Pedido, int> {
List<Pedido> PendientesDe(string cliente) => Buscar(p => p.Cliente == cliente && p.Estado == Estado.PENDIENTE);
List<Pedido> MasCarosQue(decimal importe) => Buscar(p => p.Total > importe);
}
class RepositorioPedidosMemoria : IRepositorioPedidos {
private readonly SortedDictionary<int, Pedido> pedidos = new();
public void Guardar(Pedido p) => pedidos[p.Id] = p;
public Pedido? PorId(int id) => pedidos.GetValueOrDefault(id);
public List<Pedido> Buscar(Func<Pedido, bool> criterio) => pedidos.Values.Where(criterio).ToList();
}
class Program {
static string Ids(List<Pedido> pedidos) => pedidos.Count == 0 ? "ninguno" : string.Join(", ", pedidos.Select(p => quot;#{p.Id}"));
static void Main() {
IRepositorioPedidos repo = new RepositorioPedidosMemoria();
repo.Guardar(new Pedido(1, "Ana", 45.0m, Estado.PENDIENTE));
repo.Guardar(new Pedido(2, "Luis", 120.5m, Estado.ENVIADO));
repo.Guardar(new Pedido(3, "Ana", 230.0m, Estado.PENDIENTE));
repo.Guardar(new Pedido(4, "Ana", 15.0m, Estado.ENTREGADO));
Console.WriteLine(quot;Pendientes de Ana: {Ids(repo.PendientesDe("Ana"))}");
Console.WriteLine(quot;Más de 100 €: {Ids(repo.MasCarosQue(100))}");
repo.Guardar(new Pedido(1, "Ana", 45.0m, Estado.ENVIADO)); // mismo id: se actualiza
Console.WriteLine(quot;Pendientes de Ana tras enviar el #1: {Ids(repo.PendientesDe("Ana"))}");
var p = repo.PorId(2);
Console.WriteLine(quot;Pedido #2: {(p != null ? quot;{p.Cliente}, {p.Estado}" : "no existe")}");
}
}<?php
enum Estado: string { case PENDIENTE = "PENDIENTE"; case ENVIADO = "ENVIADO"; case ENTREGADO = "ENTREGADO"; }
final class Pedido {
public function __construct(public int $id, public string $cliente, public float $total, public Estado $estado) {}
}
/** Se usa como una colección de pedidos, con consultas que se llaman como las del negocio. */
class RepositorioPedidos {
private array $pedidos = [];
public function guardar(Pedido $p): void { $this->pedidos[$p->id] = $p; ksort($this->pedidos); }
public function porId(int $id): ?Pedido { return $this->pedidos[$id] ?? null; }
public function buscar(callable $criterio): array {
return array_values(array_filter($this->pedidos, $criterio));
}
public function pendientesDe(string $cliente): array {
return $this->buscar(fn (Pedido $p) => $p->cliente === $cliente && $p->estado === Estado::PENDIENTE);
}
public function masCarosQue(float $importe): array {
return $this->buscar(fn (Pedido $p) => $p->total > $importe);
}
}
function ids(array $pedidos): string {
return $pedidos ? implode(", ", array_map(fn (Pedido $p) => "#{$p->id}", $pedidos)) : "ninguno";
}
$repo = new RepositorioPedidos();
$repo->guardar(new Pedido(1, "Ana", 45.0, Estado::PENDIENTE));
$repo->guardar(new Pedido(2, "Luis", 120.5, Estado::ENVIADO));
$repo->guardar(new Pedido(3, "Ana", 230.0, Estado::PENDIENTE));
$repo->guardar(new Pedido(4, "Ana", 15.0, Estado::ENTREGADO));
echo "Pendientes de Ana: ", ids($repo->pendientesDe("Ana")), "\n";
echo "Más de 100 €: ", ids($repo->masCarosQue(100)), "\n";
$repo->guardar(new Pedido(1, "Ana", 45.0, Estado::ENVIADO)); // mismo id: se actualiza
echo "Pendientes de Ana tras enviar el #1: ", ids($repo->pendientesDe("Ana")), "\n";
$p = $repo->porId(2);
echo "Pedido #2: ", $p ? "{$p->cliente}, {$p->estado->value}" : "no existe", "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Pendientes de Ana: #1, #3 Más de 100 €: #2, #3 Pendientes de Ana tras enviar el #1: #3 Pedido #2: Luis, ENVIADO
Spring Data JPA
Se declara la interfaz y Spring escribe la clase, incluidas las consultas que deduce del nombre de cada método.
1// Con Spring Data JPA se declara la interfaz y Spring escribe la implementación:
2// las consultas salen del nombre de los métodos.
3public interface PedidoRepository extends JpaRepository<Pedido, Long> {
4 List<Pedido> findByClienteAndEstado(String cliente, Estado estado);
5 List<Pedido> findByTotalGreaterThanOrderByTotalDesc(BigDecimal importe);
6
7 @Query("SELECT p FROM Pedido p WHERE p.fecha >= :desde")
8 List<Pedido> recientes(@Param("desde") LocalDate desde);
9}
10
11// Y el servicio lo recibe ya hecho:
12@Service
13public class PedidoService {
14 private final PedidoRepository repo;
15 public PedidoService(PedidoRepository repo) { this.repo = repo; }
16
17 public List<Pedido> pendientes(String cliente) {
18 return repo.findByClienteAndEstado(cliente, Estado.PENDIENTE);
19 }
20}# SQLAlchemy: la sesión y select() hacen de repositorio; se suele envolver en una clase propia
class RepositorioPedidos:
def __init__(self, sesion):
self._sesion = sesion
def guardar(self, pedido):
self._sesion.add(pedido)
def pendientes_de(self, cliente):
consulta = select(Pedido).where(Pedido.cliente == cliente, Pedido.estado == "PENDIENTE")
return self._sesion.scalars(consulta).all()// TypeORM: el repositorio de cada entidad viene hecho
const repo = dataSource.getRepository(Pedido);
await repo.save({ cliente: "Ana", total: 45, estado: "PENDIENTE" });
const pendientes = await repo.findBy({ cliente: "Ana", estado: "PENDIENTE" });
const caros = await repo.find({ where: { total: MoreThan(100) }, order: { total: "DESC" } });// Entity Framework Core: cada DbSet<T> es un repositorio, y las consultas se escriben con LINQ
public class TiendaContext : DbContext {
public DbSet<Pedido> Pedidos => Set<Pedido>();
}
var pendientes = await contexto.Pedidos
.Where(p => p.Cliente == "Ana" && p.Estado == Estado.PENDIENTE)
.OrderBy(p => p.Id)
.ToListAsync();<?php
// Doctrine (Symfony): cada entidad tiene su repositorio, que se puede ampliar con consultas propias
class PedidoRepository extends ServiceEntityRepository {
public function pendientesDe(string $cliente): array {
return $this->findBy(["cliente" => $cliente, "estado" => "PENDIENTE"], ["id" => "ASC"]);
}
}En la práctica
- Spring Data JPA (
JpaRepository,CrudRepository) y Spring Data MongoDB. - Room en Android (con
@Dao) y los repositorios de la arquitectura recomendada por Google, que eligen entre la red y la base de datos local. - Entity Framework (
DbSet<T>hace de repositorio), Doctrine en Symfony y los repositorios de Laravel. - Los repositorios en memoria para probar los servicios sin base de datos.
Errores típicos
- Un repositorio que devuelve filas,
ResultSeto arrays sueltos en lugar de objetos del dominio. - Un método de consulta por cada combinación posible de filtros: mejor un
buscarcon criterio o especificaciones. - Poner reglas del negocio en el repositorio (no se puede reservar una habitación ocupada): eso es del servicio, que usa las consultas del repositorio.
- Confundirlo con el DAO hasta el punto de tener los dos haciendo lo mismo: en una aplicación con ORM, normalmente basta con el repositorio.
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 repositorio genérico
Escribe RepositorioMemoria<T extends Entidad>, un repositorio en memoria que sirve para cualquier entidad con id. El main lo usa con libros y le pide consultas con criterios distintos sin que el repositorio sepa nada de libros.
- Órdenes:
libro id;título;autor;año(escribeNuevo: títulooActualizado: títulosi el id ya existía),ver id,autor texto,antes año,quitar idytodos. - Las listas salen ordenadas por id.
buscardevuelve las entidades que cumplen elPredicateque recibe.
Ejemplo
libro 3;Fortunata y Jacinta;Benito Pérez Galdós;1887 libro 1;La Regenta;Leopoldo Alas;1884 libro 2;Niebla;Miguel de Unamuno;1914 autor galdós antes 1900 todos
Nuevo: Fortunata y Jacinta Nuevo: La Regenta Nuevo: Niebla De galdós: 3. Fortunata y Jacinta — Benito Pérez Galdós (1887) Anteriores a 1900: 1. La Regenta — Leopoldo Alas (1884) 3. Fortunata y Jacinta — Benito Pérez Galdós (1887) Todos: 1. La Regenta — Leopoldo Alas (1884) 2. Niebla — Miguel de Unamuno (1914) 3. Fortunata y Jacinta — Benito Pérez Galdós (1887)
Ver la solución explicada
1import java.util.*;
2import java.util.function.Predicate;
3
4/** Lo que tiene cualquier objeto que se guarda en un repositorio: un id. */
5interface Entidad {
6 int id();
7}
8
9record Libro(int id, String titulo, String autor, int anio) implements Entidad {}
10
11/** Un repositorio en memoria que vale para cualquier entidad. */
12class RepositorioMemoria<T extends Entidad> {
13 private final Map<Integer, T> datos = new TreeMap<>();
14
15 /** Inserta o, si ya hay una con ese id, la sustituye. Devuelve true si era nueva. */
16 boolean guardar(T e) { return datos.put(e.id(), e) == null; }
17
18 Optional<T> porId(int id) { return Optional.ofNullable(datos.get(id)); }
19
20 /** Todas, ordenadas por id. */
21 List<T> todos() { return new ArrayList<>(datos.values()); }
22
23 boolean borrar(int id) { return datos.remove(id) != null; }
24
25 /** Las que cumplen el criterio, ordenadas por id. */
26 List<T> buscar(Predicate<T> criterio) {
27 List<T> r = new ArrayList<>();
28 for (T e : datos.values()) if (criterio.test(e)) r.add(e);
29 return r;
30 }
31}
32
33public class Main {
34 static void escribir(List<Libro> libros) {
35 if (libros.isEmpty()) System.out.println(" (ninguno)");
36 for (Libro l : libros) System.out.println(" " + l.id() + ". " + l.titulo() + " — " + l.autor() + " (" + l.anio() + ")");
37 }
38
39 public static void main(String[] args) {
40 RepositorioMemoria<Libro> repo = new RepositorioMemoria<>();
41 Scanner sc = new Scanner(System.in);
42 while (sc.hasNextLine()) {
43 String linea = sc.nextLine().trim();
44 if (linea.isEmpty()) continue;
45 String[] p = linea.split("\\s+", 2);
46 String arg = p.length > 1 ? p[1].trim() : "";
47 try {
48 switch (p[0]) {
49 case "libro" -> {
50 String[] c = arg.split(";");
51 if (c.length != 4) throw new NumberFormatException();
52 Libro l = new Libro(Integer.parseInt(c[0].trim()), c[1].trim(), c[2].trim(), Integer.parseInt(c[3].trim()));
53 System.out.println((repo.guardar(l) ? "Nuevo: " : "Actualizado: ") + l.titulo());
54 }
55 case "ver" -> System.out.println(repo.porId(Integer.parseInt(arg)).map(l -> l.titulo() + " — " + l.autor()).orElse("No existe el libro " + arg));
56 case "autor" -> {
57 System.out.println("De " + arg + ":");
58 escribir(repo.buscar(l -> l.autor().toLowerCase().contains(arg.toLowerCase())));
59 }
60 case "antes" -> {
61 int anio = Integer.parseInt(arg);
62 System.out.println("Anteriores a " + anio + ":");
63 escribir(repo.buscar(l -> l.anio() < anio));
64 }
65 case "quitar" -> System.out.println(repo.borrar(Integer.parseInt(arg)) ? "Quitado" : "No existe el libro " + arg);
66 case "todos" -> {
67 System.out.println("Todos:");
68 escribir(repo.todos());
69 }
70 default -> System.out.println("Orden no válida: " + linea);
71 }
72 } catch (NumberFormatException e) {
73 System.out.println("Orden no válida: " + linea);
74 }
75 }
76 }
77}Con genéricos, la misma clase sirve para libros, socios o préstamos: es lo que hace JpaRepository<T, ID>.
Pasar el criterio como un Predicate evita escribir un método por cada consulta.
2. Las reservas de un hotel
La recepción del hotel usa el repositorio de reservas para no dar dos veces la misma habitación. Escribe las tres consultas del negocio de RepositorioReservas como métodos por defecto que se apoyan en buscar: solapadas, ocupadas y delCliente.
- Órdenes:
reservar habitación cliente entrada salida(fechasAAAA-MM-DD),cancelar id,ocupadas fechaycliente nombre. La recepción ya escribe los mensajes. - Una reserva ocupa las noches de
entradaasalida, sin incluir la de salida: dos reservas se solapan si una empieza antes de que termine la otra y al revés (a.entrada < b.salidayb.entrada < a.salida). Así, entrar el día que otro sale no es un choque. ocupadasdevuelve los números de habitación sin repetir y de menor a mayor;delCliente, las reservas ordenadas por entrada (y por id si empatan).
Ejemplo
reservar 101 Ana 2026-10-10 2026-10-13 reservar 101 Luis 2026-10-12 2026-10-14 reservar 101 Luis 2026-10-13 2026-10-15 reservar 102 Marta 2026-10-11 2026-10-12 ocupadas 2026-10-11 ocupadas 2026-10-13 cliente luis
Reserva #1: Ana en la 101 del 2026-10-10 al 2026-10-13 La 101 está ocupada (reserva #1) Reserva #2: Luis en la 101 del 2026-10-13 al 2026-10-15 Reserva #3: Marta en la 102 del 2026-10-11 al 2026-10-12 Ocupadas el 2026-10-11: 101, 102 Ocupadas el 2026-10-13: 101 Reservas de luis: #2 habitación 101: 2026-10-13 → 2026-10-15
Ver la solución explicada
1import java.time.LocalDate;
2import java.util.*;
3import java.util.function.Predicate;
4
5/** Una reserva: de la noche de entrada a la de salida, sin incluir la salida. */
6record Reserva(int id, int habitacion, String cliente, LocalDate entrada, LocalDate salida) {}
7
8interface RepositorioReservas {
9 void guardar(Reserva r);
10 boolean borrar(int id);
11 List<Reserva> buscar(Predicate<Reserva> criterio);
12
13 /** Las reservas de esa habitación que se cruzan con [desde, hasta). */
14 default List<Reserva> solapadas(int habitacion, LocalDate desde, LocalDate hasta) {
15 return buscar(r -> r.habitacion() == habitacion && r.entrada().isBefore(hasta) && desde.isBefore(r.salida()));
16 }
17
18 /** Las habitaciones ocupadas la noche de esa fecha, de menor a mayor. */
19 default List<Integer> ocupadas(LocalDate fecha) {
20 TreeSet<Integer> h = new TreeSet<>();
21 for (Reserva r : buscar(r -> !fecha.isBefore(r.entrada()) && fecha.isBefore(r.salida()))) h.add(r.habitacion());
22 return new ArrayList<>(h);
23 }
24
25 /** Las reservas de un cliente (sin distinguir mayúsculas), por fecha de entrada. */
26 default List<Reserva> delCliente(String cliente) {
27 List<Reserva> r = buscar(x -> x.cliente().equalsIgnoreCase(cliente));
28 r.sort(Comparator.comparing(Reserva::entrada).thenComparingInt(Reserva::id));
29 return r;
30 }
31}
32
33class RepositorioReservasMemoria implements RepositorioReservas {
34 private final Map<Integer, Reserva> datos = new TreeMap<>();
35 public void guardar(Reserva r) { datos.put(r.id(), r); }
36 public boolean borrar(int id) { return datos.remove(id) != null; }
37 public List<Reserva> buscar(Predicate<Reserva> criterio) {
38 List<Reserva> r = new ArrayList<>();
39 for (Reserva x : datos.values()) if (criterio.test(x)) r.add(x);
40 return r;
41 }
42}
43
44/** El servicio usa las consultas del repositorio para aplicar las reglas del hotel. */
45class Recepcion {
46 private final RepositorioReservas repo;
47 private int siguiente = 1;
48 Recepcion(RepositorioReservas repo) { this.repo = repo; }
49
50 String reservar(int habitacion, String cliente, LocalDate entrada, LocalDate salida) {
51 if (!entrada.isBefore(salida)) return "Fechas no válidas";
52 List<Reserva> choques = repo.solapadas(habitacion, entrada, salida);
53 if (!choques.isEmpty()) return "La " + habitacion + " está ocupada (reserva #" + choques.get(0).id() + ")";
54 Reserva r = new Reserva(siguiente++, habitacion, cliente, entrada, salida);
55 repo.guardar(r);
56 return "Reserva #" + r.id() + ": " + cliente + " en la " + habitacion + " del " + entrada + " al " + salida;
57 }
58}
59
60public class Main {
61 public static void main(String[] args) {
62 RepositorioReservas repo = new RepositorioReservasMemoria();
63 Recepcion recepcion = new Recepcion(repo);
64 Scanner sc = new Scanner(System.in);
65 while (sc.hasNextLine()) {
66 String linea = sc.nextLine().trim();
67 if (linea.isEmpty()) continue;
68 String[] p = linea.split("\\s+");
69 try {
70 switch (p[0] + "/" + (p.length - 1)) {
71 case "reservar/4" -> System.out.println(recepcion.reservar(Integer.parseInt(p[1]), p[2], LocalDate.parse(p[3]), LocalDate.parse(p[4])));
72 case "cancelar/1" -> System.out.println(repo.borrar(Integer.parseInt(p[1])) ? "Cancelada la #" + p[1] : "No existe la reserva #" + p[1]);
73 case "ocupadas/1" -> {
74 List<Integer> h = repo.ocupadas(LocalDate.parse(p[1]));
75 System.out.println("Ocupadas el " + p[1] + ": " + (h.isEmpty() ? "ninguna" : h.toString().replaceAll("[\\[\\]]", "")));
76 }
77 case "cliente/1" -> {
78 List<Reserva> r = repo.delCliente(p[1]);
79 System.out.println("Reservas de " + p[1] + ":" + (r.isEmpty() ? " ninguna" : ""));
80 for (Reserva x : r) System.out.println(" #" + x.id() + " habitación " + x.habitacion() + ": " + x.entrada() + " → " + x.salida());
81 }
82 default -> System.out.println("Orden no válida: " + linea);
83 }
84 } catch (RuntimeException e) {
85 System.out.println("Orden no válida: " + linea);
86 }
87 }
88 }
89}La regla («no se puede reservar una habitación ocupada») está en el servicio; el repositorio solo responde preguntas del dominio («¿qué reservas se cruzan con estas fechas?»).
Los intervalos cerrados por la izquierda y abiertos por la derecha hacen que el día de salida de uno sea el de entrada de otro sin choques.
Test
Test: Repository
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.¿En qué se diferencia un repositorio de un DAO?
2.¿Qué hace Spring Data JPA con una interfaz que extiende JpaRepository?
3.¿Dónde debería estar la regla «no se puede reservar una habitación ocupada»?
4.¿Qué ventaja tiene un método buscar(Predicate<T>)?
5.¿Qué devuelve un repositorio?