These days using functional style is, well, kinda trendy. There are a lot of tutorials/articles/posts/etc. with explanation how to use Java 8 Streams, RxJava or Project Reactor. While all these explanations are useful, they often leave impression that those are the only ways/approaches for functional style in Java. Which is not the case. There are many other functional techniques. One of them are Monads.
As I've mentioned before Monad is a functional programming concept. Being properly applied this concept enables writing expressive, concise, easy to test code. But (there is always "but") proper application of this concept requires somewhat different thinking.
To show how to apply this concept to Java code I'll start with simple Java code written in traditional style and show how to refactor it into equivalent code which uses Option
Monad.
At the beginning there was the code...
So, the original code looks quite similar to traditional REST controller/request handler/etc. It receives some parameter, calls services to get necessary pieces of data and fills response container using returned values:
public UserProfileResponse getUserProfileHandler(final User.Id userId) {
final User user = userService.findById(userId);
if (user == null) {
return UserProfileResponse.error(USER_NOT_FOUND);
}
final UserProfileDetails details = userProfileService.findById(userId);
if (details == null) {
return UserProfileResponse.of(user, UserProfileDetails.defaultDetails());
}
return UserProfileResponse.of(user, details);
}
Nothing special, as promised.
Lets start refactoring this code step by step. Since only function body changes, remaining lines of code are omitted for brevity.
Step 1
First step is to move creation of the details inside response creation. This allows us to omit whole branch and remove duplication:
final User user = userService.findById(userId);
if (user == null) {
return UserProfileResponse.error(USER_NOT_FOUND);
}
final UserProfileDetails details = userProfileService.findById(userId);
return UserProfileResponse.of(user,
details == null
? UserProfileDetails.defaultDetails()
: details);
Readability of this version suffered little bit, but removing code duplication worth little bit more in the long run.
Step 2
At this step I'm going to introduce use of Option
. I'll start from the last statement since it was looking less readable:
final User user = userService.findById(userId);
if (user == null) {
return UserProfileResponse.error(USER_NOT_FOUND);
}
final Option<UserProfileDetails> details = Option.option(userProfileService.findById(userId));
return UserProfileResponse.of(user,
details.otherwiseGet(UserProfileDetails::defaultDetails));
OK, looks somewhat better. Readability is restored, but brevity suffered a bit.
Step 3
At this step I'm going to continue adding Option
. Notice how two last statements from the previous version were moved unchanged to the place, where we actually have non-null user
value:
final Option<User> user1 = Option.option(userService.findById(userId));
return user1.map(user -> {
final Option<UserProfileDetails> details = Option.option(userProfileService.findById(userId));
return UserProfileResponse.of(user, details.otherwiseGet(UserProfileDetails::defaultDetails));
})
.otherwiseGet(() -> UserProfileResponse.error(USER_NOT_FOUND));
This version has problems with indentation and definitely requires some inlining and other cleanups. I'm showing it here for sole purpose to show, probably, one of the key moments when starting using monads: code which requires value from Monad is moved unchanged into lambda. The lambda then is passed to one of mapping (to transform monad) or application methods (which are used for creating side effects). This pattern is common for all Monads and using it we can enjoy the full power of Monads. For example, writing asynchronous processing with Monads is as easy as writing traditional synchronous code.
So, let's do final cleanups.
Step 4
Final cleanups, inlining, etc., including static import for Option.option
:
return option(userService.findById(userId))
.map(user -> UserProfileResponse.of(user,
option(userProfileService.findById(userId))
.otherwiseGet(UserProfileDetails::defaultDetails)))
.otherwiseGet(() -> UserProfileResponse.error(USER_NOT_FOUND));
Now entire handling is expressed as single compact statement. There are possible some more cleanups (for example, extracting constant error response), but such a changes require changes outside function body.
I'd like to see in comments feedback on the final version of code (readability, expressiveness, etc.).