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

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

Проблем

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

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

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

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

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

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

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

Идеята

Стаята

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

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.
  • Кой е отседнал, стаята не знае. Стаята стига до всеки клиент, който пита, а името на госта не бива да излиза от хотела.
По-точно, за любопитните
IsFree има публичен set, защото клиентът получава стаята като JSON и System.Text.Json трябва да запише състоянието. Номерът, типът и цената идват през конструктора. Без set клиентът би виждал всяка стая като свободна. За повече от две състояния се ползва енумерация: 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 хваща всички откази наведнъж: сървърът ще хваща само него и ще го превръща в отговор за клиента.

По-точно, за любопитните
  • В C# няма проверени изключения. Затова сървърът трябва да ги хване на едно място, иначе отказът по правилата излиза като 500.
  • Сценарии като тези са естествени тестове. В C# за това има библиотеки като xUnit: всеки сценарий става метод, а Assert.Throws<RoomTakenException>(...) проверява изключението.

Пробвай

Кой е в 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, "Вера Колева");
Console.WriteLine("Вера е в 201");
}
catch (RoomTakenException e)
{
Console.WriteLine("Отказ за Вера");
}
Console.WriteLine(hotel.FreeRooms().Count);
Отговор

Идеята

Протоколът: адреси и JSON

Всяко действие е метод и адрес, данните са JSON, а статусът казва как е минало:

Заявка Тяло Отговор
GET /rooms/free 200 и свободните стаи
GET /rooms/102 200 и стаята или 404
POST /rooms/102/reservation {"guest":"Анна Петрова"} 200 и потвърждение, 409 за заета стая, 404 за стая, която я няма
DELETE /rooms/102/reservation?guest=Анна Петрова 200 и потвърждение или 400 с причината

Всеки отказ на модела има свой статус. RoomNotFoundException е 404 Not Found, RoomTakenException е 409 Conflict: заявката е наред, но се сблъсква с текущото състояние. Всеки друг HotelException е 400 Bad Request.

Тялото на резервацията е малък клас само за пренасяне:

public class ReservationRequest
{
public string Guest { get; set; }
}
По-точно, за любопитните
  • Командите могат да са енумерация: enum Command { Free, Check, Reserve, Cancel }. Тогава са отделен тип със затворен списък от стойности. В курса са константи, защото енумерации не сме учили.
  • DELETE няма тяло по правило, затова името е в адреса. В истинска система вместо име клиентът влиза с акаунт и сървърът знае кой е. Тогава никой не може да анулира чужда резервация, като напише чуждо име.

Виж го

Сървърът

Началото е като в урока за първия сървър: портът, без съобщенията на ASP.NET и JSON с четима кирилица:

var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All));
var app = builder.Build();
app.Urls.Add("http://localhost:5321");

Отказите на модела стават статуси на едно място, в Safe:

IResult Safe(Func<IResult> action)
{
try
{
return action();
}
catch (RoomNotFoundException e)
{
return Results.NotFound(e.Message);
}
catch (RoomTakenException e)
{
return Results.Conflict(e.Message);
}
catch (HotelException e)
{
return Results.BadRequest(e.Message);
}
}

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

app.MapGet("/rooms/free", () => hotel.FreeRooms());
app.MapGet("/rooms/{number:int}", (int number) => Safe(() => Results.Ok(hotel.Check(number))));
app.MapPost("/rooms/{number:int}/reservation", (int number, ReservationRequest request) => Safe(() =>
{
hotel.Reserve(number, request.Guest);
return Results.Ok("Стая " + number + " е твоя, " + request.Guest + ".");
}));
app.MapDelete("/rooms/{number:int}/reservation", (int number, string guest) => Safe(() =>
{
hotel.Cancel(number, guest);
return Results.Ok("Резервацията за стая " + number + " е анулирана.");
}));

Ето какво минава по мрежата:

using var http = new HttpClient { BaseAddress = new Uri("http://localhost:5321") };
async Task Show(string request, HttpResponseMessage response)
{
Console.WriteLine(request + " -> " + (int)response.StatusCode + " " + await response.Content.ReadAsStringAsync());
}
var anna = new ReservationRequest { Guest = "Анна Петрова" };
var boris = new ReservationRequest { Guest = "Борис Илиев" };
await Show("POST /rooms/102/reservation", await http.PostAsJsonAsync("/rooms/102/reservation", anna));
await Show("POST /rooms/102/reservation", await http.PostAsJsonAsync("/rooms/102/reservation", boris));
await Show("GET /rooms/102", await http.GetAsync("/rooms/102"));
await Show("DELETE /rooms/102/reservation?guest=Борис Илиев", await http.DeleteAsync("/rooms/102/reservation?guest=Борис Илиев"));
await Show("DELETE /rooms/102/reservation?guest=Анна Петрова", await http.DeleteAsync("/rooms/102/reservation?guest=Анна Петрова"));
await Show("GET /rooms/305", await http.GetAsync("/rooms/305"));
Изход
POST /rooms/102/reservation -> 200 "Стая 102 е твоя, Анна Петрова."
POST /rooms/102/reservation -> 409 "Стая 102 вече е заета."
GET /rooms/102 -> 200 {"number":102,"type":"двойна","price":85,"isFree":false}
DELETE /rooms/102/reservation?guest=Борис Илиев -> 400 "Стая 102 е резервирана от друг гост."
DELETE /rooms/102/reservation?guest=Анна Петрова -> 200 "Резервацията за стая 102 е анулирана."
GET /rooms/305 -> 404 "Няма стая 305."

Виж го

Клиентът

Клиентът крие HTTP зад методи. Отговор с грешка става същото изключение, което сървърът е хвърлил: 404, 409 и 400 се връщат като RoomNotFoundException, RoomTakenException и HotelException:

public class HotelClient
{
private readonly HttpClient http;
public HotelClient(string address)
{
http = new HttpClient { BaseAddress = new Uri(address) };
}
public async Task<List<Room>> FreeRoomsAsync()
{
return await http.GetFromJsonAsync<List<Room>>("/rooms/free");
}
public async Task<Room> CheckAsync(int number)
{
HttpResponseMessage response = await http.GetAsync("/rooms/" + number);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<Room>();
}
public async Task<string> ReserveAsync(int number, string guest)
{
var request = new ReservationRequest { Guest = guest };
HttpResponseMessage response = await http.PostAsJsonAsync("/rooms/" + number + "/reservation", request);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
public async Task<string> CancelAsync(int number, string guest)
{
HttpResponseMessage response = await http.DeleteAsync("/rooms/" + number + "/reservation?guest=" + guest);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
private static async Task ThrowIfError(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}
string message = await response.Content.ReadFromJsonAsync<string>();
int status = (int)response.StatusCode;
if (status == 404)
{
throw new RoomNotFoundException(message);
}
if (status == 409)
{
throw new RoomTakenException(message);
}
throw new HotelException(message);
}
}
var client = new HotelClient("http://localhost:5325");
Console.WriteLine(await client.ReserveAsync(102, "Анна Петрова"));
Console.WriteLine(await client.CheckAsync(102));
foreach (Room room in await client.FreeRoomsAsync())
{
Console.WriteLine("Свободна: " + room);
}
Console.WriteLine(await client.CancelAsync(102, "Анна Петрова"));
try
{
await client.CheckAsync(305);
}
catch (RoomNotFoundException e)
{
Console.WriteLine("Отказ: " + e.Message);
}
Изход
Стая 102 е твоя, Анна Петрова.
102, двойна, 85 € на нощ, заета
Свободна: 101, единична, 60 € на нощ, свободна
Свободна: 201, двойна, 95 € на нощ, свободна
Свободна: 202, апартамент, 140 € на нощ, свободна
Резервацията за стая 102 е анулирана.
Отказ: Няма стая 305.

Идеята

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

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

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

Решението: в хотела влиза само една заявка наведнъж. Тогава „провери и заеми“ е неделимо: никой не може да влезе между проверката и заемането. В C# това прави lock във всеки публичен метод на Hotel:
private readonly List<Room> rooms = new List<Room>();
private readonly Dictionary<int, string> guests = new Dictionary<int, string>();
private readonly object sync = new object();
public void Reserve(int number, string guest)
{
lock (sync)
{
if (string.IsNullOrWhiteSpace(guest))
{
throw new HotelException("Името на госта е задължително.");
}
Room room = Find(number);
if (!room.IsFree)
{
throw new RoomTakenException("Стая " + number + " вече е заета.");
}
room.Occupy();
guests[number] = guest;
}
}

public void Reserve(int number, string guest)
{
lock (sync)
{
if (string.IsNullOrWhiteSpace(guest))
{
throw new HotelException("Името на госта е задължително.");
}
Room room = Find(number);
if (!room.IsFree)
{
throw new RoomTakenException("Стая " + number + " вече е заета.");
}
room.Occupy();
guests[number] = guest;
}
}
Стъпка по стъпка, с думи
  1. Стая 102 е свободна. Анна Петрова и Борис Илиев я резервират в един и същ миг.
  2. Първо без lock. Това е един възможен ред: заявката на Анна проверява и вижда „свободна“.
  3. Заявката на Борис проверява, преди Анна да е заела стаята: и тя вижда „свободна“.
  4. Анна заема стаята и записва името си.
  5. Борис също заема стаята и записва своето име. Двамата имат потвърждение за една стая.
  6. Отначало, с lock (sync): в хотела влиза само една заявка наведнъж.
  7. Да кажем, че първа е Анна: тя взима ключалката. Проверката и заемането са една стъпка, никой не влиза между тях.
  8. Борис стига до хотела и чака, докато ключалката се освободи.
  9. Ключалката е на Борис. Проверката вече вижда „заета“.
  10. RoomTakenException. Сървърът го връща като 409 Conflict, а клиентът пак го прави RoomTakenException.
  11. Програмата от урока брои успелите: по 1 за всяка стая. Кой е бил пръв, е различно при всяко пускане.

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

var client = new HotelClient("http://localhost:5322");
async Task<bool> TryReserve(HotelClient client, int number, string guest)
{
try
{
await client.ReserveAsync(number, guest);
return true;
}
catch (RoomTakenException)
{
return false;
}
}
async Task<int> Race(HotelClient client, int number, string[] guests)
{
var attempts = new List<Task<bool>>();
foreach (string guest in guests)
{
attempts.Add(TryReserve(client, number, guest));
}
bool[] reserved = await Task.WhenAll(attempts);
return reserved.Count(r => r);
}
string[] two = { "Анна Петрова", "Борис Илиев" };
Console.WriteLine("Двама за стая 102, успешни: " + await Race(client, 102, two));
string[] agencies = new string[20];
for (int i = 0; i < agencies.Length; i++)
{
agencies[i] = "Агенция " + (i + 1);
}
Console.WriteLine("Двадесет за стая 201, успешни: " + await Race(client, 201, agencies));
Console.WriteLine("Свободни: " + string.Join("; ", await client.FreeRoomsAsync()));
Изход
Двама за стая 102, успешни: 1
Двадесет за стая 201, успешни: 1
Свободни: 101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна

Task.WhenAll пуска всички заявки наведнъж и чака отговорите. Отказът е RoomTakenException, а TryReserve го превръща в false.

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

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

Пробвай

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

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

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

Виж го

Целият хотел

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

var anna = new HotelClient("http://localhost:5323");
var boris = new HotelClient("http://localhost:5323");
async Task Say(string who, Task<string> request)
{
try
{
Console.WriteLine(who + ": " + await request);
}
catch (HotelException e)
{
Console.WriteLine(who + ": отказ, " + e.Message);
}
}
async Task<bool> TryReserve(HotelClient client, int number, string guest)
{
try
{
await client.ReserveAsync(number, guest);
return true;
}
catch (RoomTakenException)
{
return false;
}
}
async Task<int> Race(HotelClient client, int number, string[] guests)
{
var attempts = new List<Task<bool>>();
foreach (string guest in guests)
{
attempts.Add(TryReserve(client, number, guest));
}
bool[] reserved = await Task.WhenAll(attempts);
return reserved.Count(r => r);
}
await Say("Анна", anna.ReserveAsync(102, "Анна Петрова"));
await Say("Борис", boris.ReserveAsync(102, "Борис Илиев"));
Console.WriteLine("Борис: " + await boris.CheckAsync(102));
await Say("Борис", boris.CancelAsync(102, "Борис Илиев"));
await Say("Анна", anna.CancelAsync(102, "Анна Петрова"));
await Say("Борис", boris.ReserveAsync(102, "Борис Илиев"));
try
{
await anna.CheckAsync(305);
}
catch (RoomNotFoundException e)
{
Console.WriteLine("Анна: отказ, " + e.Message);
}
string[] two = { "Анна Петрова", "Вера Колева" };
Console.WriteLine("Двама за стая 201 наведнъж, успешни: " + await Race(anna, 201, two));
Console.WriteLine("Свободни: " + string.Join("; ", await anna.FreeRoomsAsync()));
Изход
Анна: Стая 102 е твоя, Анна Петрова.
Борис: отказ, Стая 102 вече е заета.
Борис: 102, двойна, 85 € на нощ, заета
Борис: отказ, Стая 102 е резервирана от друг гост.
Анна: Резервацията за стая 102 е анулирана.
Борис: Стая 102 е твоя, Борис Илиев.
Анна: отказ, Няма стая 305.
Двама за стая 201 наведнъж, успешни: 1
Свободни: 101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
По-точно, за любопитните

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

Program.cs
using System.Text.Encodings.Web;
using System.Text.Unicode;
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All));
var app = builder.Build();
app.Urls.Add("http://localhost:5323");
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));
IResult Safe(Func<IResult> action)
{
try
{
return action();
}
catch (RoomNotFoundException e)
{
return Results.NotFound(e.Message);
}
catch (RoomTakenException e)
{
return Results.Conflict(e.Message);
}
catch (HotelException e)
{
return Results.BadRequest(e.Message);
}
}
app.MapGet("/rooms/free", () => hotel.FreeRooms());
app.MapGet("/rooms/{number:int}", (int number) => Safe(() => Results.Ok(hotel.Check(number))));
app.MapPost("/rooms/{number:int}/reservation", (int number, ReservationRequest request) => Safe(() =>
{
hotel.Reserve(number, request.Guest);
return Results.Ok("Стая " + number + " е твоя, " + request.Guest + ".");
}));
app.MapDelete("/rooms/{number:int}/reservation", (int number, string guest) => Safe(() =>
{
hotel.Cancel(number, guest);
return Results.Ok("Резервацията за стая " + number + " е анулирана.");
}));
await app.StartAsync();
var anna = new HotelClient("http://localhost:5323");
var boris = new HotelClient("http://localhost:5323");
async Task Say(string who, Task<string> request)
{
try
{
Console.WriteLine(who + ": " + await request);
}
catch (HotelException e)
{
Console.WriteLine(who + ": отказ, " + e.Message);
}
}
async Task<bool> TryReserve(HotelClient client, int number, string guest)
{
try
{
await client.ReserveAsync(number, guest);
return true;
}
catch (RoomTakenException)
{
return false;
}
}
async Task<int> Race(HotelClient client, int number, string[] guests)
{
var attempts = new List<Task<bool>>();
foreach (string guest in guests)
{
attempts.Add(TryReserve(client, number, guest));
}
bool[] reserved = await Task.WhenAll(attempts);
return reserved.Count(r => r);
}
await Say("Анна", anna.ReserveAsync(102, "Анна Петрова"));
await Say("Борис", boris.ReserveAsync(102, "Борис Илиев"));
Console.WriteLine("Борис: " + await boris.CheckAsync(102));
await Say("Борис", boris.CancelAsync(102, "Борис Илиев"));
await Say("Анна", anna.CancelAsync(102, "Анна Петрова"));
await Say("Борис", boris.ReserveAsync(102, "Борис Илиев"));
try
{
await anna.CheckAsync(305);
}
catch (RoomNotFoundException e)
{
Console.WriteLine("Анна: отказ, " + e.Message);
}
string[] two = { "Анна Петрова", "Вера Колева" };
Console.WriteLine("Двама за стая 201 наведнъж, успешни: " + await Race(anna, 201, two));
Console.WriteLine("Свободни: " + string.Join("; ", await anna.FreeRoomsAsync()));
await app.StopAsync();
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 ? "свободна" : "заета");
}
}
public class Hotel
{
private readonly List<Room> rooms = new List<Room>();
private readonly Dictionary<int, string> guests = new Dictionary<int, string>();
private readonly object sync = new object();
public void Add(Room room)
{
lock (sync)
{
rooms.Add(room);
}
}
public List<Room> FreeRooms()
{
lock (sync)
{
return rooms.Where(room => room.IsFree).ToList();
}
}
public Room Check(int number)
{
lock (sync)
{
return Find(number);
}
}
public void Reserve(int number, string guest)
{
lock (sync)
{
if (string.IsNullOrWhiteSpace(guest))
{
throw new HotelException("Името на госта е задължително.");
}
Room room = Find(number);
if (!room.IsFree)
{
throw new RoomTakenException("Стая " + number + " вече е заета.");
}
room.Occupy();
guests[number] = guest;
}
}
public void Cancel(int number, string guest)
{
lock (sync)
{
Room room = Find(number);
if (room.IsFree)
{
throw new HotelException("Стая " + number + " не е резервирана.");
}
if (guests[number] != guest)
{
throw new HotelException("Стая " + number + " е резервирана от друг гост.");
}
room.Release();
guests.Remove(number);
}
}
private Room Find(int number)
{
Room room = rooms.FirstOrDefault(r => r.Number == number);
if (room == null)
{
throw new RoomNotFoundException("Няма стая " + number + ".");
}
return room;
}
}
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)
{
}
}
public class ReservationRequest
{
public string Guest { get; set; }
}
public class HotelClient
{
private readonly HttpClient http;
public HotelClient(string address)
{
http = new HttpClient { BaseAddress = new Uri(address) };
}
public async Task<List<Room>> FreeRoomsAsync()
{
return await http.GetFromJsonAsync<List<Room>>("/rooms/free");
}
public async Task<Room> CheckAsync(int number)
{
HttpResponseMessage response = await http.GetAsync("/rooms/" + number);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<Room>();
}
public async Task<string> ReserveAsync(int number, string guest)
{
var request = new ReservationRequest { Guest = guest };
HttpResponseMessage response = await http.PostAsJsonAsync("/rooms/" + number + "/reservation", request);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
public async Task<string> CancelAsync(int number, string guest)
{
HttpResponseMessage response = await http.DeleteAsync("/rooms/" + number + "/reservation?guest=" + guest);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
private static async Task ThrowIfError(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}
string message = await response.Content.ReadFromJsonAsync<string>();
int status = (int)response.StatusCode;
if (status == 404)
{
throw new RoomNotFoundException(message);
}
if (status == 409)
{
throw new RoomTakenException(message);
}
throw new HotelException(message);
}
}

Упражнение

Продължи сам

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

1. Смяна на стая. Гостът се мести от една стая в друга с POST /rooms/102/move и тяло {"guest":"Анна Петрова","to":202}. Отговорът е „Анна Петрова е в стая 202.“, а клиентът има метод MoveAsync. Ако новата стая е заета, гостът остава в старата. Смяната е един метод в Hotel, в lock: никой не бива да заеме новата стая между освобождаването на старата и заемането на новата.

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

Още идеи

  • Нощувки и сума. Резервацията е за брой нощи, а потвърждението казва сумата: „3 нощи, 255 €“.
  • Дати. Стаята се резервира за дни, например от 14 до 16 декември. Две резервации за една стая не бива да се застъпват. Свободна ли е стаята, зависи от датите.
  • Моите резервации. GET /reservations?guest=Анна Петрова казва кои стаи са на този гост.
  • Списък на чакащите. Ако стаята е заета, гостът се записва да чака. Когато се анулира, тя отива при първия в списъка.
  • Сървър, който не спира. Отдели сървъра в своя програма с await app.RunAsync(), а клиента в друга, пусната в друг терминал.

Тест

Провери се

1. Две агенции резервират стая 102 в един и същ миг, а резервацията не е синхронизирана. Какво може да стане?
2. Проверката „свободна ли е“ и заемането на стаята са в два отделни синхронизирани метода. Достатъчно ли е?
3. Стая 102 е заета. С какъв статус отговаря POST /rooms/102/reservation?
4. Защо програмата печата колко резервации са успели, а не кой е спечелил стаята?
5. Защо имената на гостите са в речник в хотела, а не в стаята?

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