Łączenie wyników kilku Eitherów

Łączenie wyników kilku Eitherów
AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0

Piszę swoją aplikację korzystając z biblioteki Vavr i flow programu jest sterowany za pomocą Either.

Mam jednak mieszane uczucia czy w ogóle dobrze do tego podchodzę...
Mam metodę która tworzy użytkownika, pod spodem której dzieje się całkiem sporo
-sprawdzana jest poprawność danych
-walidacja czy taki użytkownik już nie istnieje
-generowanie tokenu aktywacji konta
-zapisanie użytkownika na bazie
-wysyła maila z linkiem do aktywacji

I większość z tych metod, które są wołane, zwraca Either<Error, Success>

Po kolei wygląda to tak, sama metoda createUser oraz metody pomocnicze

Kopiuj
 Either<DomainError, SuccessMessage> createUser(RegisterUserDto registerUserDto) {
        var validationError = validate(registerUserDto);
        if(validationError.isPresent()) return Either.left(validationError.get());
        final var token = generateToken(registerUserDto.getUsername());
        final var user = User.createUser(registerUserDto);
        final var savedUser = user.map(this::saveUser);
        return savedUser.isRight() && token.isRight() ?
                emailFacade.sendActivationEmail(registerUserDto.getUsername(), registerUserDto.getEmail(), token.get())
                : Either.left(savedUser.getLeft());
    }

 private Optional<DomainError> validate(RegisterUserDto userDto) {
        if(userRepository.findByUsername(userDto.getUsername()).isPresent()) return Optional.of(UserError.USERNAME_ALREADY_EXISTS);
        if(userRepository.findByEmail(userDto.getEmail()).isPresent()) return Optional.of(UserError.EMAIL_ALREADY_EXISTS);
        return Optional.empty();
    }

private Either<DomainError, String> generateToken(String username) {
        return Try.of(() -> tokenRepository.generateToken(username))
                .onFailure(e -> log.severe(e.getMessage()))
                .toEither(UserError.PERSISTENCE_FAILED);
    }

    private Either<DomainError, User> saveUser(User user) {
        return Try.of(() -> save(user))
                .onFailure(e -> log.severe(e.getMessage()))
                .toEither(UserError.PERSISTENCE_FAILED);
    }

    private User save(User user) {
        userRepository.saveUser(objectMapper.userToDto(user));
        return user;
    }

Encja usera

Kopiuj
  static Either<DomainError, User> createUser(RegisterUserDto dto) {
        return validEmail(dto.getEmail()) ?
            Either.right(User.builder()
                    .username(dto.getUsername())
                    .password(dto.getPassword())
                    .email(dto.getEmail())
                    .active(false)
                    .roles(Set.of("USER"))
                    .build()) :
            Either.left(UserError.INVALID_EMAIL);
    }

No i inna domena która jest tu wołana

Kopiuj
 public Either<DomainError, SuccessMessage> sendActivationEmail(String username, String receiver, String token) {
        return emailService.sendEmail(username, receiver, token);
    }

No i zastanawiam się czy to w ogóle ma ręce i nogi, ciężko o jakieś bardziej skomplikowane przykłady w Internecie więc fajnie jakby ktoś kto korzysta z tego na co dzień spojrzał krytycznym okiem.

Charles_Ray
Abstrachując od samego Vavra, Optional#isPresent zwykle wskazuje na code smell. Poza tym Vavr ma swojego Option, może da się to ładniej schainować bez tych checków isLeft, isRight, isPresent. Jak się nie da to bez sensu cała idea IMO
danek
  • Rejestracja:ponad 10 lat
  • Ostatnio:7 miesięcy
  • Lokalizacja:Poznań
  • Postów:797
1

Jeśli dobrze zaplanujesz flow, da się taki ciąg wywołań zastąpić listą map()/flatMap() i możesz osiągnąć przykładowo coś takego:

Kopiuj
synchronized Either<UserError, UserEntity> registerUser(LoginUserInfo user) {
        return userValidator
                .validate(user)
                .map(this::createNormalUserEntity)
                .map(this::saveToRepository);
    }

Cała 'trudność' polega na tym, żeby odpowiednie operacje (te które podałeś w poście) odpowiednio podzielić na warstwy, czyli nie mieć jednego ciągu wywołań w jednym miejscu, a rozdzielić na odpowiednie obiekty (tak jak np tutaj całą walidacje mam przeniesioną gdzieś indziej, u Ciebie byłoby np. wysyłka maila byłby gdzieś 'wyżej' niż samo rejestrowanie użytkownika)


Spring? Ja tam wole mieć kontrole nad kodem ᕙ(ꔢ)ᕗ
Haste - mała biblioteka do testów z czasem.
AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0

Chyba udało mi się to trochę uprościć

Kopiuj
 Either<DomainError, SuccessMessage> createUser(CreateUserDto userDto) {
        var validationErrors = hasValidationErrors(userDto);
        return validationErrors.isPresent() ? Either.left(validationErrors.get()) : create(userDto);
    }

    private Either<DomainError, SuccessMessage> create(CreateUserDto userDto) {
        return User.createUser(userDto)
                .flatMap(this::saveUser)
                .flatMap(this::generateToken)
                .flatMap(token -> sendEmail(userDto, token));
    }

I nawet to działa. ale chciałbym się pozbyć tego isPresent i zrobić np.

Kopiuj
Either<DomainError, SuccessMessage> createUser(CreateUserDto userDto) {
        return hasValidationErrors(userDto).map(DomainError::toEitherLeft).orElse(create(userDto));
    }

Problem w tym, że pomimo tego że podaje request który nie przechodzi walidacji i w odpowiedzi dostaje poprawny komunikat, to i tak wykonuje się część w orElse, czyli zapis na bazie, wysłanie maila... Nie da się tego zapisać za pomocą map/ifPresent?

danek
  • Rejestracja:ponad 10 lat
  • Ostatnio:7 miesięcy
  • Lokalizacja:Poznań
  • Postów:797
0

A czemu nie po prostu? ;)

Kopiuj
Either<DomainError, SuccessMessage> createUser(CreateUserDto userDto) {
        return validate(userDto)
           .map(this::create)
           .mapLeft(DomainError::toEitherLeft);
    }

Spring? Ja tam wole mieć kontrole nad kodem ᕙ(ꔢ)ᕗ
Haste - mała biblioteka do testów z czasem.
edytowany 1x, ostatnio: danek
AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0
danek napisał(a):

A czemu nie po prostu? ;)

Kopiuj
Either<DomainError, SuccessMessage> createUser(CreateUserDto userDto) {
        return validate(userDto)
           .map(this::create)
           .mapLeft(DomainError::toEitherLeft);
    }

Rozumiem że validate() musiałoby zwracać Either<DomainError, SuccessMessage> ?
screenshot-20190810184416.png

danek
albo to albo Either<List<DomainError>, SuccessMessage>

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.