Expressive code: combining Guava, LambdaJ and Hamcrest

19 Oct 2011

Disclaimer: I'm a big fan of static imports. A quick Alt-Enter in IntelliJ or Ctrl-M in Eclipse often makes the code a lot more legible, and the IDE can always help you navigate if you're drawing a blank. Since you can't Ctrl-click a blog post, just leave a comment if it's not clear which library a static function is from.

Matchers for unit tests

Hamcrest matchers have been growing in popularity over the last few years, especially against plain old Junit asserts. The main advantages are probably that:

It's now pretty common to see test code that ends like this:

assertThat(item.getDiscountType(), is(XMAS_SPECIAL));
assertThat(basket.getItems(), hasItem(new Book("The art of testing")));

In fact it's so common that people often associate "Hamcrest matchers" with "Unit tests"… but it doesn't have to be this way! If they can make our test code so expressive, can you imagine the gain for production code?

Matchers in production code

In fact, another popular Java library uses matchers: LambdaJ. If you're not using it yet, it can be summed up as functional awesomeness applied to Java collections, and I strongly suggest you check it out. A typical filtering in LambdaJ will look something like this, involving the having function. And having returns a hamcrest matcher!

List<Item> cheapItems = select(items, having(on(Item.class).getPrice(), lessThan(10)));

In our current project we ended up having a good set of "domain" matchers, allowing us to work with collections in a very nice way:

public static LambdaJMatcher<Item> hasDiscount(DiscountType discount)
{
    return having(on(Item.class).getDiscountType(), is(discount));
}   

List<Items> xmasItems = select(items, hasDiscount(XMAS_SPECIAL));

Now since LambdaJ matchers are just specialised Hamcrest matchers, we can even start combining them with our favourite Hamcrest filters. All we need is a small helper functions to execute matchers in a type-safe way, and we can now write the following code. Hopefully you can understand the 3 different conditions, which almost read like plain english.

if (check(basket.getItems(), hasItem(hasDiscount(XMAS_SPECIAL)))      { ... }
if (check(basket.getItems(), not(hasItem(hasDiscount(XMAS_SPECIAL)))) { ... }
if (check(basket.getItems(), hasItem(not(hasDiscount(XMAS_SPECIAL)))) { ... }

… and the answer is:

That's already quite expressive! I find that writing this type of code not only helps a lot with maintainability, but also when communicating with testers, BAs or business representatives, in a good DDD spirit.

LambdaJ + Guava

However LambdaJ is mostly about filtering and transforming data… what if we want more? Some other parts of our project made heavy use of Guava, which packs a lot of other useful features like removing items from a list. Maybe there is a way to leverage some of Google's work? All we had to do is build a bridge between Hamcrest's matchers and Guava's predicates, and here we are. The following code removes any items with a specific discount from the list.

List<Items> items = ...;
removeIf(items, predicate(hasDiscount(XMAS_SPECIAL)));

Here's the two small helper methods we had to write, a small price to pay for nice code!

// type safe matcher
public static <T> boolean check(T actual, Matcher<? super T> matcher) {
    return matcher.matches(actual);
}

// convert hamcrest matcher to guava predicate
public static <T> Predicate<T> predicate(final Matcher<T> matcher)
{
    return new Predicate<T>() {
        @Override
        public boolean apply(T item)
        {
            return matcher.matches(item);
        }
    };
}

So of course this might look like a lot of code if you're working with a nicer language, Ruby Scala or the likes. There will also probably be Java 7 libraries that do this in a cleaner fashion in the near future… but in the enterprise world Java 6 still has many "good" days ahead, and I find that these techniques bring a lot of expressiveness to what could be a big ball of code Smiley

A small bug...

As a side note, a bug in the JDK 1.6 means that in some cases it won't infer the right generic type for Hamcrest. Sigh. If that's the case, give the compiler a hand by extracting the matchers as separate methods, for example:

public static Matcher<Iterable<? super Item>> hasItemWithDiscount(DiscountType discount)
{
    return hasItem(hasDiscount(discount));
}

Alternatively, if you don't need the contravariance you can also create simpler aliases to hasItem and hasItems:

public static Matcher<Iterable<E>> hasSingleItem(E value)
{
    return hasItem(value);
}

Comments