Core Java

Lambdas for Fluent and Stable APIs

A 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.

public interface UserStore {
  User find(Long id);
  List<User> findByLastname(String lastname);
  List<User> findByCompany(String company);
  ..
}

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.

public interface UserStore {
  User find(Long id);
  List<User> findBy(Predicate<User> p);
}

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.

public final class UserPredicates {
  public static Predicate<User> lastname(String matcher) {
    return candidate -> matcher.equals(candidate.getLastname());
  }
}

Using the new UserStore API has become pretty simple.

static import UserPredicates.lastname;

userStore.findBy(lastname("<lastname>");

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.

public interface UserStore {
  Optional<User> find(Long id);
  List<User> findBy(Predicate p);
}

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.

Optional<User> user = userStore.find(id);
String lastname = user.map(User::getLastname).orElse("");

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?

Optional<User> user = userStore.find(id);
user.ifPresent(passwordReset::send);

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.

public interface Mailer {
  void sendTextMessage(TextMessageBuilder message);
  void sendMimeMessage(MimeMessageBuilder message);
}

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.

public interface Mailer {
  void sendTextMessage(MessageConfigurator configurator);
  void sendMimeMessage(MessageConfigurator configurator);

  @FunctionalInterface
  interface MessageConfigurator {
    MessageBuilder configure(MessageBuilder message);
  }
}

To use the Mailer all we need to do is provide a lambda to build the message.

mailer.sendTextMessage(message ->
  message.from(sender).to(recipients)
      .subject("APIs")
      .body("Lambdas can make for more fluent and stable APIs")
);

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.
 

Reference: Lambdas for Fluent and Stable APIs from our JCG partner Bart Bakker at the Software Craft blog.

Bart Bakker

Bart is a technologist who specializes in agile software development. He is passionate about creating working software that is easy to change and to maintain.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Chistian
11 years ago

It’s a pity Optional is not Serializable so it can’t be used in a remote scenario (RMI/GWT).

Back to top button