Reservas de hotel
En resumen: Un proyecto de reservas de hotel: un servidor y muchos clientes reservan, consultan y anulan habitaciones a la vez, y cuando dos personas quieren la misma habitación en el mismo instante, solo una lo consigue.
Problema
Una habitación, dos confirmaciones
Un hotel pequeño tiene cuatro habitaciones. Las reservas llegan de todas partes: del sitio web, de la recepción, de las agencias de viajes. Un viernes por la tarde, dos agencias reservan la última habitación doble en el mismo segundo. Las dos reciben una confirmación. El sábado llegan dos familias a la habitación.
Vamos a construir un sistema en el que el servidor guarda las habitaciones y las reservas, y los clientes envían solicitudes por la red. Un cliente puede:
- ver las habitaciones libres;
- consultar una habitación;
- reservar una habitación a su nombre;
- anular su propia reserva.
Y cuando dos clientes reservan la misma habitación a la vez, solo uno de ellos lo consigue. Lo demostraremos con un programa que da la misma salida en cada ejecución.
Construimos de abajo arriba: el modelo sin red, las reglas y los errores, el protocolo, el servidor y el cliente, las solicitudes simultáneas y, al final, todo en un solo programa.
Más precisamente, para curiosos
node:net). Pero hay un solo hilo: Node.js atiende a todos los clientes en él y pasa de uno a otro mientras esperan a la red. No hay cerrojo. Qué protege entonces las habitaciones, lo veremos hacia el final.La idea
La habitación
Una habitación tiene un número, un tipo, un precio por noche y un estado: libre u ocupada.
class Room { private final int number; private final String type; private final int price; private boolean free = true;
public Room(int number, String type, int price) { this.number = number; this.type = type; this.price = price; }
public int getNumber() { return number; }
public String getType() { return type; }
public int getPrice() { return price; }
public boolean isFree() { return free; }
public void occupy() { free = false; }
public void release() { free = true; }
@Override public String toString() { return number + ", " + type + ", " + price + " € por noche, " + (free ? "libre" : "ocupada"); }}public class Room{ public int Number { get; } public string Type { get; } public int Price { get; } public bool IsFree { get; set; } = true;
public Room(int number, string type, int price) { Number = number; Type = type; Price = price; }
public void Occupy() { IsFree = false; }
public void Release() { IsFree = true; }
public override string ToString() { return Number + ", " + Type + ", " + Price + " € por noche, " + (IsFree ? "libre" : "ocupada"); }}class Room: def __init__(self, number, kind, price): self._number = number self._kind = kind self._price = price self._free = True
@property def number(self): return self._number
@property def kind(self): return self._kind
@property def price(self): return self._price
@property def is_free(self): return self._free
def occupy(self): self._free = False
def release(self): self._free = True
def __str__(self): state = "libre" if self._free else "ocupada" return f"{self._number}, {self._kind}, {self._price} € por noche, {state}"class Room { #number; #type; #price; #free = true;
constructor(number, type, price) { this.#number = number; this.#type = type; this.#price = price; }
get number() { return this.#number; }
get type() { return this.#type; }
get price() { return this.#price; }
get isFree() { return this.#free; }
occupy() { this.#free = false; }
release() { this.#free = true; }
toString() { const state = this.#free ? "libre" : "ocupada"; return `${this.#number}, ${this.#type}, ${this.#price} € por noche, ${state}`; }}- El número, el tipo y el precio no cambian. Solo cambia el estado, y solo a través de dos métodos con nombres claros:
occupyyrelease. - La habitación no sabe quién se aloja en ella. La habitación sale hacia cualquier cliente que pregunte, y el nombre del huésped no debe salir del hotel.
Más precisamente, para curiosos
# son privados: desde fuera no se ven en absoluto. El número, el tipo y el precio se leen con un getter sin paréntesis (room.number), y cambiar uno desde fuera (room.number = 5) en un archivo con import se detiene con TypeError, porque el getter no tiene setter. JavaScript no tiene enumeraciones. Para más de dos estados se usa un objeto congelado con sus nombres, como Protocol más abajo.Míralo
El hotel y la reserva
Map «número de habitación → nombre»:private final List<Room> rooms = new ArrayList<>();private final Map<Integer, String> guests = new HashMap<>();private readonly List<Room> rooms = new List<Room>();private readonly Dictionary<int, string> guests = new Dictionary<int, string>();def __init__(self): self._rooms = [] self._guests = {}#rooms = [];#guests = new Map();public void add(Room room) { rooms.add(room);}
public List<Room> freeRooms() { return rooms.stream().filter(Room::isFree).toList();}
public Room check(int number) { return find(number);}public void Add(Room room){ rooms.Add(room);}
public List<Room> FreeRooms(){ return rooms.Where(room => room.IsFree).ToList();}
public Room Check(int number){ return Find(number);}def add(self, room): self._rooms.append(room)
def free_rooms(self): return [room for room in self._rooms if room.is_free]
def check(self, number): return self._find(number)add(room) { this.#rooms.push(room);}
freeRooms() { return this.#rooms.filter((room) => room.isFree);}
check(number) { return this.#find(number);}#find es privado: busca la habitación por número o lanza una excepción. Así nadie de fuera puede agarrar una habitación y ocuparla saltándose las reglas:
private Room find(int number) { for (Room room : rooms) { if (room.getNumber() == number) { return room; } } throw new RoomNotFoundException("No existe la habitación " + number + ".");}private Room Find(int number){ Room room = rooms.FirstOrDefault(r => r.Number == number); if (room == null) { throw new RoomNotFoundException("No existe la habitación " + number + "."); } return room;}def _find(self, number): for room in self._rooms: if room.number == number: return room raise RoomNotFoundError(f"No existe la habitación {number}.")#find(number) { const room = this.#rooms.find((r) => r.number === number); if (room === undefined) { throw new RoomNotFoundError(`No existe la habitación ${number}.`); } return room;}La reserva comprueba el nombre, busca la habitación y comprueba si está libre. Solo entonces la ocupa:
public void reserve(int number, String guest) { if (guest.isBlank()) { throw new HotelException("El nombre del huésped es obligatorio."); } Room room = find(number); if (!room.isFree()) { throw new RoomTakenException("La habitación " + number + " ya está ocupada."); } room.occupy(); guests.put(number, guest);}public void Reserve(int number, string guest){ if (string.IsNullOrWhiteSpace(guest)) { throw new HotelException("El nombre del huésped es obligatorio."); } Room room = Find(number); if (!room.IsFree) { throw new RoomTakenException("La habitación " + number + " ya está ocupada."); } room.Occupy(); guests[number] = guest;}def reserve(self, number, guest): if not guest.strip(): raise HotelError("El nombre del huésped es obligatorio.") room = self._find(number) if not room.is_free: raise RoomTakenError(f"La habitación {number} ya está ocupada.") room.occupy() self._guests[number] = guestreserve(number, guest) { if (guest.trim() === "") { throw new HotelError("El nombre del huésped es obligatorio."); } const room = this.#find(number); if (!room.isFree) { throw new RoomTakenError(`La habitación ${number} ya está ocupada.`); } room.occupy(); this.#guests.set(number, guest);}Hotel hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));
for (Room room : hotel.freeRooms()) { System.out.println(room);}hotel.reserve(102, "Ana Torres");System.out.println("Después de la reserva: " + hotel.check(102));System.out.println("Habitaciones libres: " + hotel.freeRooms().size());101, individual, 60 € por noche, libre102, doble, 85 € por noche, libre201, doble, 95 € por noche, libre202, suite, 140 € por noche, libreDespués de la reserva: 102, doble, 85 € por noche, ocupadaHabitaciones libres: 3Hotel hotel = new Hotel();hotel.Add(new Room(101, "individual", 60));hotel.Add(new Room(102, "doble", 85));hotel.Add(new Room(201, "doble", 95));hotel.Add(new Room(202, "suite", 140));
foreach (Room room in hotel.FreeRooms()){ Console.WriteLine(room);}hotel.Reserve(102, "Ana Torres");Console.WriteLine("Después de la reserva: " + hotel.Check(102));Console.WriteLine("Habitaciones libres: " + hotel.FreeRooms().Count);101, individual, 60 € por noche, libre102, doble, 85 € por noche, libre201, doble, 95 € por noche, libre202, suite, 140 € por noche, libreDespués de la reserva: 102, doble, 85 € por noche, ocupadaHabitaciones libres: 3hotel = Hotel()hotel.add(Room(101, "individual", 60))hotel.add(Room(102, "doble", 85))hotel.add(Room(201, "doble", 95))hotel.add(Room(202, "suite", 140))
for room in hotel.free_rooms(): print(room)hotel.reserve(102, "Ana Torres")print(f"Después de la reserva: {hotel.check(102)}")print(f"Habitaciones libres: {len(hotel.free_rooms())}")101, individual, 60 € por noche, libre102, doble, 85 € por noche, libre201, doble, 95 € por noche, libre202, suite, 140 € por noche, libreDespués de la reserva: 102, doble, 85 € por noche, ocupadaHabitaciones libres: 3const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));
for (const room of hotel.freeRooms()) { console.log(String(room));}hotel.reserve(102, "Ana Torres");console.log(`Después de la reserva: ${hotel.check(102)}`);console.log(`Habitaciones libres: ${hotel.freeRooms().length}`);101, individual, 60 € por noche, libre102, doble, 85 € por noche, libre201, doble, 95 € por noche, libre202, suite, 140 € por noche, libreDespués de la reserva: 102, doble, 85 € por noche, ocupadaHabitaciones libres: 3String(room) y la plantilla de texto llaman a toString(). console.log(room) no lo llama: muestra los campos del objeto, los privados no se ven, e imprime solo Room {}.La idea
La anulación y las reglas
No toda solicitud se puede cumplir: la habitación está ocupada, no existe la habitación 305, un huésped anula la reserva de otro. Son rechazos según las reglas, así que son excepciones propias con un padre común:
class HotelException extends RuntimeException { public HotelException(String message) { super(message); }}
class RoomNotFoundException extends HotelException { public RoomNotFoundException(String message) { super(message); }}
class RoomTakenException extends HotelException { public RoomTakenException(String message) { super(message); }}public class HotelException : Exception{ public HotelException(string message) : base(message) { }}
public class RoomNotFoundException : HotelException{ public RoomNotFoundException(string message) : base(message) { }}
public class RoomTakenException : HotelException{ public RoomTakenException(string message) : base(message) { }}class HotelError(Exception): pass
class RoomNotFoundError(HotelError): pass
class RoomTakenError(HotelError): passclass HotelError extends Error { constructor(message) { super(message); this.name = "HotelError"; }}
class RoomNotFoundError extends HotelError { constructor(message) { super(message); this.name = "RoomNotFoundError"; }}
class RoomTakenError extends HotelError { constructor(message) { super(message); this.name = "RoomTakenError"; }}La anulación comprueba que la habitación esté ocupada y que la anule el mismo huésped:
public void cancel(int number, String guest) { Room room = find(number); if (room.isFree()) { throw new HotelException("La habitación " + number + " no está reservada."); } if (!guests.get(number).equals(guest)) { throw new HotelException("La habitación " + number + " está reservada por otro huésped."); } room.release(); guests.remove(number);}public void Cancel(int number, string guest){ Room room = Find(number); if (room.IsFree) { throw new HotelException("La habitación " + number + " no está reservada."); } if (guests[number] != guest) { throw new HotelException("La habitación " + number + " está reservada por otro huésped."); } room.Release(); guests.Remove(number);}def cancel(self, number, guest): room = self._find(number) if room.is_free: raise HotelError(f"La habitación {number} no está reservada.") if self._guests[number] != guest: raise HotelError(f"La habitación {number} está reservada por otro huésped.") room.release() del self._guests[number]cancel(number, guest) { const room = this.#find(number); if (room.isFree) { throw new HotelError(`La habitación ${number} no está reservada.`); } if (this.#guests.get(number) !== guest) { throw new HotelError(`La habitación ${number} está reservada por otro huésped.`); } room.release(); this.#guests.delete(number);}El enunciado del proyecto pide comprobar el sistema en distintos escenarios: reservar, consultar y anular, tanto cuando son válidos como cuando no lo son. Cada escenario es una línea de la salida:
Hotel hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));
hotel.reserve(102, "Ana Torres");System.out.println("1. Habitación libre: " + hotel.check(102));try { hotel.reserve(102, "Bruno Silva");} catch (RoomTakenException e) { System.out.println("2. Habitación ocupada: " + e.getMessage());}try { hotel.cancel(102, "Bruno Silva");} catch (HotelException e) { System.out.println("3. Reserva ajena: " + e.getMessage());}hotel.cancel(102, "Ana Torres");System.out.println("4. Anulación: " + hotel.check(102));try { hotel.cancel(102, "Ana Torres");} catch (HotelException e) { System.out.println("5. Segunda anulación: " + e.getMessage());}try { hotel.reserve(305, "Ana Torres");} catch (RoomNotFoundException e) { System.out.println("6. No existe esa habitación: " + e.getMessage());}try { hotel.reserve(201, " ");} catch (HotelException e) { System.out.println("7. Sin nombre: " + e.getMessage());}1. Habitación libre: 102, doble, 85 € por noche, ocupada2. Habitación ocupada: La habitación 102 ya está ocupada.3. Reserva ajena: La habitación 102 está reservada por otro huésped.4. Anulación: 102, doble, 85 € por noche, libre5. Segunda anulación: La habitación 102 no está reservada.6. No existe esa habitación: No existe la habitación 305.7. Sin nombre: El nombre del huésped es obligatorio.Hotel hotel = new Hotel();hotel.Add(new Room(101, "individual", 60));hotel.Add(new Room(102, "doble", 85));hotel.Add(new Room(201, "doble", 95));hotel.Add(new Room(202, "suite", 140));
hotel.Reserve(102, "Ana Torres");Console.WriteLine("1. Habitación libre: " + hotel.Check(102));try{ hotel.Reserve(102, "Bruno Silva");}catch (RoomTakenException e){ Console.WriteLine("2. Habitación ocupada: " + e.Message);}try{ hotel.Cancel(102, "Bruno Silva");}catch (HotelException e){ Console.WriteLine("3. Reserva ajena: " + e.Message);}hotel.Cancel(102, "Ana Torres");Console.WriteLine("4. Anulación: " + hotel.Check(102));try{ hotel.Cancel(102, "Ana Torres");}catch (HotelException e){ Console.WriteLine("5. Segunda anulación: " + e.Message);}try{ hotel.Reserve(305, "Ana Torres");}catch (RoomNotFoundException e){ Console.WriteLine("6. No existe esa habitación: " + e.Message);}try{ hotel.Reserve(201, " ");}catch (HotelException e){ Console.WriteLine("7. Sin nombre: " + e.Message);}1. Habitación libre: 102, doble, 85 € por noche, ocupada2. Habitación ocupada: La habitación 102 ya está ocupada.3. Reserva ajena: La habitación 102 está reservada por otro huésped.4. Anulación: 102, doble, 85 € por noche, libre5. Segunda anulación: La habitación 102 no está reservada.6. No existe esa habitación: No existe la habitación 305.7. Sin nombre: El nombre del huésped es obligatorio.hotel = Hotel()hotel.add(Room(101, "individual", 60))hotel.add(Room(102, "doble", 85))hotel.add(Room(201, "doble", 95))hotel.add(Room(202, "suite", 140))
hotel.reserve(102, "Ana Torres")print(f"1. Habitación libre: {hotel.check(102)}")try: hotel.reserve(102, "Bruno Silva")except RoomTakenError as e: print(f"2. Habitación ocupada: {e}")try: hotel.cancel(102, "Bruno Silva")except HotelError as e: print(f"3. Reserva ajena: {e}")hotel.cancel(102, "Ana Torres")print(f"4. Anulación: {hotel.check(102)}")try: hotel.cancel(102, "Ana Torres")except HotelError as e: print(f"5. Segunda anulación: {e}")try: hotel.reserve(305, "Ana Torres")except RoomNotFoundError as e: print(f"6. No existe esa habitación: {e}")try: hotel.reserve(201, " ")except HotelError as e: print(f"7. Sin nombre: {e}")1. Habitación libre: 102, doble, 85 € por noche, ocupada2. Habitación ocupada: La habitación 102 ya está ocupada.3. Reserva ajena: La habitación 102 está reservada por otro huésped.4. Anulación: 102, doble, 85 € por noche, libre5. Segunda anulación: La habitación 102 no está reservada.6. No existe esa habitación: No existe la habitación 305.7. Sin nombre: El nombre del huésped es obligatorio.const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));
hotel.reserve(102, "Ana Torres");console.log(`1. Habitación libre: ${hotel.check(102)}`);try { hotel.reserve(102, "Bruno Silva");} catch (e) { if (!(e instanceof RoomTakenError)) throw e; console.log(`2. Habitación ocupada: ${e.message}`);}try { hotel.cancel(102, "Bruno Silva");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`3. Reserva ajena: ${e.message}`);}hotel.cancel(102, "Ana Torres");console.log(`4. Anulación: ${hotel.check(102)}`);try { hotel.cancel(102, "Ana Torres");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`5. Segunda anulación: ${e.message}`);}try { hotel.reserve(305, "Ana Torres");} catch (e) { if (!(e instanceof RoomNotFoundError)) throw e; console.log(`6. No existe esa habitación: ${e.message}`);}try { hotel.reserve(201, " ");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`7. Sin nombre: ${e.message}`);}1. Habitación libre: 102, doble, 85 € por noche, ocupada2. Habitación ocupada: La habitación 102 ya está ocupada.3. Reserva ajena: La habitación 102 está reservada por otro huésped.4. Anulación: 102, doble, 85 € por noche, libre5. Segunda anulación: La habitación 102 no está reservada.6. No existe esa habitación: No existe la habitación 305.7. Sin nombre: El nombre del huésped es obligatorio.La excepción concreta dice cuál es el problema. HotelError atrapa todos los rechazos de una vez: el servidor atrapará solo esa y la convertirá en una respuesta para el cliente.
catch no tiene tipo y atrapa todo. Por eso cada catch pregunta con instanceof si el error es suyo, y relanza cualquier otro con throw e.Más precisamente, para curiosos
- No hay excepciones comprobadas: ningún método declara qué lanza. Por eso la recepción las atrapará en un solo lugar.
- Escenarios como estos son tests naturales. Con
node:testcada uno se convierte en untest(...), yassert.throws(() => hotel.reserve(102, "Bruno Silva"), RoomTakenError)comprueba la excepción.
Pruébalo
¿Quién está en la 201?
¿Qué imprimirá el programa?
const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));
hotel.reserve(101, "Ana Torres");hotel.reserve(201, "Bruno Silva");hotel.cancel(101, "Ana Torres");try { hotel.reserve(201, "Vera Gil"); console.log("Vera está en la 201");} catch (e) { if (!(e instanceof RoomTakenError)) throw e; console.log("Rechazo para Vera");}console.log(hotel.freeRooms().length);Ana ocupa la 101 y después la libera. Bruno ocupa la 201, así que Vera recibe RoomTakenError, y la línea «Vera está en la 201» no se ejecuta. Solo la 201 está ocupada, así que hay tres habitaciones libres.
La idea
El protocolo: una línea «COMANDO:texto»
Cada mensaje es una línea COMANDO:texto, y el servidor responde con una línea:
| El cliente envía | El servidor responde |
|---|---|
LIBRES |
OK: y las habitaciones libres, separadas por ; |
CONSULTAR:102 |
OK:102, doble, 85 € por noche, ocupada |
RESERVAR:102;Ana Torres |
OK:La habitación 102 es tuya, Ana Torres. o ERROR: y el motivo |
ANULAR:102;Ana Torres |
OK:La reserva de la habitación 102 está anulada. o ERROR: y el motivo |
SALIR |
ADIÓS, y el servidor cierra la conexión |
Las palabras del protocolo son constantes en un solo lugar (Object.freeze impide que el objeto se cambie):
const Protocol = Object.freeze({ FREE: "LIBRES", CHECK: "CONSULTAR", RESERVE: "RESERVAR", CANCEL: "ANULAR", EXIT: "SALIR",
OK: "OK", ERROR: "ERROR", BYE: "ADIÓS",});La recepción traduce una línea del protocolo en una llamada al hotel y de vuelta:
class Reception { #hotel;
constructor(hotel) { this.#hotel = hotel; }
...}handle(line) { const colon = line.indexOf(":"); const command = colon < 0 ? line : line.slice(0, colon); const text = colon < 0 ? "" : line.slice(colon + 1); try { switch (command) { case Protocol.FREE: return `${Protocol.OK}:${this.#join(this.#hotel.freeRooms())}`; case Protocol.CHECK: return `${Protocol.OK}:${this.#hotel.check(this.#number(text))}`; case Protocol.RESERVE: return this.#reserve(text); case Protocol.CANCEL: return this.#cancel(text); default: throw new HotelError(`Comando desconocido ${command}.`); } } catch (e) { if (!(e instanceof HotelError)) throw e; return `${Protocol.ERROR}:${e.message}`; }}#reserve(text) { const [numberText, guest] = this.#numberAndGuest(text); const number = this.#number(numberText); this.#hotel.reserve(number, guest); return `${Protocol.OK}:La habitación ${number} es tuya, ${guest}.`;}
#cancel(text) { const [numberText, guest] = this.#numberAndGuest(text); const number = this.#number(numberText); this.#hotel.cancel(number, guest); return `${Protocol.OK}:La reserva de la habitación ${number} está anulada.`;}
#numberAndGuest(text) { const semicolon = text.indexOf(";"); if (semicolon < 0) { throw new HotelError("Escribe número y nombre, por ejemplo «102;Ana Torres»."); } return [text.slice(0, semicolon), text.slice(semicolon + 1)];}
#number(text) { const number = Number(text); if (text.trim() === "" || !Number.isInteger(number)) { throw new HotelError("El número de la habitación es un número entero."); } return number;}
#join(rooms) { if (rooms.length === 0) { return "No hay habitaciones libres."; } return rooms.join("; ");}catch pregunta si el error es un HotelError. Si no lo es, es un error en el código y no un rechazo según las reglas, y sigue hacia arriba. Number(text) no lanza: para «ciento dos» da NaN, y para un texto vacío 0. Por eso #number comprueba por su cuenta que el texto no esté vacío y que el número sea entero.La recepción no recuerda nada del cliente: cada línea lleva todo lo que necesita, incluso el nombre del huésped. Por eso basta una recepción para todos los clientes. Puedes comprobarlo sin red:
const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));
const reception = new Reception(hotel);const lines = [ "RESERVAR:102;Ana Torres", "CONSULTAR:102", "RESERVAR:102;Bruno Silva", "ANULAR:102", "CONSULTAR:ciento dos", "MOSTRAR",];for (const line of lines) { console.log(`> ${line}`); console.log(reception.handle(line));}> RESERVAR:102;Ana TorresOK:La habitación 102 es tuya, Ana Torres.> CONSULTAR:102OK:102, doble, 85 € por noche, ocupada> RESERVAR:102;Bruno SilvaERROR:La habitación 102 ya está ocupada.> ANULAR:102ERROR:Escribe número y nombre, por ejemplo «102;Ana Torres».> CONSULTAR:ciento dosERROR:El número de la habitación es un número entero.> MOSTRARERROR:Comando desconocido MOSTRAR.Más precisamente, para curiosos
-
JavaScript no tiene enumeraciones. Un objeto congelado es lo más parecido: una lista cerrada de nombres que nadie puede cambiar. En un archivo con
import, el intentoProtocol.FREE = "TODAS"se detiene conTypeError, yProtocol.FREEsigue siendo"LIBRES". - El protocolo no tiene estado: cada línea vale por sí misma. Eso lo hace fácil de comprobar y fácil para atender a más clientes. Si el cliente tuviera que decir primero «soy Ana», haría falta una recepción por cliente, como la sesión del concesionario de coches.
Míralo
El servidor
No hay grupo de hilos. createServer llama a su función para cada cliente nuevo, y el ClientHandler de ese cliente lee sus líneas con for await. Mientras el cliente calla, el hilo atiende a los demás: await no lo detiene, dice «sigue desde aquí cuando te toque».
class ClientHandler { #socket; #reception;
constructor(socket, reception) { this.#socket = socket; this.#reception = reception; }
async run() { try { for await (const line of createInterface({ input: this.#socket })) { if (line === Protocol.EXIT) { break; } this.#socket.write(`${this.#reception.handle(line)}\n`); } this.#socket.end(`${Protocol.BYE}\n`); } catch (e) { console.log(`Se cortó la conexión con el cliente: ${e.message}`); } }}HotelServer arranca el servidor en el puerto. listen y close no ocurren enseguida, así que start y stop devuelven una Promise y se llaman con await:
class HotelServer { #server; #port;
constructor(port, hotel) { const reception = new Reception(hotel); this.#port = port; this.#server = createServer((socket) => new ClientHandler(socket, reception).run()); }
start() { return new Promise((resolve) => this.#server.listen(this.#port, "localhost", resolve)); }
stop() { return new Promise((resolve) => this.#server.close(resolve)); }}- Haya cuantos clientes haya, un solo hilo los atiende a todos. No espera a la red: hace otro trabajo hasta que llega la línea.
- Todos los clientes llaman al mismo
Hotel. Así que el hotel tendrá que proteger sus habitaciones. Cómo, en un momento.
Míralo
El cliente
await. Las líneas del socket se leen con el mismo createInterface con el que leemos del teclado: next() da la siguiente línea en cuanto llega. Sin texto, send envía solo el comando. close() dice SALIR y cierra la conexión:class HotelClient { #socket; #lines;
constructor(host, port) { this.#socket = createConnection({ host, port }); this.#lines = createInterface({ input: this.#socket })[Symbol.asyncIterator](); }
async send(command, text) { const line = text === undefined ? command : `${command}:${text}`; this.#socket.write(`${line}\n`); return (await this.#lines.next()).value; }
async close() { await this.send(Protocol.EXIT); this.#socket.end(); }}El servidor y el cliente en un solo programa:
const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));const server = new HotelServer(5351, hotel);await server.start();
const client = new HotelClient("localhost", 5351);console.log(await client.send(Protocol.RESERVE, "102;Ana Torres"));console.log(await client.send(Protocol.CHECK, "102"));console.log(await client.send(Protocol.FREE));console.log(await client.send(Protocol.CANCEL, "102;Ana Torres"));console.log(await client.send(Protocol.CHECK, "305"));await client.close();await server.stop();OK:La habitación 102 es tuya, Ana Torres.OK:102, doble, 85 € por noche, ocupadaOK:101, individual, 60 € por noche, libre; 201, doble, 95 € por noche, libre; 202, suite, 140 € por noche, libreOK:La reserva de la habitación 102 está anulada.ERROR:No existe la habitación 305.La idea
Dos para una habitación
await) mientras se ejecuta otra, puede pasar esto:| Agencia A | Agencia B | |
|---|---|---|
| 1 | comprueba: la 102 está libre | |
| 2 | comprueba: la 102 está libre | |
| 3 | ocupa la 102 para su huésped | |
| 4 | ocupa la 102 para su huésped y anota a ese huésped |
Las dos reciben una confirmación. No hay ningún error en ninguna línea de código por separado. El error es que los pasos de las dos solicitudes se intercalan.
La solución: que nadie se meta entre la comprobación y la ocupación de la habitación. Entonces «comprobar y ocupar» es atómico. En JavaScript nuestra reserva ya lo es. Node.js ejecuta el código en un solo hilo, y una función síncrona se ejecuta hasta el final antes de que empiece otro código. En reserve no hay ningún await, así que ninguna solicitud puede meterse en medio. No hace falta cerrojo:
reserve(number, guest) { if (guest.trim() === "") { throw new HotelError("El nombre del huésped es obligatorio."); } const room = this.#find(number); if (!room.isFree) { throw new RoomTakenError(`La habitación ${number} ya está ocupada.`); } room.occupy(); this.#guests.set(number, guest);}await en medio de la reserva. Si las habitaciones estuvieran en una base de datos, la comprobación esperaría su respuesta con await. Mientras espera, otra solicitud consulta la misma habitación y la ve libre. Por eso «comprobar y ocupar» tiene que ser un solo paso síncrono o una sola consulta a la base de datos.reserve(number, guest) { if (guest.trim() === "") { throw new HotelError("El nombre del huésped es obligatorio."); } const room = this.#find(number); if (!room.isFree) { throw new RoomTakenError(`La habitación ${number} ya está ocupada.`); } room.occupy(); this.#guests.set(number, guest);}Paso a paso, en palabras
- La habitación 102 está libre. Ana Torres y Bruno Silva la reservan en el mismo instante.
- Primero con un await entre la comprobación y la ocupación, como si las habitaciones estuvieran en una base de datos. La solicitud de Ana comprueba, ve «libre» y espera.
- Mientras Ana espera, la solicitud de Bruno comprueba: también ve «libre».
- Ana ocupa la habitación y anota su nombre.
- Bruno también ocupa la habitación y anota su nombre. Los dos tienen una confirmación para la misma habitación.
- Desde el principio, sin await: reserve es síncrona y se ejecuta hasta el final antes de que empiece otro código.
- Digamos que Ana es la primera: es el turno de su solicitud. La comprobación y la ocupación son un solo paso, y nadie se mete entre ellas.
- La solicitud de Bruno ha llegado y espera: hay un solo hilo, y hace una cosa a la vez.
- Ahora es el turno de Bruno. Ahora la comprobación ve «ocupada».
- RoomTakenError. La recepción la devuelve como una línea de error, y Bruno puede pedir otra habitación.
- El programa de la lección cuenta las que tienen éxito: 1 por habitación. Quién fue primero cambia en cada ejecución.
La prueba: dos clientes reservan la 102 a la vez, y luego veinte agencias reservan la 201 a la vez. Se recogen las respuestas, y el programa imprime cuántas tuvieron éxito:
async function reserve(port, room, guest) { try { const client = new HotelClient("localhost", port); const reply = await client.send(Protocol.RESERVE, `${room};${guest}`); await client.close(); return reply; } catch (e) { return `${Protocol.ERROR}:${e.message}`; }}
async function race(port, room, guests) { const replies = await Promise.all(guests.map((guest) => reserve(port, room, guest))); return replies.filter((reply) => reply.startsWith(Protocol.OK)).length;}const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));const server = new HotelServer(5352, hotel);await server.start();
const two = ["Ana Torres", "Bruno Silva"];console.log(`Dos para la habitación 102, con éxito: ${await race(5352, 102, two)}`);
const agencies = [];for (let i = 1; i <= 20; i++) { agencies.push(`Agencia ${i}`);}console.log(`Veinte para la habitación 201, con éxito: ${await race(5352, 201, agencies)}`);
const desk = new HotelClient("localhost", 5352);console.log(await desk.send(Protocol.FREE));await desk.close();await server.stop();Dos para la habitación 102, con éxito: 1Veinte para la habitación 201, con éxito: 1OK:101, individual, 60 € por noche, libre; 202, suite, 140 € por noche, librePromise.all arranca todas las solicitudes a la vez y espera las respuestas, y race cuenta las que tuvieron éxito. Hay un solo hilo, y aun así las solicitudes se intercalan: cada una espera a la red mientras se ejecutan las demás.Quién se quedó con la habitación cambia en cada ejecución, así que el programa no lo imprime. Cuántas tuvieron éxito es siempre lo mismo: una por habitación.
Más precisamente, para curiosos
-
Con un
awaitentre la comprobación y la ocupación de la habitación, el error no es raro: dos personas consiguen la habitación en cada ejecución (mira la siguiente tarjeta). Por eso cadaawaites un lugar para pararse a pensar: qué puede cambiar otra solicitud mientras esta espera. -
Si de verdad necesitas un
awaiten medio de la reserva, las solicitudes para una habitación se ponen en fila una tras otra, por ejemplo con una cola de promesas (Promise) o con una biblioteca de bloqueo. Este proyecto no lo necesita. - En un sistema real las habitaciones están en una base de datos, y la protección ocurre allí: una transacción o una condición en la propia consulta (
UPDATE rooms SET free = false WHERE number = 102 AND free = true). Entonces se cumple incluso con varios servidores, no solo dentro de un programa.
Pruébalo
Bloqueado, pero no del todo
async separados. La recepción espera primero la comprobación y luego la ocupación. Dos clientes a la vez consiguen la misma habitación en cada ejecución. ¿En qué línea está el error?Elige la línea con el error.
await this.#hotel.isFree(number) la solicitud se detiene y espera. Justo entonces otra solicitud comprueba la misma habitación y la ve libre. La comprobación y la ocupación tienen que ser un solo paso sin await entre ellas, como en nuestro reserve.Míralo
El hotel completo
Los escenarios de prueba del enunciado, esta vez por la red, con dos clientes conectados a la vez. Al final dos personas reservan la 201 en el mismo instante:
async function say(who, client, command, text) { const reply = await client.send(command, text); console.log(`${who} > ${command}:${text}`); console.log(`${who} < ${reply}`);}
const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));const server = new HotelServer(5353, hotel);await server.start();
const anna = new HotelClient("localhost", 5353);const boris = new HotelClient("localhost", 5353);await say("Ana", anna, Protocol.RESERVE, "102;Ana Torres");await say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva");await say("Bruno", boris, Protocol.CHECK, "102");await say("Bruno", boris, Protocol.CANCEL, "102;Bruno Silva");await say("Ana", anna, Protocol.CANCEL, "102;Ana Torres");await say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva");await say("Ana", anna, Protocol.CHECK, "305");await anna.close();await boris.close();
const two = ["Ana Torres", "Vera Gil"];console.log(`Dos para la habitación 201 a la vez, con éxito: ${await race(5353, 201, two)}`);const desk = new HotelClient("localhost", 5353);console.log(await desk.send(Protocol.FREE));await desk.close();await server.stop();Ana > RESERVAR:102;Ana TorresAna < OK:La habitación 102 es tuya, Ana Torres.Bruno > RESERVAR:102;Bruno SilvaBruno < ERROR:La habitación 102 ya está ocupada.Bruno > CONSULTAR:102Bruno < OK:102, doble, 85 € por noche, ocupadaBruno > ANULAR:102;Bruno SilvaBruno < ERROR:La habitación 102 está reservada por otro huésped.Ana > ANULAR:102;Ana TorresAna < OK:La reserva de la habitación 102 está anulada.Bruno > RESERVAR:102;Bruno SilvaBruno < OK:La habitación 102 es tuya, Bruno Silva.Ana > CONSULTAR:305Ana < ERROR:No existe la habitación 305.Dos para la habitación 201 a la vez, con éxito: 1OK:101, individual, 60 € por noche, libre; 202, suite, 140 € por noche, libreMás precisamente, para curiosos
El programa completo, de principio a fin:
import { createConnection, createServer } from "node:net";import { createInterface } from "node:readline";
class Room { #number; #type; #price; #free = true;
constructor(number, type, price) { this.#number = number; this.#type = type; this.#price = price; }
get number() { return this.#number; }
get type() { return this.#type; }
get price() { return this.#price; }
get isFree() { return this.#free; }
occupy() { this.#free = false; }
release() { this.#free = true; }
toString() { const state = this.#free ? "libre" : "ocupada"; return `${this.#number}, ${this.#type}, ${this.#price} € por noche, ${state}`; }}
class Hotel { #rooms = []; #guests = new Map();
add(room) { this.#rooms.push(room); }
freeRooms() { return this.#rooms.filter((room) => room.isFree); }
check(number) { return this.#find(number); }
reserve(number, guest) { if (guest.trim() === "") { throw new HotelError("El nombre del huésped es obligatorio."); } const room = this.#find(number); if (!room.isFree) { throw new RoomTakenError(`La habitación ${number} ya está ocupada.`); } room.occupy(); this.#guests.set(number, guest); }
cancel(number, guest) { const room = this.#find(number); if (room.isFree) { throw new HotelError(`La habitación ${number} no está reservada.`); } if (this.#guests.get(number) !== guest) { throw new HotelError(`La habitación ${number} está reservada por otro huésped.`); } room.release(); this.#guests.delete(number); }
#find(number) { const room = this.#rooms.find((r) => r.number === number); if (room === undefined) { throw new RoomNotFoundError(`No existe la habitación ${number}.`); } return room; }}
class HotelError extends Error { constructor(message) { super(message); this.name = "HotelError"; }}
class RoomNotFoundError extends HotelError { constructor(message) { super(message); this.name = "RoomNotFoundError"; }}
class RoomTakenError extends HotelError { constructor(message) { super(message); this.name = "RoomTakenError"; }}
const Protocol = Object.freeze({ FREE: "LIBRES", CHECK: "CONSULTAR", RESERVE: "RESERVAR", CANCEL: "ANULAR", EXIT: "SALIR",
OK: "OK", ERROR: "ERROR", BYE: "ADIÓS",});
class Reception { #hotel;
constructor(hotel) { this.#hotel = hotel; }
handle(line) { const colon = line.indexOf(":"); const command = colon < 0 ? line : line.slice(0, colon); const text = colon < 0 ? "" : line.slice(colon + 1); try { switch (command) { case Protocol.FREE: return `${Protocol.OK}:${this.#join(this.#hotel.freeRooms())}`; case Protocol.CHECK: return `${Protocol.OK}:${this.#hotel.check(this.#number(text))}`; case Protocol.RESERVE: return this.#reserve(text); case Protocol.CANCEL: return this.#cancel(text); default: throw new HotelError(`Comando desconocido ${command}.`); } } catch (e) { if (!(e instanceof HotelError)) throw e; return `${Protocol.ERROR}:${e.message}`; } }
#reserve(text) { const [numberText, guest] = this.#numberAndGuest(text); const number = this.#number(numberText); this.#hotel.reserve(number, guest); return `${Protocol.OK}:La habitación ${number} es tuya, ${guest}.`; }
#cancel(text) { const [numberText, guest] = this.#numberAndGuest(text); const number = this.#number(numberText); this.#hotel.cancel(number, guest); return `${Protocol.OK}:La reserva de la habitación ${number} está anulada.`; }
#numberAndGuest(text) { const semicolon = text.indexOf(";"); if (semicolon < 0) { throw new HotelError("Escribe número y nombre, por ejemplo «102;Ana Torres»."); } return [text.slice(0, semicolon), text.slice(semicolon + 1)]; }
#number(text) { const number = Number(text); if (text.trim() === "" || !Number.isInteger(number)) { throw new HotelError("El número de la habitación es un número entero."); } return number; }
#join(rooms) { if (rooms.length === 0) { return "No hay habitaciones libres."; } return rooms.join("; "); }}
class ClientHandler { #socket; #reception;
constructor(socket, reception) { this.#socket = socket; this.#reception = reception; }
async run() { try { for await (const line of createInterface({ input: this.#socket })) { if (line === Protocol.EXIT) { break; } this.#socket.write(`${this.#reception.handle(line)}\n`); } this.#socket.end(`${Protocol.BYE}\n`); } catch (e) { console.log(`Se cortó la conexión con el cliente: ${e.message}`); } }}
class HotelServer { #server; #port;
constructor(port, hotel) { const reception = new Reception(hotel); this.#port = port; this.#server = createServer((socket) => new ClientHandler(socket, reception).run()); }
start() { return new Promise((resolve) => this.#server.listen(this.#port, "localhost", resolve)); }
stop() { return new Promise((resolve) => this.#server.close(resolve)); }}
class HotelClient { #socket; #lines;
constructor(host, port) { this.#socket = createConnection({ host, port }); this.#lines = createInterface({ input: this.#socket })[Symbol.asyncIterator](); }
async send(command, text) { const line = text === undefined ? command : `${command}:${text}`; this.#socket.write(`${line}\n`); return (await this.#lines.next()).value; }
async close() { await this.send(Protocol.EXIT); this.#socket.end(); }}
async function say(who, client, command, text) { const reply = await client.send(command, text); console.log(`${who} > ${command}:${text}`); console.log(`${who} < ${reply}`);}
async function reserve(port, room, guest) { try { const client = new HotelClient("localhost", port); const reply = await client.send(Protocol.RESERVE, `${room};${guest}`); await client.close(); return reply; } catch (e) { return `${Protocol.ERROR}:${e.message}`; }}
async function race(port, room, guests) { const replies = await Promise.all(guests.map((guest) => reserve(port, room, guest))); return replies.filter((reply) => reply.startsWith(Protocol.OK)).length;}
const hotel = new Hotel();hotel.add(new Room(101, "individual", 60));hotel.add(new Room(102, "doble", 85));hotel.add(new Room(201, "doble", 95));hotel.add(new Room(202, "suite", 140));const server = new HotelServer(5353, hotel);await server.start();
const anna = new HotelClient("localhost", 5353);const boris = new HotelClient("localhost", 5353);await say("Ana", anna, Protocol.RESERVE, "102;Ana Torres");await say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva");await say("Bruno", boris, Protocol.CHECK, "102");await say("Bruno", boris, Protocol.CANCEL, "102;Bruno Silva");await say("Ana", anna, Protocol.CANCEL, "102;Ana Torres");await say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva");await say("Ana", anna, Protocol.CHECK, "305");await anna.close();await boris.close();
const two = ["Ana Torres", "Vera Gil"];console.log(`Dos para la habitación 201 a la vez, con éxito: ${await race(5353, 201, two)}`);const desk = new HotelClient("localhost", 5353);console.log(await desk.send(Protocol.FREE));await desk.close();await server.stop();Ejercicio
Sigue por tu cuenta
Trabaja con el programa completo de la tarjeta anterior.
1. Cambio de habitación. El huésped se muda de una habitación a otra con MOVER:102;202;Ana Torres. La respuesta es OK:Ana Torres está en la habitación 202. Si la habitación nueva está ocupada, el huésped se queda en la antigua. El cambio es un solo método en Hotel, síncrono, sin ningún await dentro: nadie debe ocupar la habitación nueva entre la liberación de la antigua y la ocupación de la nueva.
OK:La habitación 102 es tuya, Ana Torres.OK:La habitación 201 es tuya, Bruno Silva.ERROR:La habitación 201 ya está ocupada.OK:Ana Torres está en la habitación 202.OK:101, individual, 60 € por noche, libre; 102, doble, 85 € por noche, libreMás ideas
- Noches y total. Una reserva es para un número de noches, y la confirmación dice el total: «3 noches, 255 €».
- Fechas. Una habitación se reserva por días, por ejemplo del 14 al 16 de diciembre. Dos reservas de la misma habitación no deben solaparse. Si la habitación está libre depende de las fechas.
- Mis reservas.
MIAS:Ana Torresdice qué habitaciones son de este huésped. - Lista de espera. Si la habitación está ocupada, el huésped se apunta para esperar. Cuando se anula, pasa al primero de la lista.
-
Un cliente desde el teclado. Un segundo programa lee comandos del teclado con
ask()y los envía al servidor, que se ejecuta en otra terminal.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Funciones flecha · Métodos de arrays · Async, await e hilos · Programación en red
- Ejercicios sobre el tema: Proyecto