Хотелски резервации
Проблем
Една стая, две потвърждения
Малък хотел има четири стаи. Резервации идват отвсякъде: от сайта, от рецепцията, от туристически агенции. Една петъчна вечер две агенции резервират последната двойна стая в една и съща секунда. И двете получават потвърждение. В събота в стаята пристигат две семейства.
Ще направим система, в която сървърът пази стаите и резервациите, а клиентите пращат заявки по мрежата. Клиентът може:
- да види свободните стаи;
- да провери една стая;
- да резервира стая на свое име;
- да анулира своята резервация.
А когато двама клиенти резервират една стая едновременно, само единият успява. Това ще докажем с програма, която дава един и същ изход при всяко пускане.
Строим отдолу нагоре: моделът без мрежа, правилата и грешките, протоколът, сървърът и клиентът, едновременните заявки и накрая всичко в една програма.
По-точно, за любопитните
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 ? "свободна" : "заета"); }}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 + " € на нощ, " + (IsFree ? "свободна" : "заета"); }}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 = "свободна" if self._free else "заета" return f"{self._number}, {self._kind}, {self._price} € на нощ, {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 ? "свободна" : "заета"; return `${this.#number}, ${this.#type}, ${this.#price} € на нощ, ${state}`; }}- Номерът, типът и цената не се менят. Мени се само състоянието, и то с два метода с ясни имена:
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<>();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 е частен: търси стаята по номер или хвърля изключение. Така никой отвън не може да вземе стая и да я заеме покрай правилата:
private Room find(int number) { for (Room room : rooms) { if (room.getNumber() == number) { return room; } } throw new RoomNotFoundException("Няма стая " + number + ".");}private Room Find(int number){ Room room = rooms.FirstOrDefault(r => r.Number == number); if (room == null) { throw new RoomNotFoundException("Няма стая " + number + "."); } return room;}def _find(self, number): for room in self._rooms: if room.number == number: return room raise RoomNotFoundError(f"Няма стая {number}.")#find(number) { const room = this.#rooms.find((r) => r.number === number); if (room === undefined) { throw new RoomNotFoundError(`Няма стая ${number}.`); } return room;}Резервацията проверява името, намира стаята и проверява дали е свободна. Едва тогава я заема:
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);}public void Reserve(int number, string guest){ if (string.IsNullOrWhiteSpace(guest)) { throw new HotelException("Името на госта е задължително."); } Room room = Find(number); if (!room.IsFree) { throw new RoomTakenException("Стая " + number + " вече е заета."); } room.Occupy(); guests[number] = guest;}def reserve(self, number, guest): if not guest.strip(): raise HotelError("Името на госта е задължително.") room = self._find(number) if not room.is_free: raise RoomTakenError(f"Стая {number} вече е заета.") room.occupy() self._guests[number] = guestreserve(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);}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 € на нощ, заетаСвободни стаи: 3Hotel 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));
foreach (Room room in hotel.FreeRooms()){ Console.WriteLine(room);}hotel.Reserve(102, "Анна Петрова");Console.WriteLine("След резервацията: " + hotel.Check(102));Console.WriteLine("Свободни стаи: " + hotel.FreeRooms().Count);101, единична, 60 € на нощ, свободна102, двойна, 85 € на нощ, свободна201, двойна, 95 € на нощ, свободна202, апартамент, 140 € на нощ, свободнаСлед резервацията: 102, двойна, 85 € на нощ, заетаСвободни стаи: 3hotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))
for room in hotel.free_rooms(): print(room)hotel.reserve(102, "Анна Петрова")print(f"След резервацията: {hotel.check(102)}")print(f"Свободни стаи: {len(hotel.free_rooms())}")101, единична, 60 € на нощ, свободна102, двойна, 85 € на нощ, свободна201, двойна, 95 € на нощ, свободна202, апартамент, 140 € на нощ, свободнаСлед резервацията: 102, двойна, 85 € на нощ, заетаСвободни стаи: 3const 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 (const room of hotel.freeRooms()) { console.log(String(room));}hotel.reserve(102, "Анна Петрова");console.log(`След резервацията: ${hotel.check(102)}`);console.log(`Свободни стаи: ${hotel.freeRooms().length}`);101, единична, 60 € на нощ, свободна102, двойна, 85 € на нощ, свободна201, двойна, 95 € на нощ, свободна202, апартамент, 140 € на нощ, свободнаСлед резервацията: 102, двойна, 85 € на нощ, заетаСвободни стаи: 3String(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 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"; }}Анулирането проверява, че стаята е заета и че я анулира същият гост:
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);}public void Cancel(int number, string guest){ Room room = Find(number); if (room.IsFree) { throw new HotelException("Стая " + number + " не е резервирана."); } if (guests[number] != guest) { throw new HotelException("Стая " + number + " е резервирана от друг гост."); } room.Release(); guests.Remove(number);}def cancel(self, number, guest): room = self._find(number) if room.is_free: raise HotelError(f"Стая {number} не е резервирана.") if self._guests[number] != guest: raise HotelError(f"Стая {number} е резервирана от друг гост.") room.release() del self._guests[number]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);}Условието на проекта иска системата да се провери в различни сценарии: резервация, проверка и анулиране, и когато са правилни, и когато не са. Всеки сценарий е ред в изхода:
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. Без име: Името на госта е задължително.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, "Анна Петрова");Console.WriteLine("1. Свободна стая: " + hotel.Check(102));try{ hotel.Reserve(102, "Борис Илиев");}catch (RoomTakenException e){ Console.WriteLine("2. Заета стая: " + e.Message);}try{ hotel.Cancel(102, "Борис Илиев");}catch (HotelException e){ Console.WriteLine("3. Чужда резервация: " + e.Message);}hotel.Cancel(102, "Анна Петрова");Console.WriteLine("4. Анулиране: " + hotel.Check(102));try{ hotel.Cancel(102, "Анна Петрова");}catch (HotelException e){ Console.WriteLine("5. Второ анулиране: " + e.Message);}try{ hotel.Reserve(305, "Анна Петрова");}catch (RoomNotFoundException e){ Console.WriteLine("6. Няма такава стая: " + e.Message);}try{ hotel.Reserve(201, " ");}catch (HotelException e){ Console.WriteLine("7. Без име: " + e.Message);}1. Свободна стая: 102, двойна, 85 € на нощ, заета2. Заета стая: Стая 102 вече е заета.3. Чужда резервация: Стая 102 е резервирана от друг гост.4. Анулиране: 102, двойна, 85 € на нощ, свободна5. Второ анулиране: Стая 102 не е резервирана.6. Няма такава стая: Няма стая 305.7. Без име: Името на госта е задължително.hotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))
hotel.reserve(102, "Анна Петрова")print(f"1. Свободна стая: {hotel.check(102)}")try: hotel.reserve(102, "Борис Илиев")except RoomTakenError as e: print(f"2. Заета стая: {e}")try: hotel.cancel(102, "Борис Илиев")except HotelError as e: print(f"3. Чужда резервация: {e}")hotel.cancel(102, "Анна Петрова")print(f"4. Анулиране: {hotel.check(102)}")try: hotel.cancel(102, "Анна Петрова")except HotelError as e: print(f"5. Второ анулиране: {e}")try: hotel.reserve(305, "Анна Петрова")except RoomNotFoundError as e: print(f"6. Няма такава стая: {e}")try: hotel.reserve(201, " ")except HotelError as e: print(f"7. Без име: {e}")1. Свободна стая: 102, двойна, 85 € на нощ, заета2. Заета стая: Стая 102 вече е заета.3. Чужда резервация: Стая 102 е резервирана от друг гост.4. Анулиране: 102, двойна, 85 € на нощ, свободна5. Второ анулиране: Стая 102 не е резервирана.6. Няма такава стая: Няма стая 305.7. Без име: Името на госта е задължително.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(102, "Анна Петрова");console.log(`1. Свободна стая: ${hotel.check(102)}`);try { hotel.reserve(102, "Борис Илиев");} catch (e) { if (!(e instanceof RoomTakenError)) throw e; console.log(`2. Заета стая: ${e.message}`);}try { hotel.cancel(102, "Борис Илиев");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`3. Чужда резервация: ${e.message}`);}hotel.cancel(102, "Анна Петрова");console.log(`4. Анулиране: ${hotel.check(102)}`);try { hotel.cancel(102, "Анна Петрова");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`5. Второ анулиране: ${e.message}`);}try { hotel.reserve(305, "Анна Петрова");} catch (e) { if (!(e instanceof RoomNotFoundError)) throw e; console.log(`6. Няма такава стая: ${e.message}`);}try { hotel.reserve(201, " ");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`7. Без име: ${e.message}`);}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);Анна заема 101 и после я освобождава. Борис заема 201, затова Вера получава RoomTakenError и редът „Вера е в 201“ не се изпълнява. Заета е само 201, свободни са три стаи.
Идеята
Протоколът: ред „КОМАНДА:текст“
Всяко съобщение е един ред КОМАНДА:текст, а сървърът отговаря с един ред:
| Клиентът праща | Сървърът отговаря |
|---|---|
СВОБОДНИ |
ОК: и свободните стаи, разделени с ; |
ПРОВЕРИ: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);}Стъпка по стъпка, с думи
- Стая 102 е свободна. Анна Петрова и Борис Илиев я резервират в един и същ миг.
- Първо с await между проверката и заемането, както ако стаите са в база данни. Заявката на Анна проверява, вижда „свободна“ и чака.
- Докато Анна чака, заявката на Борис проверява: и тя вижда „свободна“.
- Анна заема стаята и записва името си.
- Борис също заема стаята и записва своето име. Двамата имат потвърждение за една стая.
- Отначало, без await: reserve е синхронна и върви докрай, преди да тръгне друг код.
- Да кажем, че първа е Анна: нейната заявка е на ход. Проверката и заемането са една стъпка, никой не влиза между тях.
- Заявката на Борис е пристигнала и чака: нишката е една и изпълнява едно нещо наведнъж.
- Сега е ред на Борис. Проверката вече вижда „заета“.
- RoomTakenError. Рецепцията го връща като ред с грешка, а Борис може да пита за друга стая.
- Програмата от урока брои успелите: по 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 методи. Рецепцията чака първо проверката, после заемането. Двама клиенти наведнъж получават една стая при всяко пускане. На кой ред е грешката?Избери реда с грешката.
await this.#hotel.isFree(number) заявката спира и чака. Точно тогава друга заявка проверява същата стая и я вижда свободна. Проверката и заемането трябва да са една стъпка без await между тях, както в нашата reserve.Виж го
Целият хотел
Тестовите сценарии от условието, този път по мрежата, с двама клиенти, свързани наведнъж. Накрая двама резервират 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 € на нощ, свободнаПо-точно, за любопитните
Цялата програма, от началото до края:
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()и ги праща на сървъра, пуснат в друг терминал.
Тест