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

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

Проблем

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

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

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

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

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

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

По-точно, за любопитните
Моделът е един и същ в Java и C#. В Java клиентите пращат редове текст по сокет и всеки клиент е в своя нишка. В C# сървърът е уеб услуга с ASP.NET Core, а клиентът я вика с HttpClient. В двата езика една и съща ключова идея пази стаите: synchronized.

Идеята

Стаята

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

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.
  • Кой е отседнал, стаята не знае. Стаята стига до всеки клиент, който пита, а името на госта не бива да излиза от хотела.
По-точно, за любопитните
Състоянието е boolean free. Може и с текст („свободна“, „заета“, „в ремонт“), но тогава всяко сравнение е с низ и сгрешена буква е бъг. За повече от две състояния се ползва енумерация: enum RoomState { FREE, TAKEN, REPAIR }.

Виж го

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

Хотелът държи стаите в списък, а гостите в речник „номер на стая → име“:
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

Идеята

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

Не всяка заявка е изпълнима: стаята е заета, номер 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. Без име: Името на госта е задължително.

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

По-точно, за любопитните
  • Изключенията наследяват RuntimeException, затова методите не пишат throws. run() на нишка и ламбдите не могат да хвърлят проверени изключения.
  • Сценарии като тези са естествени тестове. С JUnit всеки става метод с @Test и assertThrows(RoomTakenException.class, () -> hotel.reserve(102, "Борис Илиев")).

Пробвай

Кой е в 201

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

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(101, "Анна Петрова");
hotel.reserve(201, "Борис Илиев");
hotel.cancel(101, "Анна Петрова");
try {
hotel.reserve(201, "Вера Колева");
System.out.println("Вера е в 201");
} catch (RoomTakenException e) {
System.out.println("Отказ за Вера");
}
System.out.println(hotel.freeRooms().size());
Отговор

Идеята

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

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

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

Думите на протокола са константи на едно място:

class Protocol {
public static final String FREE = "СВОБОДНИ";
public static final String CHECK = "ПРОВЕРИ";
public static final String RESERVE = "РЕЗЕРВИРАЙ";
public static final String CANCEL = "АНУЛИРАЙ";
public static final String EXIT = "КРАЙ";
public static final String OK = "ОК";
public static final String ERROR = "ГРЕШКА";
public static final String BYE = "ДОВИЖДАНЕ";
}

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

class Reception {
private final Hotel hotel;
public Reception(Hotel hotel) {
this.hotel = hotel;
}
...
}
public String handle(String line) {
int colon = line.indexOf(':');
String command = colon < 0 ? line : line.substring(0, colon);
String text = colon < 0 ? "" : line.substring(colon + 1);
try {
switch (command) {
case Protocol.FREE:
return Protocol.OK + ":" + join(hotel.freeRooms());
case Protocol.CHECK:
return Protocol.OK + ":" + hotel.check(number(text));
case Protocol.RESERVE:
return reserve(text);
case Protocol.CANCEL:
return cancel(text);
default:
throw new HotelException("Непозната команда " + command + ".");
}
} catch (HotelException e) {
return Protocol.ERROR + ":" + e.getMessage();
}
}
private String reserve(String text) {
String[] parts = numberAndGuest(text);
int number = number(parts[0]);
hotel.reserve(number, parts[1]);
return Protocol.OK + ":Стая " + number + " е твоя, " + parts[1] + ".";
}
private String cancel(String text) {
String[] parts = numberAndGuest(text);
int number = number(parts[0]);
hotel.cancel(number, parts[1]);
return Protocol.OK + ":Резервацията за стая " + number + " е анулирана.";
}
private static String[] numberAndGuest(String text) {
String[] parts = text.split(";", 2);
if (parts.length != 2) {
throw new HotelException("Пиши номер и име, например „102;Анна Петрова“.");
}
return parts;
}
private static int number(String text) {
try {
return Integer.parseInt(text.trim());
} catch (NumberFormatException e) {
throw new HotelException("Номерът на стаята е цяло число.");
}
}
private static String join(List<Room> rooms) {
if (rooms.isEmpty()) {
return "Няма свободни стаи.";
}
return String.join("; ", rooms.stream().map(Room::toString).toList());
}

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

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));
Reception reception = new Reception(hotel);
String[] lines = {
"РЕЗЕРВИРАЙ:102;Анна Петрова",
"ПРОВЕРИ:102",
"РЕЗЕРВИРАЙ:102;Борис Илиев",
"АНУЛИРАЙ:102",
"ПРОВЕРИ:сто и две",
"ПОКАЖИ"
};
for (String line : lines) {
System.out.println("> " + line);
System.out.println(reception.handle(line));
}
Изход
> РЕЗЕРВИРАЙ:102;Анна Петрова
ОК:Стая 102 е твоя, Анна Петрова.
> ПРОВЕРИ:102
ОК:102, двойна, 85 € на нощ, заета
> РЕЗЕРВИРАЙ:102;Борис Илиев
ГРЕШКА:Стая 102 вече е заета.
> АНУЛИРАЙ:102
ГРЕШКА:Пиши номер и име, например „102;Анна Петрова“.
> ПРОВЕРИ:сто и две
ГРЕШКА:Номерът на стаята е цяло число.
> ПОКАЖИ
ГРЕШКА:Непозната команда ПОКАЖИ.
По-точно, за любопитните
  • Командите могат да са енумерация: enum Command { FREE, CHECK, RESERVE, CANCEL, EXIT }. Тогава са отделен тип със затворен списък от стойности. В курса са константи, защото енумерации не сме учили.
  • Протоколът е без състояние: всеки ред е сам за себе си. Това го прави лесен за проверка и за повече клиенти. Ако клиентът трябваше първо да каже „аз съм Анна“, рецепцията щеше да е по една на клиент, както сесията в автокъщата.

Виж го

Сървърът

Всеки свързан клиент получава свой ClientHandler, а рецепцията е обща:

class ClientHandler implements Runnable {
private final Socket socket;
private final Reception reception;
public ClientHandler(Socket socket, Reception reception) {
this.socket = socket;
this.reception = reception;
}
@Override
public void run() {
try (socket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String line;
while ((line = in.readLine()) != null && !line.equals(Protocol.EXIT)) {
out.println(reception.handle(line));
}
out.println(Protocol.BYE);
} catch (IOException e) {
System.out.println("Връзката с клиента прекъсна: " + e.getMessage());
}
}
}

HotelServer приема клиентите в своя нишка и дава всеки на басейн от 10 нишки:

class HotelServer {
private final ServerSocket serverSocket;
private final ExecutorService pool = Executors.newFixedThreadPool(10);
private final Reception reception;
public HotelServer(int port, Hotel hotel) throws IOException {
this.serverSocket = new ServerSocket(port);
this.reception = new Reception(hotel);
}
public void start() {
Thread acceptor = new Thread(() -> acceptAll());
acceptor.setDaemon(true);
acceptor.start();
}
private void acceptAll() {
while (true) {
try {
Socket client = serverSocket.accept();
pool.execute(new ClientHandler(client, reception));
} catch (IOException e) {
return;
}
}
}
public void stop() throws IOException {
serverSocket.close();
pool.shutdown();
}
}
  • Десет клиенти се обслужват наведнъж. Единадесетият е приет и чака в опашката на басейна, докато някой не си тръгне.
  • И десетте нишки викат един и същ Hotel. Затова хотелът ще трябва да пази стаите си. Как, след малко.

Виж го

Клиентът

Клиентът праща ред и чете отговора. close() казва КРАЙ и затваря връзката:
class HotelClient {
private final Socket socket;
private final BufferedReader in;
private final PrintWriter out;
public HotelClient(String host, int port) throws IOException {
socket = new Socket(host, port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
}
public String send(String command, String text) throws IOException {
out.println(command + ":" + text);
return in.readLine();
}
public String send(String command) throws IOException {
out.println(command);
return in.readLine();
}
public void close() throws IOException {
send(Protocol.EXIT);
socket.close();
}
}

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

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));
HotelServer server = new HotelServer(5321, hotel);
server.start();
HotelClient client = new HotelClient("localhost", 5321);
System.out.println(client.send(Protocol.RESERVE, "102;Анна Петрова"));
System.out.println(client.send(Protocol.CHECK, "102"));
System.out.println(client.send(Protocol.FREE));
System.out.println(client.send(Protocol.CANCEL, "102;Анна Петрова"));
System.out.println(client.send(Protocol.CHECK, "305"));
client.close();
server.stop();
Изход
ОК:Стая 102 е твоя, Анна Петрова.
ОК:102, двойна, 85 € на нощ, заета
ОК:101, единична, 60 € на нощ, свободна; 201, двойна, 95 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
ОК:Резервацията за стая 102 е анулирана.
ГРЕШКА:Няма стая 305.

Идеята

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

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

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

Решението: в хотела влиза само една заявка наведнъж. Тогава „провери и заеми“ е неделимо: никой не може да влезе между проверката и заемането. В Java това прави synchronized на всеки публичен метод на Hotel:
public synchronized 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 synchronized 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);
}
Стъпка по стъпка, с думи
  1. Стая 102 е свободна. Анна Петрова и Борис Илиев я резервират в един и същ миг.
  2. Първо без synchronized. Това е един възможен ред: заявката на Анна проверява и вижда „свободна“.
  3. Заявката на Борис проверява, преди Анна да е заела стаята: и тя вижда „свободна“.
  4. Анна заема стаята и записва името си.
  5. Борис също заема стаята и записва своето име. Двамата имат потвърждение за една стая.
  6. Отначало, със synchronized: в хотела влиза само една нишка наведнъж.
  7. Да кажем, че първа е Анна: тя взима ключалката. Проверката и заемането са една стъпка, никой не влиза между тях.
  8. Борис стига до хотела и чака, докато ключалката се освободи.
  9. Ключалката е на Борис. Проверката вече вижда „заета“.
  10. RoomTakenException. Рецепцията го връща като ред с грешка, а Борис може да пита за друга стая.
  11. Програмата от урока брои успелите: по 1 за всяка стая. Кой е бил пръв, е различно при всяко пускане.

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

static String reserve(int port, int room, String guest) {
try {
HotelClient client = new HotelClient("localhost", port);
String reply = client.send(Protocol.RESERVE, room + ";" + guest);
client.close();
return reply;
} catch (IOException e) {
return Protocol.ERROR + ":" + e.getMessage();
}
}
static int race(int port, int room, String[] guests) throws InterruptedException {
String[] replies = new String[guests.length];
List<Thread> clients = new ArrayList<>();
for (int i = 0; i < guests.length; i++) {
int n = i;
Thread client = new Thread(() -> replies[n] = reserve(port, room, guests[n]));
clients.add(client);
client.start();
}
for (Thread client : clients) {
client.join();
}
int reserved = 0;
for (String reply : replies) {
if (reply.startsWith(Protocol.OK)) {
reserved++;
}
}
return reserved;
}
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));
HotelServer server = new HotelServer(5322, hotel);
server.start();
String[] two = { "Анна Петрова", "Борис Илиев" };
System.out.println("Двама за стая 102, успешни: " + race(5322, 102, two));
String[] agencies = new String[20];
for (int i = 0; i < agencies.length; i++) {
agencies[i] = "Агенция " + (i + 1);
}
System.out.println("Двадесет за стая 201, успешни: " + race(5322, 201, agencies));
HotelClient desk = new HotelClient("localhost", 5322);
System.out.println(desk.send(Protocol.FREE));
desk.close();
server.stop();
Изход
Двама за стая 102, успешни: 1
Двадесет за стая 201, успешни: 1
ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
Всеки клиент е в своя нишка и пише отговора си на своето място в масива. main чака всички с join и брои успелите.

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

По-точно, за любопитните
  • Без заключването програмата пак почти винаги би казала 1: прозорецът между проверката и заемането е малък. Но „почти винаги“ значи, че веднъж на хиляди резервации в стаята пристигат двама. Такъв бъг не се хваща с пускане, а с мислене: кои данни са общи и кой ги мени наведнъж.
  • synchronized на метод заключва целия обект Hotel. Докато една нишка резервира 102, друга чака дори за 201. При четири стаи това е добре. При хиляди стаи се заключва по стая, например synchronized (room) { ... }.
  • В истинска система стаите са в база данни и пазенето е там: транзакция или условие в самата заявка (UPDATE rooms SET free = false WHERE number = 102 AND free = true). Тогава важи и при няколко сървъра, а не само в една програма.

Пробвай

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

Колега раздели резервацията на два метода, и двата synchronized. Рецепцията вика първо проверката, после заемането. При много заявки наведнъж понякога пак двама получават една стая. На кой ред е грешката?

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

Виж го

Целият хотел

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

static void say(String who, HotelClient client, String command, String text) throws IOException {
String reply = text.isEmpty() ? client.send(command) : client.send(command, text);
System.out.println(who + " > " + command + (text.isEmpty() ? "" : ":" + text));
System.out.println(who + " < " + reply);
}
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));
HotelServer server = new HotelServer(5323, hotel);
server.start();
HotelClient anna = new HotelClient("localhost", 5323);
HotelClient boris = new HotelClient("localhost", 5323);
say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова");
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
say("Борис", boris, Protocol.CHECK, "102");
say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев");
say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова");
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
say("Анна", anna, Protocol.CHECK, "305");
anna.close();
boris.close();
String[] two = { "Анна Петрова", "Вера Колева" };
System.out.println("Двама за стая 201 наведнъж, успешни: " + race(5323, 201, two));
HotelClient desk = new HotelClient("localhost", 5323);
System.out.println(desk.send(Protocol.FREE));
desk.close();
server.stop();
Изход
Анна > РЕЗЕРВИРАЙ:102;Анна Петрова
Анна < ОК:Стая 102 е твоя, Анна Петрова.
Борис > РЕЗЕРВИРАЙ:102;Борис Илиев
Борис < ГРЕШКА:Стая 102 вече е заета.
Борис > ПРОВЕРИ:102
Борис < ОК:102, двойна, 85 € на нощ, заета
Борис > АНУЛИРАЙ:102;Борис Илиев
Борис < ГРЕШКА:Стая 102 е резервирана от друг гост.
Анна > АНУЛИРАЙ:102;Анна Петрова
Анна < ОК:Резервацията за стая 102 е анулирана.
Борис > РЕЗЕРВИРАЙ:102;Борис Илиев
Борис < ОК:Стая 102 е твоя, Борис Илиев.
Анна > ПРОВЕРИ:305
Анна < ГРЕШКА:Няма стая 305.
Двама за стая 201 наведнъж, успешни: 1
ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
По-точно, за любопитните

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

Main.java
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
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 ? "свободна" : "заета");
}
}
class Hotel {
private final List<Room> rooms = new ArrayList<>();
private final Map<Integer, String> guests = new HashMap<>();
public synchronized void add(Room room) {
rooms.add(room);
}
public synchronized List<Room> freeRooms() {
return rooms.stream().filter(Room::isFree).toList();
}
public synchronized Room check(int number) {
return find(number);
}
public synchronized 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 synchronized 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);
}
private Room find(int number) {
for (Room room : rooms) {
if (room.getNumber() == number) {
return room;
}
}
throw new RoomNotFoundException("Няма стая " + number + ".");
}
}
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);
}
}
class Protocol {
public static final String FREE = "СВОБОДНИ";
public static final String CHECK = "ПРОВЕРИ";
public static final String RESERVE = "РЕЗЕРВИРАЙ";
public static final String CANCEL = "АНУЛИРАЙ";
public static final String EXIT = "КРАЙ";
public static final String OK = "ОК";
public static final String ERROR = "ГРЕШКА";
public static final String BYE = "ДОВИЖДАНЕ";
}
class Reception {
private final Hotel hotel;
public Reception(Hotel hotel) {
this.hotel = hotel;
}
public String handle(String line) {
int colon = line.indexOf(':');
String command = colon < 0 ? line : line.substring(0, colon);
String text = colon < 0 ? "" : line.substring(colon + 1);
try {
switch (command) {
case Protocol.FREE:
return Protocol.OK + ":" + join(hotel.freeRooms());
case Protocol.CHECK:
return Protocol.OK + ":" + hotel.check(number(text));
case Protocol.RESERVE:
return reserve(text);
case Protocol.CANCEL:
return cancel(text);
default:
throw new HotelException("Непозната команда " + command + ".");
}
} catch (HotelException e) {
return Protocol.ERROR + ":" + e.getMessage();
}
}
private String reserve(String text) {
String[] parts = numberAndGuest(text);
int number = number(parts[0]);
hotel.reserve(number, parts[1]);
return Protocol.OK + ":Стая " + number + " е твоя, " + parts[1] + ".";
}
private String cancel(String text) {
String[] parts = numberAndGuest(text);
int number = number(parts[0]);
hotel.cancel(number, parts[1]);
return Protocol.OK + ":Резервацията за стая " + number + " е анулирана.";
}
private static String[] numberAndGuest(String text) {
String[] parts = text.split(";", 2);
if (parts.length != 2) {
throw new HotelException("Пиши номер и име, например „102;Анна Петрова“.");
}
return parts;
}
private static int number(String text) {
try {
return Integer.parseInt(text.trim());
} catch (NumberFormatException e) {
throw new HotelException("Номерът на стаята е цяло число.");
}
}
private static String join(List<Room> rooms) {
if (rooms.isEmpty()) {
return "Няма свободни стаи.";
}
return String.join("; ", rooms.stream().map(Room::toString).toList());
}
}
class ClientHandler implements Runnable {
private final Socket socket;
private final Reception reception;
public ClientHandler(Socket socket, Reception reception) {
this.socket = socket;
this.reception = reception;
}
@Override
public void run() {
try (socket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String line;
while ((line = in.readLine()) != null && !line.equals(Protocol.EXIT)) {
out.println(reception.handle(line));
}
out.println(Protocol.BYE);
} catch (IOException e) {
System.out.println("Връзката с клиента прекъсна: " + e.getMessage());
}
}
}
class HotelServer {
private final ServerSocket serverSocket;
private final ExecutorService pool = Executors.newFixedThreadPool(10);
private final Reception reception;
public HotelServer(int port, Hotel hotel) throws IOException {
this.serverSocket = new ServerSocket(port);
this.reception = new Reception(hotel);
}
public void start() {
Thread acceptor = new Thread(() -> acceptAll());
acceptor.setDaemon(true);
acceptor.start();
}
private void acceptAll() {
while (true) {
try {
Socket client = serverSocket.accept();
pool.execute(new ClientHandler(client, reception));
} catch (IOException e) {
return;
}
}
}
public void stop() throws IOException {
serverSocket.close();
pool.shutdown();
}
}
class HotelClient {
private final Socket socket;
private final BufferedReader in;
private final PrintWriter out;
public HotelClient(String host, int port) throws IOException {
socket = new Socket(host, port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
}
public String send(String command, String text) throws IOException {
out.println(command + ":" + text);
return in.readLine();
}
public String send(String command) throws IOException {
out.println(command);
return in.readLine();
}
public void close() throws IOException {
send(Protocol.EXIT);
socket.close();
}
}
public class Main {
static void say(String who, HotelClient client, String command, String text) throws IOException {
String reply = text.isEmpty() ? client.send(command) : client.send(command, text);
System.out.println(who + " > " + command + (text.isEmpty() ? "" : ":" + text));
System.out.println(who + " < " + reply);
}
static String reserve(int port, int room, String guest) {
try {
HotelClient client = new HotelClient("localhost", port);
String reply = client.send(Protocol.RESERVE, room + ";" + guest);
client.close();
return reply;
} catch (IOException e) {
return Protocol.ERROR + ":" + e.getMessage();
}
}
static int race(int port, int room, String[] guests) throws InterruptedException {
String[] replies = new String[guests.length];
List<Thread> clients = new ArrayList<>();
for (int i = 0; i < guests.length; i++) {
int n = i;
Thread client = new Thread(() -> replies[n] = reserve(port, room, guests[n]));
clients.add(client);
client.start();
}
for (Thread client : clients) {
client.join();
}
int reserved = 0;
for (String reply : replies) {
if (reply.startsWith(Protocol.OK)) {
reserved++;
}
}
return reserved;
}
public static void main(String[] args) throws Exception {
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));
HotelServer server = new HotelServer(5323, hotel);
server.start();
HotelClient anna = new HotelClient("localhost", 5323);
HotelClient boris = new HotelClient("localhost", 5323);
say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова");
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
say("Борис", boris, Protocol.CHECK, "102");
say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев");
say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова");
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев");
say("Анна", anna, Protocol.CHECK, "305");
anna.close();
boris.close();
String[] two = { "Анна Петрова", "Вера Колева" };
System.out.println("Двама за стая 201 наведнъж, успешни: " + race(5323, 201, two));
HotelClient desk = new HotelClient("localhost", 5323);
System.out.println(desk.send(Protocol.FREE));
desk.close();
server.stop();
}
}

Упражнение

Продължи сам

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

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

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

Още идеи

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

Тест

Провери се

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

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