What is the efficient/proper way to flow multiple objects in reactor
Asked Answered
M

1

14

I am new to reactive programming and to get my hand on I am trying to build a near to real example.

When you see reactor tutorials they show you very easy examples like.

return userRepository.findById(1);

or something like dealing with flux the break the "brown little fox" string and find unique letters etc. But mostly these tutorials stick to single object and unfortunately i am unable to find any guide lines or tutorial which show a side by side examples to type same code first in imperative then in reactive, thats why i see lots of new comers in reactive programming faces a lot of learning issues.

but my point is in real life applications we deals with multiple objects like below sample code I wrote in reactor. Apologies for bad code i am still learning.

public Mono<ServerResponse> response(ServerRequest serverRequest) {

        return
                Mono.just(new UserRequest())
                        .map(userRequest -> {
                            Optional<String> name = serverRequest.queryParam("name");
                            if (name.isPresent() && !name.get().isEmpty()) {
                                userRequest.setName(name.get());
                                return userRequest;
                            }
                            throw new RuntimeException("Invalid name");
                        })
                        .map(userRequest -> {
                            Optional<String> email = serverRequest.queryParam("email");
                            if (email.isPresent() && !email.get().isEmpty()) {
                                userRequest.setEmail(email.get());
                                return userRequest;
                            }
                            throw new RuntimeException("Invalid email");
                        })
                        .map(userRequest -> {
                            userRequest.setUuid(UUID.randomUUID().toString());
                            return userRequest;
                        })
                        .flatMap(userRequest ->
                                userRepository
                                        .findByEmail(userRequest.getEmail())
                                        .switchIfEmpty(Mono.error(new RuntimeException("User not found")))
                                        .map(user -> Tuples.of(userRequest, user))
                        )
                        .map(tuple -> {
                            String cookiePrefix = tuple.getT2().getCode() + tuple.getT1().getUuid();
                            return Tuples.of(tuple.getT1(), tuple.getT2(), cookiePrefix);
                        })
                        //Some more chaining here.
                        .flatMap(tuple ->
                                ServerResponse
                                        .ok()
                                        .cookie(ResponseCookie.from(tuple.getT3(), tuple.getT2().getRating()).build())
                                        .bodyValue("Welcome")
                        );

    }

consider above code first i started with UserRequest object to map querystring in this object. then i need some data from database and so on reactive chaining continue more works to do. Now consider

  • UserRequest Object from first chaining method and
  • User document i fetched from db then i do lot more operations but at the end of chaining i need both of these objects to process final response. The only way to achieve that i found on google is Tuple. but the code look like more dirty after that since in every next operator i have to do
tuple.getT()
tuple.getT2()

So finally i would like to ask is that the proper way or i am missing something here. Because i learned one thing in reactive that data flows nothing more but like in imperative in the middle of logic we got oh i need another variable/object so i define it on top and use it but in reactive after 5th or 6th operator when developer realize ohh i need that object too here that was i created in 2nd operator then i have to go back and pass that in chaining to get in my 5th or 6th operator is that a proper way to do that.

Millwater answered 3/7, 2020 at 19:9 Comment(0)
C
19

There's generally two strategies that can be used to avoid "tuple hell", sometimes in isolation & sometimes in tandem:

  • Use your own "custom" tuple class that's much more descriptive of types (I would nearly always recommend this in production code rather than using the built-in Tuple classes);
  • Concatenate some of your map() / flatMap() calls so that declaring tuples isn't required.

In addition, there's more rules to bear in mind that can help things in general here:

  • Never mutate objects in a reactive chain unless you have no other choice - use immutable objects with the @With pattern instead;
  • Don't use multiple map() calls chained together for returning the same type - favour doing everything in a single map call instead;
  • Farm reusable elements of a long reactive chain out to separate methods, and embed them in your main reactive chain using map(), flatMap() or transform().

If we take the above examples into practice, we can farm the first three map calls out into a single method that "populates" the user object, using the @With style rather than setters (though you can use setters here if you really must):

private UserRequest populateUser(UserRequest userRequest, ServerRequest serverRequest) {
    return userRequest
            .withName(serverRequest.queryParam("name")
                    .filter(s -> !s.isEmpty())
                    .orElseThrow(() -> new RuntimeException("Invalid name")))
            .withEmail(serverRequest.queryParam("email")
                    .filter(s -> !s.isEmpty())
                    .orElseThrow(() -> new RuntimeException("Invalid email")))
            .withUuid(UUID.randomUUID().toString());
}

We can also farm out the part of the chain that looks up a user from the database. This part likely will need some form of new type, but instead of a Tuple, create a separate class - let's call it VerifiedUser - which will take the userRequest and user objects. This type can then also be responsible for generating the response cookie object, and providing it via a simple getter. (I'll leave writing the VerifiedUser task as an exercise for the author - that should be pretty trivial.)

We'd then have a method like this:

private Mono<VerifiedUser> lookupUser(UserRequest userRequest) {
    return userRepository
            .findByEmail(userRequest.getEmail())
            .map(user -> new VerifiedUser(userRequest, user)) //VerifiedUser can contain the logic to produce the ResponseCookie
            .switchIfEmpty(Mono.error(new RuntimeException("User not found")));
}

So now we have two separate, small methods, which each take on a single responsibility. We also have another simple type, VerifiedUser, which is a named container type that's much more descriptive & useful than a Tuple. This type also gives us a cookie value.

This process has meant our main reactive chain can now become very simple indeed:

return Mono.just(new UserRequest())
        .map(userRequest -> populateUser(userRequest, serverRequest))
        .flatMap(this::lookupUser)
        .flatMap(verifiedUser ->
                ServerResponse.ok()
                        .cookie(verifiedUser.getCookie())
                        .bodyValue("Welcome")
        );

The end result is a chain that's safer (since we're not mutating a value in the chain, everything is kept immutable), much clearer to read, and much easier to extend in the future should we ever need to. If we need to go further then we could as well - if the methods created here needed to be used elsewhere for instance, they could easily be farmed out as spring beans conforming to a functional interface, then injected at will (and easily unit tested.)

(As an aside, you're certainly correct that, at the time of writing, there's plenty of trivial tutorials but very little "in-depth" or "real-world" material out there. Such is often the case with reasonably new frameworks, but it certainly makes them hard to master, and results in lots of unmaintainable code out there in the wild!)

Cestar answered 3/7, 2020 at 22:29 Comment(12)
Thanks for such a detailed answer. You clear lot of my confusion and the code you showed is much cleaner. Just one thing that i would like to clear you said we should use immutable object is that mean if i have a class with 10 properties it means after populating all properties using with then it will create 10 object am i right? If that so is that performance efficent?Millwater
@MuhammadIlyas You're very welcome. It will create 10 intermediate objects, but in practice (in my experience at least) the performance overhead will be negligible to non-existent. The overhead in creating a new object is minimal, and those objects will be GC'd incredibly quickly since they're very short-lived. Any negligible performance impact is far and away offset by the cleaner code that comes as a result, at least IMHO.Cestar
i just want to know one more thing u used filter and orElsethrow these are not reactive operators I think it is java 8 stream api. so using it here is safe? i means its a blocking thing.Millwater
@MuhammadIlyas You can use it safely as long as you're not using it on a blocking operation, which is why it's perfectly safe here. (It's the Optional API which is similar to the stream API.)Cestar
Alright. i am completely agree if its not a blocking operation than its fine. but just for curiosity to achieve this in more reactive way i tried this code serverRequest.queryParam("name").map(Mono::just).orElseGet(Mono::empty) .filter(userId -> !user.isEmpty()) // further reactive chaining. so whats your opinion about that is that safe too?Millwater
@MuhammadIlyas It's safe and it'll work fine, but I prefer not wrapping in Mono.just() if at all possible. A mono or flux implies that you're dealing with a reactive operation when, in this case, you're not - which makes your code less clear IMHO. By keeping it to the optional or stream API, you make it abundantly clear that this is a synchronous, non-blocking operation.Cestar
@MichaelBerry I would prefer builder rather then with. The code looks similar and does not create redundant instances.Stuffy
@HonzaZidek You can use a builder, but then you need to remember to instantiate all the fields copied from the original object - whereas using "with", you only need to worry about the ones that have changed. That makes it less error prone for only the cost of a few intermediate objects, which are hoovered up pretty instantaneously by the GC cycle anyway.Cestar
@MichaelBerry The compiler does not let you forget instantiating the object. This is a completely artificial worry. And this is not necessarily "a few" redundant objects - if such a thing is called in a loop or really many times it can become a burden if for the contemporary GC. And using a builder does not lessen the readability.Stuffy
@HonzaZidek The compiler doesn't ensure that you've instantiated all fields when using a standard builder (the "step builder" pattern can, but that's much less common.) Re: readability - if you have a large number of other fields it can certainly make the code more verbose & therefore less readable. GC certainly can be an issue with huge numbers of withers in some circumstances, but in my experience it's not most of the time - as always, I'd benchmark and check before assuming the worst.Cestar
@MichaelBerry If you use the "step builder", I suppose each with returns a different type? In that case you not only infest the heap with unnecessary objects, but you also infest the application with a lot of unnecessary classes. And it's just enough to add validation to the last build() step to ensure that you've instantiated all fields. I understand the usefulness of the "step builder" only in more complex cases, like reactor.test.StepVerifier or org.springframework.r2dbc.core.DatabaseClient. but not just as a replacement of common setters.Stuffy
@HonzaZidek I agree, I wasn't advocating the use of the step builder at all - simply stating that was the only way to get a compile-time guarantee that you've instantiated all your fields in a builder. You can of course add validation to your build method, but that will only work as a runtime guarantee, not a compile time one.Cestar

© 2022 - 2024 — McMap. All rights reserved.