Aplikacja Animal Shelter w końcu nabrała rozpędu! Udało mi się utworzyć kawałek kodu spełniającego wcześniej zdefiniowane założenia i wykorzystującego zadeklarowaną infrastrukturę. W tej chwili jest to tylko funkcjonalność akceptacji zwierząt do schroniska, jednak mam już zarys rozwiązania dla innych zadań. Nie jest ono idealne, ale na pewno jest lepsze niż to, które prezentowałem w trzecim artykule tej serii. Przejdźmy, więc od implementacji jaką stworzyłem, ale muszę najpierw dodać pewne sprostowanie.

Lista wszystkich wpisów dotyczących projektu AnimalShelter:
#1 - Opis projektu AnimalShelter
#2 - Pierwsze kroki w backendzie
#3 - Refactoring i prace rozwojowe części serwerowej
#4 - Tworzenie GUI w Angularze
#5 - Zatrzymaj się, przemyśl i zacznij działać!
#6 - Pomysł na architekturę
#7 - Wykorzystanie CQRS
#8 - Ponowna implementacja
#9 - Rozterki architektoniczne
#10 - Podsumowanie + implementacja wysyłki maili
#11 - Programowania ciąg dalszy
#12 - Dopinanie zadań do końca

Gatunki zwierząt w schronisku

Po głębszym zastanowieniu doszedłem do wniosku, że jednak nie ma potrzeby dodawania funkcjonalności, która będzie trzymała dostępne gatunki schroniska poza aplikacją np. w bazie dane. Na tą chwilę pozostanę po prostu przy umieszczeniu ich w kodzie w postaci enuma. Jeżeli zajdzie potrzeba to do dopiero wtedy zrobi się stosowną zmianę. Nie ma co kombinować póki jeszcze dokładnie nie zna się specyfiki biznesowej. Dodatkowo jednym z racjonalnych powodów stojących za trzymaniem gatunków w kodzie jest przecież fakt, że rybki i koty przechowuje się w innych rodzajach klatek… Trzeba by determinować rodzaje dostępnych pomieszczań dla każdego gatunku, aby wiedzieć ile dokładnie jest miejsca w schronisku, a to już wydaje się już wychodzić poza aktualnie zaplanowany rozwój.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package pl.devcezz.animalshelter.animal;

import io.vavr.collection.List;

public enum Species {
  Dog, Cat;

  public static Species of(String value) {
    if (value == null) {
      throw new IllegalArgumentException("species cannot be null");
    }

    String trimmedValue = value.trim();
    if (trimmedValue.isEmpty()) {
      throw new IllegalArgumentException("species cannot be empty");
    }

    return List.of(Species.values())
        .find(animalSpecies -> animalSpecies.name().equals(trimmedValue))
        .getOrElseThrow(() -> new IllegalArgumentException("species cannot be of value " + trimmedValue));
  }
}

Z tego powodu wpadłem na pomysł będący kompromisem. Jeżeli będziemy chcieli zgłosić zwierzaka, którego gatunek jest obecny w schronisku to będzie on szedł standardową procedurą. Jednak w przypadku procesowania innego rodzaju pupila trzeba złożyć wniosek do zarządcy schroniska, aby zdecydował czy można go przyjąć do schroniska. Oczywiście na tą chwilę nie będę tego implementował, ale wydaje mi się to rozsądnym rozwiązaniem, które zasługuje na głębsze zastanowienie się. Jeżeli będzie dużo zgłoszonych zwierząt tą mniej standardową ścieżką to wtedy najwyżej dorobi się mechanizm, który będzie trzymał na zewnątrz gatunki.

Podział na moduły

Myślałem sporo nad enkapsulacją mojego kodu i doszedłem do wniosku, że warto skorzystać z dostępu pakietowego. Jednak wtedy spora część klas znajdowała by się w jednym miejscu zaciemniając cały obraz. W tym momencie bardzo ważnym narzędziem okazał się Maven, który pozwolił mi na trzymanie wszystkiego w jednym pakiecie, ale… w różnych modułach. Dla ułatwienia zamieszczam poniżej grafikę przedstawiającą tą sytuację.

Podział aplikacji na pakiety oraz moduły Mavena
Podział aplikacji na pakiety oraz moduły Mavena

W ten oto sposób mogę mieć pakiet o nazwie a, który będzie w modułach A, B i C. Można dzięki temu skorzystać z klas znajdujących się w pakiecie a modułu A przez klasy pakietu a modułu B. Oczywiście jeśli doda się odpowiednią zależność w pliku pom.xml. Ten zabieg w bardzo prosty sposób porządkuje nam kod i oddziela odpowiedzialności. W module A możemy trzymać nasze obiekty domenowe. Natomiast w module B dodamy zależność do framework’a jakim jest Spring i dzięki niemu zbudujemy np. API REST’owe.

Podział na moduły w kodzie

W przypadku mojego projektu istnieją dwa moduły (zaznaczone na pomarańczowo): webservice oraz application, a w każdym z nich istnieje pakiet pl.devcezz.animalshelter.animal. Moduł webservice posiada klasę AnimalConfig w tym pakiecie, a moduł application - AcceptingAnimal (zaznaczone na zielono). Dodając zależność w pliku pom.xml w module webservice do modułu application uzyskujemy dostęp w klasie AnimalConfig do AcceptingAnimal (zaznaczone na niebiesko). W ten sposób możemy zarejestrować bean dla klasy, która znajduje się w module nie mającym zależności do Springa. Dzięki temu zyskujemy wolność, aby podmienić framework z kontenerem zależności bez ingerencji w logikę biznesową!

Logika akceptowania zwierząt do schroniska

Przyjrzyjmy się teraz w jaki sposób zrealizowałem funkcjonalność akceptowania zwierząt do schroniska. Na początku, gdy przychodzi request następuje walidacja formatu oraz struktury. To pierwsze sprawdzenie zapewnia framework, natomiast struktura musi spełniać poniżej przedstawione wymagania. Oczywiście stworzyłem własną adnotację @ShelterSpecies, aby sprawdzić czy przekazywany gatunek znajduje się wśród akceptowalnych (na ten moment czy jest kotem albo psem).

1
2
3
4
record AcceptAnimalRequest(
    @NotBlank @Size(min=2, max=11) String name,
    @NotNull @PositiveOrZero @Max(30) Integer age,
    @NotBlank @ShelterSpecies String species) {}

Następnie kontroler przekazuje utworzoną przez siebie komendę, na podstawie żądania, na szynę. Następnie mechanizm, który przedstawiłem w tym artykule decyduje komu ma przekazać wybraną komendę do obsługi. W tym przypadku wybiera za cel klasę AcceptingAnimal, która implementuje wymagany interfejs CommandHandler<AcceptAnimalCommand>. W ten oto sposób wykorzystałem stworzoną przez siebie infrastrukturę. Następnym krokiem jest obsługa logiki biznesowej.

Schemat implementacji przyjęcia zwierzaka

Gdy wysłana komenda trafi do AcceptingAnimal to od razu pobierany jest obiekt Shelter stworzony przez fabrykę ShelterFactory. Oczywiście niezbędne dane w postaci limitów oraz ilości obecnych zwierząt w schronisku pochodzą z bazy danych. Następnie to wygenerowany obiekt Shelter decyduje czy dane zwierzątko może zostać zarejestrowane w schronisku czy nie. Jeśli żaden limit nie został przekroczony to generowany jest pomyślny event AcceptingAnimalSucceeded. Gdy przekroczony został limit bezpieczeństwa tworzony jest obiekt klasy AcceptingAnimalWarned. Ten limit to nic innego jak informacja o tym, że zbliża się brak miejsc w schronisku. Oczywiście, gdy nie można przyjąć zwierzątka to tworzymy event AcceptingAnimalFailed.

Odczyt schroniska z bazy danych

W zależności od rodzaju eventu zapisywany jest nowy zwierzak do bazy danych lub nie. Gdy akcja zakończy się pomyślnie lub z ostrzeżeniem to odbywa się zapis i event trafia na szynę. W przypadku błędu również wysyłany jest event, a później rzucany jest wyjątek/błąd dla użytkownika systemu.

Oczywiście infrastruktura szyny eventów decyduje do jakich klas wysłać aktualny event. Tutaj istnieje relacja, że jeden event może mieć wiele handlerów. Jednak w naszym przypadku będzie to 1 do 1. Z tego powodu powstały klasy: HandleFailedAcceptance, HandleWarnedAcceptance oraz HandleSucceededAcceptance. Na ten moment nie ma w nich żadnej logiki, ale to one będą odpowiedzialne za wysyłkę wiadomości email do pracowników.

Obsługa przyjęcia zwierzaka

Jeżeli jesteś zainteresowany kodem rozwiązania to zapraszam Cię serdecznie na mojego GitHuba!

Podsumowanie

W końcu udało mi się pójść do przodu z projektem wykorzystując przy tym własnoręcznie stworzony CQRS. Jak do tej pory nie napotkałem jeszcze żadnych problemów, ale podejrzewam, że w końcu mogą przyjść. Na tą chwilę jednak cieszę się z tego postępu. Wydaje mi się, że właśnie o to mi chodziło! Chciałbym poznać Twoje zdanie co można by jeszcze zrobić inaczej. Byłbym wdzięczny za merytoryczne uwagi! 😊