Lambdas for Fluent and Stable APIs
November 19th, 2013A few weeks ago I wrote an introduction on Java 8 lambdas. In this introduction I explained what a lambda is and how to use them in conjunction with the new Stream API, that is also introduced in Java 8.
The Stream API provides a more functional interface to collections. This interface heavily depends on lambdas. But there’s much more to lambdas than improved collection processing
Lambdas offer you a chance to build much more fluent APIs. To show this, as an example I like to use a UserStore
which facilitates fetching and saving users using a database. Its public API commonly looks like the following.
The list of findBy
methods often is longer than just the two I’ve included here. As the system grows it’s likely that there will be others. While that works, the thing is, all these methods basically do the same thing. They return all the users with a property that matches a specific value.
Some frameworks offer work-arounds for this mess. If you’ve used Hibernate you’re probably aware that they provide a work-around to this with findByExample
where you provide a User
as an example object that provides the properties and values to query by. Any value that is set in this example object is used to query by, while any field that is null
is excluded from the query. You can tweak this behavior a little, but there are a number of problems with this approach. Think of default values, required fields (i.e. fields that cannot be made null
), and immutability. iBatis, MyBatis, and also Spring Data, use code generation to save you time implementing all these methods, leaving the API bloated with a list of findBy
methods.
These work-arounds might come a long way, but they do leave their own specific problems behind. A different approach would be to use lambdas.
Lambdas can help us to decouple the query part from the filter specification. Let’s change the findBy
functions to a single function that accepts a lambda.
That’s a much better API. Obviously it’s a bit naive as the predicate checks a User
object; you generally want to filter using your database query. Nonetheless it serves the purpose of this example pretty well, and you can experiment with your own lambdas to filter using database queries. [Note: Predicate
comes with Java 8 and is located in the java.util.function
package.]
Before you might want to get mad because at least with the previous API the predicates were bundled in one place, we can still bundle (common) predicates. For example, by creating a utility class UserPredicates
that contains them.
Using the new UserStore
API has become pretty simple.
There one thing left in UserStore
that really bothers me, though. The find(id)
function returns a user. But what if there’s no such user?
Optional
To improve on this we can (and should) look at another new feature of Java 8, Optional. This is sort of the Java implementation of the maybe monad. It looks a lot like Scala’s Option
.
With Optional
we can better express that a function may return a value but not necessarily, and prevent using null
. In our find(id)
example returning Optional
clearly expresses that we might find a user with the requested ID, but maybe no such user exists.
Not only does the API now document the fact that you might get a user, it also never returns null
. I consider an API that never returns null
much safer. One day a new programmer might not realize that find
can return null
and a null-pointer exception is the result. Just hopefully the team catches it before production. Null-pointer exceptions are really easy to prevent, as long as you never use null
.
We can use functions on Optional
to get a value from user if available, or to get a default value otherwise. For example, to safely get the user’s lastname we write the following.
This code is pretty expressive and doesn’t require a lot of explaination. If there’s a user, get it’s lastname. Otherwise, get an empty string.
What if we need to send the user a password-reset email, if it is found?
The password-reset is sent if a user is found, otherwise nothing happens.
Since Java does not support destructuring like many other languages that provide maybe (e.g. Haskell, Clojure, and Scala), we are limited to the functions of Optional
. This makes Optional
weaker than its equivalent in any of these languages.
Builder
Of course not only the APIs of repositories can benefit from lambdas. Optional
is also a good example of an API that benefits from lambdas. Personally I also find lambdas particulary useful to replace passing builders around. Rather than passing a specific builder to a function, often it improves decoupling by yielding a builder from the function. Let me show you an example for sending email to clarify this idea.
To use the Mailer
we need to pass a specific builder into it. These builders have a common interface but are different in the kind of message they build. And the Mailer
has separate methods because it must add different information based on the type being used. Because of this any client-code is tightly coupled to pass the correct builder.
As you probably suspect, this is where lambdas are helpful. Rather than requiring the client to create a builder and pass it in, the Mailer
functions can create the builder they need and yield it to a lambda.
To use the Mailer
all we need to do is provide a lambda to build the message.
The API now is much more stable. Client-code is decoupled from any changes in specific builders, and won’t break as long as functions on the builder remain compatible.
As the examples helped me show, lambdas can help you build more fluent and stable APIs that are more intention revealing. These APIs require not much documentation for other programmers to get going because it’s actually hard for them to get it wrong. As a general guideline I prefer clear and intention revealing code over documentation. Fix, don’t document.
Of course I’ve only shown a few examples in this article. Lambdas are much more widely applicable than only with the examples here. I hope this article provided you with some new insights in what lambdas can help you with, and that you can think of ways how they can improve your code.