Łączenie wyników kilku Eitherów

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

 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

  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

 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.

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:

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)

0

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

 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.

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?

0

A czemu nie po prostu? ;)

Either<DomainError, SuccessMessage> createUser(CreateUserDto userDto) {
        return validate(userDto)
           .map(this::create)
           .mapLeft(DomainError::toEitherLeft);
    }
0
danek napisał(a):

A czemu nie po prostu? ;)

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

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