Към съдържанието
wannacode

Хотелски резервации

Проблем

Една стая, две потвърждения

Малък хотел има четири стаи. Резервации идват отвсякъде: от сайта, от рецепцията, от туристически агенции. Една петъчна вечер две агенции резервират последната двойна стая в една и съща секунда. И двете получават потвърждение. В събота в стаята пристигат две семейства.

Ще направим система, в която сървърът пази стаите и резервациите, а клиентите пращат заявки по мрежата. Клиентът може:

  • да види свободните стаи;
  • да провери една стая;
  • да резервира стая на свое име;
  • да анулира своята резервация.

А когато двама клиенти резервират една стая едновременно, само единият успява. Това ще докажем с програма, която дава един и същ изход при всяко пускане.

Строим отдолу нагоре: моделът без мрежа, правилата и грешките, протоколът, сървърът и клиентът, едновременните заявки и накрая всичко в една програма.

По-точно, за любопитните
Моделът е един и същ на четирите езика на курса. В JavaScript, както в Java, клиентите пращат редове текст по сокет (модулът node:net). Нишката обаче е една: Node.js обслужва всички клиенти в нея и минава от един към друг, докато чакат мрежата. Ключалка няма. Какво пази стаите тогава, ще видим към края.

Идеята

Стаята

Стаята има номер, тип, цена за нощ и състояние: свободна или заета.

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 + " € на нощ, " + (free ? "свободна" : "заета");
}
}
  • Номерът, типът и цената не се менят. Мени се само състоянието, и то с два метода с ясни имена: occupy и release.
  • Кой е отседнал, стаята не знае. Стаята стига до всеки клиент, който пита, а името на госта не бива да излиза от хотела.
По-точно, за любопитните
Полетата с # са частни: отвън не се виждат изобщо. Номерът, типът и цената се четат през getter без скоби (room.number), а смяна отвън (room.number = 5) във файл с import спира с TypeError, защото getter-ът няма setter. Енумерации в JavaScript няма. За повече от две състояния се ползва замразен обект с имената им, както Protocol по-долу.

Виж го

Хотелът и резервацията

Хотелът държи стаите в масив, а гостите в Map „номер на стая → име“:
private final List<Room> rooms = new ArrayList<>();
private final Map<Integer, String> guests = new HashMap<>();
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);
}

#find е частен: търси стаята по номер или хвърля изключение. Така никой отвън не може да вземе стая и да я заеме покрай правилата:

private Room find(int number) {
for (Room room : rooms) {
if (room.getNumber() == number) {
return room;
}
}
throw new RoomNotFoundException("Няма стая " + number + ".");
}

Резервацията проверява името, намира стаята и проверява дали е свободна. Едва тогава я заема:

public void reserve(int number, String guest) {
if (guest.isBlank()) {
throw new HotelException("Името на госта е задължително.");
}
Room room = find(number);
if (!room.isFree()) {
throw new RoomTakenException("Стая " + number + " вече е заета.");
}
room.occupy();
guests.put(number, guest);
}
Hotel hotel = new Hotel();
hotel.add(new Room(101, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
for (Room room : hotel.freeRooms()) {
System.out.println(room);
}
hotel.reserve(102, "Анна Петрова");
System.out.println("След резервацията: " + hotel.check(102));
System.out.println("Свободни стаи: " + hotel.freeRooms().size());
Изход
101, единична, 60 € на нощ, свободна
102, двойна, 85 € на нощ, свободна
201, двойна, 95 € на нощ, свободна
202, апартамент, 140 € на нощ, свободна
След резервацията: 102, двойна, 85 € на нощ, заета
Свободни стаи: 3
String(room) и шаблонният низ викат toString(). console.log(room) не го вика: той показва полетата на обекта, а частните не се виждат, и печата само Room {}.

Идеята

Анулиране и правилата

Не всяка заявка е изпълнима: стаята е заета, номер 305 няма, гостът анулира чужда резервация. Това са откази по правилата, затова са свои изключения с общ родител:

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 void cancel(int number, String guest) {
Room room = find(number);
if (room.isFree()) {
throw new HotelException("Стая " + number + " не е резервирана.");
}
if (!guests.get(number).equals(guest)) {
throw new HotelException("Стая " + number + " е резервирана от друг гост.");
}
room.release();
guests.remove(number);
}

Условието на проекта иска системата да се провери в различни сценарии: резервация, проверка и анулиране, и когато са правилни, и когато не са. Всеки сценарий е ред в изхода:

Hotel hotel = new Hotel();
hotel.add(new Room(101, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
hotel.reserve(102, "Анна Петрова");
System.out.println("1. Свободна стая: " + hotel.check(102));
try {
hotel.reserve(102, "Борис Илиев");
} catch (RoomTakenException e) {
System.out.println("2. Заета стая: " + e.getMessage());
}
try {
hotel.cancel(102, "Борис Илиев");
} catch (HotelException e) {
System.out.println("3. Чужда резервация: " + e.getMessage());
}
hotel.cancel(102, "Анна Петрова");
System.out.println("4. Анулиране: " + hotel.check(102));
try {
hotel.cancel(102, "Анна Петрова");
} catch (HotelException e) {
System.out.println("5. Второ анулиране: " + e.getMessage());
}
try {
hotel.reserve(305, "Анна Петрова");
} catch (RoomNotFoundException e) {
System.out.println("6. Няма такава стая: " + e.getMessage());
}
try {
hotel.reserve(201, " ");
} catch (HotelException e) {
System.out.println("7. Без име: " + e.getMessage());
}
Изход
1. Свободна стая: 102, двойна, 85 € на нощ, заета
2. Заета стая: Стая 102 вече е заета.
3. Чужда резервация: Стая 102 е резервирана от друг гост.
4. Анулиране: 102, двойна, 85 € на нощ, свободна
5. Второ анулиране: Стая 102 не е резервирана.
6. Няма такава стая: Няма стая 305.
7. Без име: Името на госта е задължително.

Конкретното изключение казва какъв е проблемът. HotelError хваща всички откази наведнъж: сървърът ще хваща само него и ще го превръща в отговор за клиента.

catch в JavaScript няма тип и хваща всичко. Затова всеки catch пита с instanceof дали грешката е неговата, а чуждата хвърля нататък с throw e.
По-точно, за любопитните
  • Проверени изключения няма: никой метод не обявява какво хвърля. Затова рецепцията ще ги хване на едно място.
  • Сценарии като тези са естествени тестове. С node:test всеки става test(...), а assert.throws(() => hotel.reserve(102, "Борис Илиев"), RoomTakenError) проверява изключението.

Пробвай

Кой е в 201

Какво ще отпечата програмата?

const hotel = new Hotel();
hotel.add(new Room(101, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
hotel.reserve(101, "Анна Петрова");
hotel.reserve(201, "Борис Илиев");
hotel.cancel(101, "Анна Петрова");
try {
hotel.reserve(201, "Вера Колева");
console.log("Вера е в 201");
} catch (e) {
if (!(e instanceof RoomTakenError)) throw e;
console.log("Отказ за Вера");
}
console.log(hotel.freeRooms().length);
Отговор

Идеята

Протоколът: ред „КОМАНДА:текст“

Всяко съобщение е един ред КОМАНДА:текст, а сървърът отговаря с един ред:

Клиентът праща Сървърът отговаря
СВОБОДНИ ОК: и свободните стаи, разделени с ;
ПРОВЕРИ:102 ОК:102, двойна, 85 € на нощ, заета
РЕЗЕРВИРАЙ:102;Анна Петрова ОК:Стая 102 е твоя, Анна Петрова. или ГРЕШКА: и причината
АНУЛИРАЙ:102;Анна Петрова ОК:Резервацията за стая 102 е анулирана. или ГРЕШКА: и причината
КРАЙ ДОВИЖДАНЕ и сървърът затваря връзката

Думите на протокола са константи на едно място (Object.freeze не дава обектът да се променя):

const Protocol = Object.freeze({
FREE: "СВОБОДНИ",
CHECK: "ПРОВЕРИ",
RESERVE: "РЕЗЕРВИРАЙ",
CANCEL: "АНУЛИРАЙ",
EXIT: "КРАЙ",
OK: "ОК",
ERROR: "ГРЕШКА",
BYE: "ДОВИЖДАНЕ",
});

Рецепцията превежда ред от протокола в извикване на хотела и обратно:

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(`Непозната команда ${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}:Стая ${number} е твоя, ${guest}.`;
}
#cancel(text) {
const [numberText, guest] = this.#numberAndGuest(text);
const number = this.#number(numberText);
this.#hotel.cancel(number, guest);
return `${Protocol.OK}:Резервацията за стая ${number} е анулирана.`;
}
#numberAndGuest(text) {
const semicolon = text.indexOf(";");
if (semicolon < 0) {
throw new HotelError("Пиши номер и име, например „102;Анна Петрова“.");
}
return [text.slice(0, semicolon), text.slice(semicolon + 1)];
}
#number(text) {
const number = Number(text);
if (text.trim() === "" || !Number.isInteger(number)) {
throw new HotelError("Номерът на стаята е цяло число.");
}
return number;
}
#join(rooms) {
if (rooms.length === 0) {
return "Няма свободни стаи.";
}
return rooms.join("; ");
}
catch пита дали грешката е HotelError. Ако не е, това е бъг в кода, а не отказ по правилата, и тя продължава нагоре. Number(text) не хвърля: за „сто и две“ дава NaN, а за празен текст 0. Затова #number проверява сам, че текстът не е празен и числото е цяло.

Рецепцията не помни нищо за клиента: всеки ред носи всичко нужно, дори името на госта. Затова една рецепция стига за всички клиенти. Проверява се без мрежа:

const hotel = new Hotel();
hotel.add(new Room(101, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
const reception = new Reception(hotel);
const lines = [
"РЕЗЕРВИРАЙ:102;Анна Петрова",
"ПРОВЕРИ:102",
"РЕЗЕРВИРАЙ:102;Борис Илиев",
"АНУЛИРАЙ:102",
"ПРОВЕРИ:сто и две",
"ПОКАЖИ",
];
for (const line of lines) {
console.log(`> ${line}`);
console.log(reception.handle(line));
}
Изход
> РЕЗЕРВИРАЙ:102;Анна Петрова
ОК:Стая 102 е твоя, Анна Петрова.
> ПРОВЕРИ:102
ОК:102, двойна, 85 € на нощ, заета
> РЕЗЕРВИРАЙ:102;Борис Илиев
ГРЕШКА:Стая 102 вече е заета.
> АНУЛИРАЙ:102
ГРЕШКА:Пиши номер и име, например „102;Анна Петрова“.
> ПРОВЕРИ:сто и две
ГРЕШКА:Номерът на стаята е цяло число.
> ПОКАЖИ
ГРЕШКА:Непозната команда ПОКАЖИ.
По-точно, за любопитните
  • Енумерации в JavaScript няма. Замразеният обект е най-близкото: затворен списък от имена, който никой не може да промени. Във файл с import опитът Protocol.FREE = "FREE" спира с TypeError, а Protocol.FREE остава "СВОБОДНИ".
  • Протоколът е без състояние: всеки ред е сам за себе си. Това го прави лесен за проверка и за повече клиенти. Ако клиентът трябваше първо да каже „аз съм Анна“, рецепцията щеше да е по една на клиент, както сесията в автокъщата.

Виж го

Сървърът

Басейн от нишки няма. createServer вика функцията си за всеки нов клиент, а неговият ClientHandler чете редовете му с for await. Докато клиентът мълчи, нишката обслужва другите: await не я спира, а казва „продължи оттук, когато дойде ред“.

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(`Връзката с клиента прекъсна: ${e.message}`);
}
}
}

HotelServer пуска сървъра на порта. listen и close не стават веднага, затова start и stop връщат Promise и се викат с 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));
}
}
  • Колкото и клиенти да има, всички се обслужват от една нишка. Тя не чака мрежата: върши друго, докато редът дойде.
  • Всички клиенти викат един и същ Hotel. Затова хотелът ще трябва да пази стаите си. Как, след малко.

Виж го

Клиентът

Клиентът праща ред и чака отговора с await. Редовете от сокета се четат със същия createInterface, с който четем от клавиатурата: next() дава следващия ред, щом пристигне. Без текст send праща само командата. close() казва КРАЙ и затваря връзката:
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();
}
}

Сървърът и клиентът в една програма:

const hotel = new Hotel();
hotel.add(new Room(101, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
const server = new HotelServer(5321, hotel);
await server.start();
const client = new HotelClient("localhost", 5321);
console.log(await client.send(Protocol.RESERVE, "102;Анна Петрова"));
console.log(await client.send(Protocol.CHECK, "102"));
console.log(await client.send(Protocol.FREE));
console.log(await client.send(Protocol.CANCEL, "102;Анна Петрова"));
console.log(await client.send(Protocol.CHECK, "305"));
await client.close();
await server.stop();
Изход
ОК:Стая 102 е твоя, Анна Петрова.
ОК:102, двойна, 85 € на нощ, заета
ОК:101, единична, 60 € на нощ, свободна; 201, двойна, 95 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
ОК:Резервацията за стая 102 е анулирана.
ГРЕШКА:Няма стая 305.

Идеята

Двама за една стая

Резервацията е две стъпки: провери, че стаята е свободна, после я заеми. Ако заявката спре и чака между тях (await), докато върви друга, може да стане това:
Агенция А Агенция Б
1 проверява: 102 е свободна
2 проверява: 102 е свободна
3 заема 102 за своя гост
4 заема 102 за своя гост и записва него

И двете получават потвърждение. Няма грешка в нито един ред код, грешката е в това, че стъпките на двете заявки се преплитат.

Решението: никой не бива да влиза между проверката и заемането. Тогава „провери и заеми“ е неделимо. В JavaScript нашата резервация вече е такава. Node.js изпълнява кода в една нишка, а синхронна функция върви докрай, преди да започне друг код. В reserve няма await, затова никоя заявка не може да влезе по средата. Ключалка не трябва:

reserve(number, guest) {
if (guest.trim() === "") {
throw new HotelError("Името на госта е задължително.");
}
const room = this.#find(number);
if (!room.isFree) {
throw new RoomTakenError(`Стая ${number} вече е заета.`);
}
room.occupy();
this.#guests.set(number, guest);
}
Опасно е await насред резервацията. Ако стаите бяха в база данни, проверката щеше да чака отговор от нея с await. Докато тя чака, друга заявка проверява същата стая и я вижда свободна. Затова „провери и заеми“ трябва да е една синхронна стъпка или една заявка към базата.

reserve(number, guest) {
if (guest.trim() === "") {
throw new HotelError("Името на госта е задължително.");
}
const room = this.#find(number);
if (!room.isFree) {
throw new RoomTakenError(`Стая ${number} вече е заета.`);
}
room.occupy();
this.#guests.set(number, guest);
}
Стъпка по стъпка, с думи
  1. Стая 102 е свободна. Анна Петрова и Борис Илиев я резервират в един и същ миг.
  2. Първо с await между проверката и заемането, както ако стаите са в база данни. Заявката на Анна проверява, вижда „свободна“ и чака.
  3. Докато Анна чака, заявката на Борис проверява: и тя вижда „свободна“.
  4. Анна заема стаята и записва името си.
  5. Борис също заема стаята и записва своето име. Двамата имат потвърждение за една стая.
  6. Отначало, без await: reserve е синхронна и върви докрай, преди да тръгне друг код.
  7. Да кажем, че първа е Анна: нейната заявка е на ход. Проверката и заемането са една стъпка, никой не влиза между тях.
  8. Заявката на Борис е пристигнала и чака: нишката е една и изпълнява едно нещо наведнъж.
  9. Сега е ред на Борис. Проверката вече вижда „заета“.
  10. RoomTakenError. Рецепцията го връща като ред с грешка, а Борис може да пита за друга стая.
  11. Програмата от урока брои успелите: по 1 за всяка стая. Кой е бил пръв, е различно при всяко пускане.

Доказателството: двама клиенти резервират 102 наведнъж, после двадесет агенции резервират 201 наведнъж. Отговорите се събират, а програмата печата колко са успели:

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, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
const server = new HotelServer(5322, hotel);
await server.start();
const two = ["Анна Петрова", "Борис Илиев"];
console.log(`Двама за стая 102, успешни: ${await race(5322, 102, two)}`);
const agencies = [];
for (let i = 1; i <= 20; i++) {
agencies.push(`Агенция ${i}`);
}
console.log(`Двадесет за стая 201, успешни: ${await race(5322, 201, agencies)}`);
const desk = new HotelClient("localhost", 5322);
console.log(await desk.send(Protocol.FREE));
await desk.close();
await server.stop();
Изход
Двама за стая 102, успешни: 1
Двадесет за стая 201, успешни: 1
ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
Всеки клиент е своя връзка. Promise.all пуска всички заявки наведнъж и чака отговорите, а race брои успелите. Нишката е една, а заявките пак се преплитат: всяка чака мрежата, докато другите вървят.

Кой е спечелил стаята, е различно при всяко пускане, затова програмата не го печата. Колко са успели, е винаги едно и също: по един за всяка стая.

По-точно, за любопитните
  • С await между проверката и заемането грешката не е рядка: двама получават стаята при всяко пускане (виж следващата карта). Затова всеки await е място, на което да спреш и да помислиш: какво може да промени друга заявка, докато тази чака.
  • Ако все пак трябва await насред резервацията, заявките за една стая се нареждат една след друга, например с опашка от обещания (Promise) или с библиотека за ключалки. В този проект не трябва.
  • В истинска система стаите са в база данни и пазенето е там: транзакция или условие в самата заявка (UPDATE rooms SET free = false WHERE number = 102 AND free = true). Тогава важи и при няколко сървъра, а не само в една програма.

Пробвай

Заключено, но не съвсем

Стаите ще се пазят в база данни, затова колега направи проверката и заемането отделни async методи. Рецепцията чака първо проверката, после заемането. Двама клиенти наведнъж получават една стая при всяко пускане. На кой ред е грешката?

Избери реда с грешката.

Виж го

Целият хотел

Тестовите сценарии от условието, този път по мрежата, с двама клиенти, свързани наведнъж. Накрая двама резервират 201 в един миг:

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, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
const server = new HotelServer(5323, hotel);
await server.start();
const anna = new HotelClient("localhost", 5323);
const boris = new HotelClient("localhost", 5323);
await say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова");
await say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
await say("Борис", boris, Protocol.CHECK, "102");
await say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев");
await say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова");
await say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
await say("Анна", anna, Protocol.CHECK, "305");
await anna.close();
await boris.close();
const two = ["Анна Петрова", "Вера Колева"];
console.log(`Двама за стая 201 наведнъж, успешни: ${await race(5323, 201, two)}`);
const desk = new HotelClient("localhost", 5323);
console.log(await desk.send(Protocol.FREE));
await desk.close();
await server.stop();
Изход
Анна > РЕЗЕРВИРАЙ:102;Анна Петрова
Анна < ОК:Стая 102 е твоя, Анна Петрова.
Борис > РЕЗЕРВИРАЙ:102;Борис Илиев
Борис < ГРЕШКА:Стая 102 вече е заета.
Борис > ПРОВЕРИ:102
Борис < ОК:102, двойна, 85 € на нощ, заета
Борис > АНУЛИРАЙ:102;Борис Илиев
Борис < ГРЕШКА:Стая 102 е резервирана от друг гост.
Анна > АНУЛИРАЙ:102;Анна Петрова
Анна < ОК:Резервацията за стая 102 е анулирана.
Борис > РЕЗЕРВИРАЙ:102;Борис Илиев
Борис < ОК:Стая 102 е твоя, Борис Илиев.
Анна > ПРОВЕРИ:305
Анна < ГРЕШКА:Няма стая 305.
Двама за стая 201 наведнъж, успешни: 1
ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
По-точно, за любопитните

Цялата програма, от началото до края:

hotel.js
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 ? "свободна" : "заета";
return `${this.#number}, ${this.#type}, ${this.#price} € на нощ, ${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("Името на госта е задължително.");
}
const room = this.#find(number);
if (!room.isFree) {
throw new RoomTakenError(`Стая ${number} вече е заета.`);
}
room.occupy();
this.#guests.set(number, guest);
}
cancel(number, guest) {
const room = this.#find(number);
if (room.isFree) {
throw new HotelError(`Стая ${number} не е резервирана.`);
}
if (this.#guests.get(number) !== guest) {
throw new HotelError(`Стая ${number} е резервирана от друг гост.`);
}
room.release();
this.#guests.delete(number);
}
#find(number) {
const room = this.#rooms.find((r) => r.number === number);
if (room === undefined) {
throw new RoomNotFoundError(`Няма стая ${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: "СВОБОДНИ",
CHECK: "ПРОВЕРИ",
RESERVE: "РЕЗЕРВИРАЙ",
CANCEL: "АНУЛИРАЙ",
EXIT: "КРАЙ",
OK: "ОК",
ERROR: "ГРЕШКА",
BYE: "ДОВИЖДАНЕ",
});
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(`Непозната команда ${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}:Стая ${number} е твоя, ${guest}.`;
}
#cancel(text) {
const [numberText, guest] = this.#numberAndGuest(text);
const number = this.#number(numberText);
this.#hotel.cancel(number, guest);
return `${Protocol.OK}:Резервацията за стая ${number} е анулирана.`;
}
#numberAndGuest(text) {
const semicolon = text.indexOf(";");
if (semicolon < 0) {
throw new HotelError("Пиши номер и име, например „102;Анна Петрова“.");
}
return [text.slice(0, semicolon), text.slice(semicolon + 1)];
}
#number(text) {
const number = Number(text);
if (text.trim() === "" || !Number.isInteger(number)) {
throw new HotelError("Номерът на стаята е цяло число.");
}
return number;
}
#join(rooms) {
if (rooms.length === 0) {
return "Няма свободни стаи.";
}
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(`Връзката с клиента прекъсна: ${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, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
const server = new HotelServer(5323, hotel);
await server.start();
const anna = new HotelClient("localhost", 5323);
const boris = new HotelClient("localhost", 5323);
await say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова");
await say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
await say("Борис", boris, Protocol.CHECK, "102");
await say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев");
await say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова");
await say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
await say("Анна", anna, Protocol.CHECK, "305");
await anna.close();
await boris.close();
const two = ["Анна Петрова", "Вера Колева"];
console.log(`Двама за стая 201 наведнъж, успешни: ${await race(5323, 201, two)}`);
const desk = new HotelClient("localhost", 5323);
console.log(await desk.send(Protocol.FREE));
await desk.close();
await server.stop();

Упражнение

Продължи сам

Работи с цялата програма от предишната карта.

1. Смяна на стая. Гостът се мести от една стая в друга с ПРЕМЕСТИ:102;202;Анна Петрова. Отговорът е ОК:Анна Петрова е в стая 202. Ако новата стая е заета, гостът остава в старата. Смяната е един метод в Hotel, синхронен, без await вътре: никой не бива да заеме новата стая между освобождаването на старата и заемането на новата.

Изход
ОК:Стая 102 е твоя, Анна Петрова.
ОК:Стая 201 е твоя, Борис Илиев.
ГРЕШКА:Стая 201 вече е заета.
ОК:Анна Петрова е в стая 202.
ОК:101, единична, 60 € на нощ, свободна; 102, двойна, 85 € на нощ, свободна

Още идеи

  • Нощувки и сума. Резервацията е за брой нощи, а потвърждението казва сумата: „3 нощи, 255 €“.
  • Дати. Стаята се резервира за дни, например от 14 до 16 декември. Две резервации за една стая не бива да се застъпват. Свободна ли е стаята, зависи от датите.
  • Моите резервации. МОИ:Анна Петрова казва кои стаи са на този гост.
  • Списък на чакащите. Ако стаята е заета, гостът се записва да чака. Когато се анулира, тя отива при първия в списъка.
  • Клиент от клавиатурата. Втора програма чете команди от клавиатурата с ask() и ги праща на сървъра, пуснат в друг терминал.

Тест

Провери се

1. Две агенции резервират стая 102 в един и същ миг, а между проверката и заемането има await. Какво става?
2. Защо нашата reserve няма ключалка?
3. Стая 102 е заета. Какво получава клиентът, който праща РЕЗЕРВИРАЙ:102;Борис Илиев?
4. Защо програмата печата колко резервации са успели, а не кой е спечелил стаята?
5. Защо имената на гостите са в речник в хотела, а не в стаята?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·