Spring clean architecture

3

Cześć,
Zacząłem ostatnio tworzyć projekt którego głównym celem jest zapoznanie się z architekturą heksagonalna, separacją logiki domenowej od frameworka (Spring) oraz poznanie biblioteki Vavr
Funkcjonalnie ma to być Internetowy dziennik treningowy, czyli układanie treningów / diety, śledzenie swoich postępów, itp. Póki co jednak nic z tego nie jest ruszone, zacząłem od modułu użytkownika który obecnie umożliwia rejestracje i potwierdzenie założenia konta.
Po stronie infrastruktury, skonfigurowane jest security na podstawie JWT oraz połączenie do MongoDB.
Obecnie siadam do frontendu (Angular) i bardzo zależałoby mi na review tego co obecnie napisałem.

https://gitlab.com/angryprogrammer01/springcleanarchitecture

1

Wygląda to naprawdę nieźle! :) z plusów:

  1. klasy package scope
  2. podział na pakiety per feature
  3. zastosowanie dependency inversion principle.
  4. kod jest czytelny
  5. testy jednostkowe przez fasadę, a nie na wszędobylskich mockach

Nad czym bym się zastanowił:

  1. Brak testów integracyjnych - skąd wiesz, że Twoja aplikacja zadziała na produkcji?
  2. Klasa ResponseResolver - użyj @ControllerAdvice
  3. Jest kilka „worków”, które mogą się rozrastać - np. ten ObjectCreator w testach, czy pakiet „common” - tutaj możesz zrobić podpakiet „vavr” na extensiony stricte dotyczące Vavra
  4. Brak struktury given/when/then testów (lub arrange/act/assert), przez co są nieczytelne i sprawdzają za dużo. Brakuje mi tez testów na ścieżki alternatywne - widać, że nie robisz TDD.
  5. Kolizja nazwy klasy ObjectMapper. Wiem, że to domena i nie wie o Springu, natomiast jest to i tak mylące. Poza tym nazwa aby ogólna, proponuję UserConverter.
  6. Nie podoba mi się, że fasadę do testów tworzysz inaczej niż produkcyjną - testowa metoda powinna wołać produkcyjna, abyś testował kod, który będzie odpalany na prodzie.
  7. Jeden kontroler i fasada zarówno do pobierania zasobu Usera, jak i do jego rejestracji i linków aktywacyjnych - te ostatnie nie są raczej zasobami w rozumieniu REST oraz będą miały pewnie inne reguły security. Rozważyłbym rozwód kontrolerów, moduł może zostać jeden.

Na zakończenie powtórzę - jest nieźle! :)

0

@Charles_Ray: co do punktu 6, czyli tworzymy Fasadę tą samą metodą w Configuration, ale po prostu przekazujemy tam w parametrach nasze implementacje InMemory?

0

@weiss: w testach możesz od razu zrobić new XFacade(cosInMemory, cosInMemory) bez potrzeby konfiguracji itp. Klasy Configuration są zazwyczaj tylko na potrzeby spięcia całości frameworkiem itp

0

Czyli klasa Configuration przyda się nam jeśli będziemy z Fasady musieli zrobić Bean żeby mógł on tam podpiąć swoje adnotacje potrzebne do działania. Czyli np. przekazujemy Springowej DAO?

0

Bardzo fajny kod. Mi też się podoba :).
Tylko, mam pytanie: Czy na pewno dobrym pomysłem jest transformacja z Dto -> Obiekt domenowy -> Dto -> encja? Czy to nie jest już trochę overengineering? :)

0

Czyżbym widział pewną inspiracje? ;) Z rzeczy które teraz na szybko znalazłem

User userToEntity(UserDto user) {
        if(user == null) return null;
//uciete
    }

Nie ładnie zwracać nulle ;)

   private Success deactivate(ActivationTokenDto tokenDto) throws RuntimeException {
        var deactivatedToken = ActivationTokenDto.builder()
                .tokenID(tokenDto.getTokenID())
                .username(tokenDto.getUsername())
                .activated(true)
                .expirationDateTime(LocalDateTime.now())
                .build();
        tokenRepository.updateToken(deactivatedToken);
        return new Success();
    }

Czemu RuntimeException?

private Either<Error, Success> activateUser(UserDto userDto) {
        var user = mapper.userToEntity(userDto);
        user.activate();
        return Try.of(() -> activate(mapper.userToDto(user)))
                .onFailure(e -> log.severe(e.getMessage()))
                .toEither(UserError.PERSISTENCE_FAILED);
    }

Jeśli activate(mapper.userToDto(user)) sie wysypie to nie jest problem ze user.activate() się wykonało?

@Charles_Ray: ResponseResolver jest po to, żeby nie używać wyjątków ;)

0

Tylko, mam pytanie: Czy na pewno dobrym pomysłem jest transformacja z Dto -> Obiekt domenowy -> Dto -> encja? Czy to nie jest już trochę overengineering? :)

Doszedłem do wniosku że nie, przynajmniej nie w projekcie tego kalibru. Pozbyłem się obiektów domenowych, model==dto jest teraz publiczny, a cała logika przeniesiona jest do serwisów realizujących use case.

Nie ładnie zwracać nulle ;)

To już poprawione ;)

Czemu RuntimeException?

Zastanawiam się właśnie co robić w przypadku błędów jakie mogą lecieć z bazy?

Jeśli activate(mapper.userToDto(user)) sie wysypie to nie jest problem ze user.activate() się wykonało?

@Transactional po stronie repozytorium?

0

Zastanawiam się właśnie co robić w przypadku błędów jakie mogą lecieć z bazy?

Zazwyczaj wtedy connector do bazy rzuca wyjątkiem (i nawet wtedy powinien)

2

Dlaczego plik UserConfiguration znajduje się z w katalogu z domeną? Z tego co widzę ma on adnotacje ze Springa (@Configuration, @Configuration), czy nie lepiej byłoby go przenieść do jakiegoś innego pakietu, w którym konfigurujesz sobie rzeczy związane ze Springiem? Np. pakiet configuration.

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