Agregat DDD do rezerwacji biletów

0

Co sądzicie o tym agregacie (DDD) do rezerwowania i anulowania biletów? Jest ok, czy jakoś inaczej byście go zrobili?

@Entity
@Table(name = "screenings")
@Getter
@ToString(exclude = "tickets")
public class Screening {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime date;

    private Long filmId;

    private Long hallId;

    @OneToMany
    private List<Ticket> tickets;

    protected Screening() {}

    public Screening(LocalDateTime date, Long filmId, Long hallId
    ) {
        this.date = date;
        this.filmId = filmId;
        this.hallId = hallId;
        this.tickets = new ArrayList<>();
    }

    public void bookTickets(List<Ticket> tickets, TicketBookingPolicy ticketBookingPolicy) {
        ticketBookingPolicy.checkScreeningDate(this.date);
        tickets.forEach(ticket -> {
            if (this.tickets.contains(ticket)) {
                throw new TicketAlreadyExistsException();
            }
        });
        this.tickets.addAll(tickets);
    }

    public Ticket cancelTicket(Long ticketId, TicketCancellingPolicy ticketCancellingPolicy) {
        ticketCancellingPolicy.checkScreeningDate(this.date);
        var foundTicket = tickets
                .stream()
                .filter(ticket -> ticket.getId().equals(ticketId))
                .findFirst()
                .orElseThrow(TicketNotFoundException::new);
        foundTicket.cancel();
        return foundTicket;
    }
}

Jeden z problemów jaki widzę na razie, to to że przy anulowaniu trzeba się przeiterować po całej kolekcji biletów, ale ta kolekcja nie będzie duża, w najgorszym wypadku ok 100 rekordów.

1

Po pierwsze to ja bym zrobił pojedyncze addTicket i zmusił klienta do dodawania ticketów pojedynczo, a nie listę. Lepiej wywalić błąd na jednym tickecie niż mieć problem z całym screeningien, bo jakiś jeden już istnieje. To problem klienta, że dodaje zduplikowany bilet, ale niech wie jaki i niech tylko ten jeden będzie problemem, a nie cala kolekcja.

Anulowanie możesz napisać pod spodem implementację na jedną query do bazy, więc obejdzie się bez iteracji. Przykryjesz to ładną metodą cancelScreening i wsio.

Ale autorytetem w DDD nie jestem więc niech inni się wypowiedzą.

0

A jakie jest wymaganie biznesowe? Bo od tego zaczyna się zabawa w modelowanie domeny na poziomie wzorców taktycznych. Robiłeś jakiś event storming, modelling whirpool, albo jakikolwiek inny proces do eksploracji domeny, czy tak sobie wesoło klepiesz co tam Ci ślina na język przyniesie?

0
markone_dev napisał(a):

A jakie jest wymaganie biznesowe? Bo od tego zaczyna się zabawa w modelowanie domeny na poziomie wzorców taktycznych. Robiłeś jakiś event storming, modelling whirpool, albo jakikolwiek inny proces do eksploracji domeny, czy tak sobie wesoło klepiesz co tam Ci ślina na język przyniesie?

Wymagania biznesowe:

  1. Użytkownik może rezerwować wiele biletów
  2. Rezerwować bilety można najpóźniej godzinę przed seansem
  3. Może istnieć tylko jeden bilet na dane miejsce
  4. Anulować bilet można najpóźniej 24h przed seansem
0
Nofenak napisał(a):
    ticketCancellingPolicy.checkScreeningDate(this.date);

Nigdzie tego nie przypisujesz ani nie używasz w innym miejscu. Jak to więc działa? Czy checkScreeningDate rzuca wyjątkiem, jeśli data jest nie halo?

0
LukeJL napisał(a):
Nofenak napisał(a):
    ticketCancellingPolicy.checkScreeningDate(this.date);

Nigdzie tego nie przypisujesz ani nie używasz w innym miejscu. Jak to więc działa? Czy checkScreeningDate rzuca wyjątkiem, jeśli data jest nie halo?

Dokładnie tak

1
Nofenak napisał(a):

Wymagania biznesowe:

  1. Użytkownik może rezerwować wiele biletów
  2. Rezerwować bilety można najpóźniej godzinę przed seansem
  3. Może istnieć tylko jeden bilet na dane miejsce
  4. Anulować bilet można najpóźniej 24h przed seansem

Ja tu poletka do DDD nie widzę, ale na siłę mogłoby to wyglądać tak (C#).

Primo mieszasz pojęcia biletu z miejscem na sali kinowej. Ja na potrzeby przykładu przyjąłem, że mówimy o miejscach nie o biletach, bo bilet to szczegół sprzedażowy. Nie znam się na systemach obsługi kin, ale mogę sobie wyobrazić sytuację że na jednym bilecie masz kilka miejsc.

Secundo przyjąłem założenie że ilość dostępnych miejsc może się różnić w zależności od seansu. Wtedy masz Screening (seans), który wie ile miejsc jest dostępnych i to on jest agregatem przy rezerwacji miejsc i sprawdza czy miejsce jest dostępne czy nie. Chociaż tak jak napisałem jest to zrobione na siłę bo przy tej logice to nie trzeba się bawić w agregaty.

internal class Screening : AggregateRoot
{
    public ScreeningId Id { get; }
    public DateTime StartsOn { get; }
    public DateTime EndsOn { get; }
    public IEnumerable<Seat> Seats => _seats;

    public Result BookSeat(Seat seat, CustomerId customerId)
    {
        if (IsLateBooking())
            return Result.Failure("Tickets can be booked no later than one hour before the show");

        if (!IsSeatAvailable(seat.Number))
            return Result.Failure($"Selected seat ({seat.Number}) is booked already");

        seatToBook.Reserve(customerId);

        AddEvent(new SeatBooked(
            screeningId: Id.Value,
            seatNumber: seat.Number,
            customerId: customerId.Value);

        return Result.Success($"A seat with number ({seat.Number}) is booked successfully.");
    }

    public Result FreeUpSeat()
    {
        // TODO
    }

    private bool IsLateBooking()
    {
        // TODO
    }

    private bool IsSeatAvailable(int seatNumber)
    {
        var seatToBook = _seats.Find(s => s.Number == seatNumber);
        return seatToBook.IsAvailable; //may have more complex logic, enough for the demo, lol
    }
}

Po walidacji reguł biznesowych które mogą być w agregacie, albo zewnętrznej polityce w zależności od tego jak często mogą się one zmieniać następuje rezerwacja miejsca seatToBook.Reserve() po czym następuje zarejestrowanie zdarzenia, które wywoła handler SeatBookedHandler który to utworzy w systemie rezerwację (być może w rzeczywistym systemie rezerwacja to osobna poddziedzina albo bounded context. Być może na jednej rezerwacji powinno być kilka miejsc per klient? Nie wiem, zmyślam.

internal class SeatBookedHandler : IEventHandler<SeatBooked>
{
    private readonly IReservationRepository _reservationRepository;

    public SeatBookedHandler(IReservationRepository reservationRepository)
    {
        _reservationRepository = reservationRepository;
    }

    public Result Handle(SeatBooked @event)
    {
        var reservation = new Reservation(
            screeningId: ScreeningId.FromValue(@event.ScreeningId),
            customerId: CustomerId.FromValue(@event.CustomerId),
            seatNumber: @event.SeatNumber);

        _reservationRepository.Save(reservation);

        return Result.Success("Reservation created.");

    }
}
0

Ja bym zrobił inaczej - pojedyńczy ticket jest agregatem. Operacja na pojedyńczym ticketcie powinna sie zamykać w jednej transakcji: to jest utworzenie biletu, rezerwacja, zapłata, anulowanie. Dwa bilety na to samo wydarzenie powinno sie dać rezerwować w tym samym czasie.

No ale ja jestem skażony praktyką, wiem że jak ten agregat będzie miał 50k biletów to DB co to trzyma pierdyknie...

0
0xmarcin napisał(a):

Ja bym zrobił inaczej - pojedyńczy ticket jest agregatem. Operacja na pojedyńczym ticketcie powinna sie zamykać w jednej transakcji: to jest utworzenie biletu, rezerwacja, zapłata, anulowanie.

Ten agregat nie bierze pod uwagę dostępnych biletów / miejsc

0

Zarządzanie dostępnymi miejscami to inny problem. Można go rozwiązać na 100 sposobów np. kolejkami - rezerwacaj bierze następny ticket w kolejce, zwraca na sam koniec. Także wybór wolnego miejsca sprawadza się do pewnego query, nie ma nic wspólnego z transakcją więc nie musi być wciskana w agregat.

0

Zgadzam się z przedmówcą

Operacja na pojedyńczym ticketcie powinna sie zamykać w jednej transakcji: to jest utworzenie biletu, rezerwacja, zapłata, anulowanie. Dwa bilety na to samo wydarzenie powinno sie dać rezerwować w tym samym czasie.

Ja bym się zastanowił nad tym czemu ten agregat (część domeny) jest encją bazodanową?
Baza danych to jest infrastruktura, tutaj Ci przeciekają warstwy...
Ja bym oddzielił model biznesowy od danych zapisywanych w repo. Dzięki temu mogę decydować jak chcę to zapisać (mongo, postgres, a może kafka?)

0

Każda abstrakcja cieknie, trzeba myśleć i o DDD i o tym jak to będzie pod spodem działało. Łatwo wymienić Postgres'a na MariaDB ale już ciężko przejść z Mongo na SQLa

0

To może nie od strony ddd. Użytkownik wchodzi na stronę kina. Wybiera kino, seans, widzi widok sali ewentualnie aktualny. Wybiera wolne miejsca. Front nie pozwala wybrać więcej niż 1 raz tego samego miejsca. Skąd pochodzi widok wolnych i zajętych krzeseł dla danego kina i seansu? Pewnie z materialized view (co by te joiny zamieść pod dywan, no i to query musi cykać a nie dzwonić). I teraz każda transakcja kończy się zapisaniem rekordu do bazy key-value, gdzie key to hash (cinemaIDscreaningIDhallIDseatID) a value to boolean. Jednym słowem każda transakcja kończy się sprawdzeniem value, lock, update, unlock 1 rekordu. Dla 100 klientów to pewnie bezsensu. Dla takiego Multikina jak znalazł. Ticket to sprawa poboczna jako etap realizacji skutecznej transakcji.

1 użytkowników online, w tym zalogowanych: 0, gości: 1