Should I rely on “mono of item” or “plain item” arguments when composing reactive chains?

ぐ巨炮叔叔 提交于 2019-12-22 17:22:56

问题


I am have two versions of a Webflux/Reactor handler class. This class mimics a user sign up use case - as far as the business logic is concerned.

The first version of the class relies upon a Mono<User> whereas the second version uses a plain User.

First version of the class: this is the version relying on a Mono<User> argument down the chain. Notice the top level public method createUser uses a userMono.

@Component
@RequiredArgsConstructor
public class UserHandler {

    private final @NonNull UserRepository userRepository;
    private final @NonNull UserValidator userValidator;


    public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
        Mono<User> userMono = serverRequest.bodyToMono(User.class).cache();
        return validateUser(userMono)
            .switchIfEmpty(validateEmailNotExists(userMono))
            .switchIfEmpty(saveUser(userMono))
            .single();
    }

    private Mono<ServerResponse> validateUser(Mono<User> userMono) {
        return userMono
            .map(this::computeErrors)
            .filter(AbstractBindingResult::hasErrors)
            .flatMap(err ->
                status(BAD_REQUEST)
                    .contentType(APPLICATION_JSON)
                    .body(BodyInserters.fromObject(err.getAllErrors()))
            );
    }

    private AbstractBindingResult computeErrors(User user) {
        AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
        userValidator.validate(user, errors);
        return errors;
    }

    private Mono<ServerResponse> validateEmailNotExists(Mono<User> userMono) {
        return userMono
            .flatMap(user -> userRepository.findByEmail(user.getEmail()))
            .flatMap(existingUser ->
                status(BAD_REQUEST)
                    .contentType(APPLICATION_JSON)
                    .body(BodyInserters.fromObject("User already exists."))
            );
    }

    private Mono<ServerResponse> saveUser(Mono<User> userMono) {
        return userMono
            .flatMap(userRepository::save)
            .flatMap(newUser -> status(CREATED)
                .contentType(APPLICATION_JSON)
                .body(BodyInserters.fromObject(newUser))
            );
    }
}

Second version of the class: that is the version relying on a User argument.

@Component
@RequiredArgsConstructor
public class UserHandler {

    private final @NonNull UserRepository userRepository;
    private final @NonNull UserValidator userValidator;

    public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
        return serverRequest
            .bodyToMono(User.class)
            .flatMap(user ->
                validateUser(user)
                    .switchIfEmpty(validateEmailNotExists(user))
                    .switchIfEmpty(saveUser(user))
                    .single()
            );
    }

    private Mono<ServerResponse> validateUser(User user) {
        return Mono.just(new BeanPropertyBindingResult(user, User.class.getName()))
            .doOnNext(err -> userValidator.validate(user, err))
            .filter(AbstractBindingResult::hasErrors)
            .flatMap(err ->
                status(BAD_REQUEST)
                    .contentType(APPLICATION_JSON)
                    .body(BodyInserters.fromObject(err.getAllErrors()))
            );
    }

    private Mono<ServerResponse> validateEmailNotExists(User user) {
        return userRepository.findByEmail(user.getEmail())
            .flatMap(existingUser ->
                status(BAD_REQUEST)
                    .contentType(APPLICATION_JSON)
                    .body(BodyInserters.fromObject("User already exists."))
            );
    }

    private Mono<ServerResponse> saveUser(User user) {
        return userRepository.save(user)
            .flatMap(newUser -> status(CREATED)
                .contentType(APPLICATION_JSON)
                .body(BodyInserters.fromObject(newUser))
            );
    }
}

Now my questions:

  • What are the pros and cons of each of the two versions?

  • Which of the two would you recommend keeping?

Any feedback, advice and opinion welcome.


回答1:


you want feedback here is my feedback

your methods dont really make sense. If you look at the declarations of your methods without the containing code.

private Mono<ServerResponse> validateUser(User user)

this makes no sense, this method should validate a user but you return a ServerResponse? In my opinion a validation should first takes place, and return some sort of boolean, or a list of validation errors.

I do not recommend any of the solutions you have presented, you should instead looking into and start using Mono.error instead of doing switchIfEmpty

You should seperate response building from validation logic.

What happens if validation rules change? or you want other responses based on what validation fails? right now they are together.

you can already see that you are returning the same bad request in two places but with different error messages. Duplications

This is my opinion and what i would do:

  • receive a request
  • map request to a user (bodyToMono)
  • validate the user in a method that vill return a list containing number of errors
  • check this list if user validation has failed, and if failed map the mono user to a mono error containing a illegalArgumentException with some sort of text of the error.
  • map the exception in the mono error to a status code
  • if validation passes save the user in a Mono.doOnSuccess block

this to me is much more clear and predictable with separations of return codes, and validation logic.




回答2:


Using Thomas Andolf's advice together with that of other users, I came up with the following implementation:

@Component
@RequiredArgsConstructor
public class UserHandler {

    private final @NonNull UserRepository userRepository;
    private final @NonNull UserValidator userValidator;

    public Mono<ServerResponse> findUsers(ServerRequest serverRequest) {
        return ok()
            .contentType(APPLICATION_JSON)
            .body(userRepository.findAll(), User.class);
    }

    public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
        return serverRequest.bodyToMono(User.class)
            .flatMap(this::validate)
            .flatMap(this::validateEmailNotExists)
            .flatMap(this::saveUser)
            .flatMap(newUser -> status(CREATED)
                .contentType(APPLICATION_JSON)
                .body(BodyInserters.fromValue(newUser))
            )
            .onErrorResume(ValidationException.class, e -> status(BAD_REQUEST)
                .contentType(APPLICATION_JSON)
                .body(BodyInserters.fromValue(e.getErrors()))
            )
            .onErrorResume(DuplicateUserException.class, e -> status(CONFLICT)
                .contentType(APPLICATION_JSON)
                .body(BodyInserters.fromValue(e.getErrorMessage()))
            );
    }

    private Mono<User> validateEmailNotExists(User user) {
        return userRepository.findByEmail(user.getEmail())
            .flatMap(userMono -> Mono.<User>error(new DuplicateUserException("User already exists")))
            .switchIfEmpty(Mono.just(user));
    }

    private Mono<User> saveUser(User user) {
        return userRepository.save(user);
    }

    private Mono<User> validate(User user) {
        AbstractBindingResult errors = computeErrors(user);
        return errors.hasErrors() ? Mono.error(new ValidationException(errors.getAllErrors())) : Mono.just(user);
    }

    private AbstractBindingResult computeErrors(User user) {
        AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
        userValidator.validate(user, errors);
        return errors;
    }

}

It relies upon Mono.error, custom exceptions, and the onErrorResume() operator. It is equivalent to the two implementations in the question but somewhat leaner.



来源:https://stackoverflow.com/questions/57839652/should-i-rely-on-mono-of-item-or-plain-item-arguments-when-composing-reactiv

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!