Jest to kolejny wpis z serii dotykowanej konkursowi 100commitów. Na ten moment muszę przyznać, że cel dotyczący refaktoryzacji, jaki chciałem osiągnąć, oddalił się. Wychodzę z założenia, że techniki refaktoryzacyjne jednak lepiej się ćwiczy na cudzych projektach. Wtedy musimy odnaleźć wymagania biznesowe zaszyte w kodzie. Natomiast jak piszemy coś własnego i 3 dni później chcemy to refaktoryzować to zaczynamy naginać wymagania. Przynajmniej ja tak mam. Zauważyłem tą tendencję również podczas implementowania docelowego rozwiązania.

Trudno jest być w dwóch kapeluszach jednocześnie - biznesowym oraz deweloperskim. Dlatego porzucam ten jeden z celów i idę dalej. Skupiam się teraz na dostarczeniu działającej aplikacji w domenie parkowania pojazdów na parkingu.

No właśnie, co do docelowego rozwiązania. Tutaj znowu inspiruję się legendarnym już projektem library-by-example. Próbowałem tego podejścia wcześniej przy moim projekcie AnimalShelter, jednak wtedy poległem. Dobra, ale dzisiaj nie o tym. Chciałem Ci zaprezentować wygląd serwisu aplikacyjnego stworzonego na wzór library-by-example. Muszę przyznać, że jest on naprawdę trywialny. Pobieramy dane, wykonujemy logikę biznesową i publikujemy rezultat. Nic więcej.

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
26
27
28
29
30
31
32
@Slf4j
@RequiredArgsConstructor
public class ParkingOnParkingSpot {

    private final ParkingSpots parkingSpots;

    public Try<Result> park(@NonNull ParkVehicleCommand command) {
        return Try.of(() -> {
            ParkingSpot parkingSpot = load(command.getParkingSpotId(), command.getWhen());
            Either<ParkingFailed, VehicleParkedEvents> result = parkingSpot.park(command.getVehicle());
            return Match(result).of(
                    Case($Left($()), this::publishEvents),
                    Case($Right($()), this::publishEvents));
        }).onFailure(throwable -> log.error("Failed to park vehicle", throwable));
    }

    private Result publishEvents(ParkingFailed parkingFailed) {
        parkingSpots.publish(parkingFailed);
        return Rejection;
    }

    private Result publishEvents(VehicleParkedEvents vehicleParked) {
        parkingSpots.publish(vehicleParked);
        return Success;
    }

    private ParkingSpot load(ParkingSpotId parkingSpotId, Instant when) {
        return parkingSpots.findBy(parkingSpotId, when)
                .getOrElseThrow(() -> new IllegalArgumentException("Cannot find parking spot with id: " + parkingSpotId));
    }

}

Takie rozwiązanie naprawdę łatwo testuje się jednostkowo. Według mnie świadczy to o dobrym design.

1
2
3
4
5
6
7
8
9
10
11
12
13
def 'should successfully parking vehicle if parking spot has enough place'() {
  given:
    ParkingOnParkingSpot parkingOnParkingSpot = new ParkingOnParkingSpot(repository)
  and:
    persisted(emptyParkingSpotWith(parkingSpotId, 1))
  
  when:
    def result = parkingOnParkingSpot.park(new ParkVehicleCommand(parkingSpotId, alrightVehicle, Instant.now()))
  
  then:
    result.isSuccess()
    result.get() == Result.Success
}

Prawda, że wygląda to przyjemnie? Jedyną zależnością zewnętrzną tutaj jest repozytorium, które możemy zaślepić na różne sposoby.

Cały ten efekt można osiągnąć poprzez dobre wydzielenie odpowiedzialności modułów. Sam muszę się jednak przekonać o tym czy dobrze to zrobiłem. Ale tego dowiem się dopiero w przyszłości…