Jak wiemy agregat jest jednym z rozwiązań problemu związanego ze współbieżnym dostępem. Załóżmy, że w wybranym rozwiązaniu, chcemy oprzeć decyzje zachodzące w agregacie o zdarzenia. Mogłoby to wyglądać w następujący sposób.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Either<RuntimeException, ParkingSpotOccupiedEvents> occupy(
    BeneficiaryId beneficiaryId,
    SpotUnits spotUnits
) {
  if (outOfUse) {
    return Either.left(new IllegalArgumentException("out of order"));
  }
  if (exceedsAllowedSpace(spotUnits)) {
    return Either.left(new IllegalArgumentException("not enough space"));
  }
  return Either.right(
      ParkingSpotOccupiedEvents.events(
          new ParkingSpotOccupied(
              parkingSpotId,
              Occupation.newOne(beneficiaryId, parkingSpotId, spotUnits))));
}

W ten sposób uzyskujemy funkcyjne podejście. Mamy jakiś stan pobrany z bazy danych i na jego podstawie podejmujemy decyzje. Jeśli się nie dokonać zmiany ze względów biznesowych to możemy np. zwrócić wyjątek. Natomiast jeśli akcja się powiedzie to zwrócimy zdarzenie bądź zdarzenia do opublikowania. Pojawia się w tym momencie pewien problem. Skąd baza danych ma wiedzieć na podstawie jakiej wersji agregatu była podejmowana decyzja?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public Try<Occupation> occupy(
    BeneficiaryId beneficiaryId,
    ParkingSpotId parkingSpotId,
    SpotUnits spotUnits
) {
  return Try.of(() -> {
    if (!beneficiaryRepository.isPresent(beneficiaryId)) {
      throw new IllegalStateException("cannot find beneficiary with id " + beneficiaryId);
    }
    ParkingSpot parkingSpot = findBy(parkingSpotId);

    log.debug("occupying parking spot with id {} by beneficiary with id {}", parkingSpot.getParkingSpotId(), beneficiaryId);
    var result = parkingSpot.occupy(beneficiaryId, spotUnits);

    return Match(result).of(
        Case($Right($()), event -> {
          parkingSpotRepository.publish(event);
          return event.occupied().occupation();
        }),
        Case($Left($()), exception -> {
          throw exception;
        })
    );
  }).onFailure(exception -> log.error("cannot occupy parking spot, reason: {}", exception.getMessage()));
}

No właśnie… W swoim projekcie domeny parkingowej mocno wzorowałem się na library-by-example, ale w przypadku tego repozytorium wydaje mi się, że nie było konieczne aż tak restrykcyjne pilnowanie limitów dla Patron. W moim przypadku chciałem mieć wysoką spójność. Dlatego zdecydowałem się na poniższe rozwiązanie.

1
2
3
4
5
6
record ParkingSpotOccupied(
    @NonNull ParkingSpotId parkingSpotId,
    @NonNull Occupation occupation,
    @NonNull Version parkingSpotVersion
) implements ParkingSpotEvent {
}

Po prostu w zdarzeniu dodałem wersję, na której operowałem podejmując daną decyzję. Nie do końca czuję, że to odpowiednie podejście, ale rozwiązało mój problem. Na pewno powoduje to inne wyzwanie, ale o nim napiszę w kolejnym wpisie.