2015-03-22

Clean Code and Java 8

I write more and more Java 8 code. But within the last week I wondered if streams and lambdas may lead to code that nobody understands (like generics back in the days). But I’ll give you a few examples and my opinion about it.

Messy Code because of Streams and Lambdas?

I wrote some code like this:

items.stream()
.filter(item -> mapper.hasMappingFor(item))
.map(item -> mapper.map(item))
.findFirst();

Wouldn’t it be more readable for other developers seeing the code for the first time, if I write it like that?

for(Item item : items) {
if (mapper.hasMappingFor(item)) return mapper.map(item);
}

Or does it just feel that way because I see these for-loops every day? In general, I really like fluent APIs and for me the stream-solution seems to be very elegant. Are developers complaining about Java 8 code because they are just old-fashioned? Or is it really harder to understand and maybe also harder to maintain?

Elegance of Streams and Lambdas

Last week I attend at one of our internal coding events and we worked on the yahtzee kata. I had to leave early but because I had a lot of ideas, I worked on that kata on the train, focusing on Java 8 features and check if the code feels clean. The problem I was solving: get all possible yahtzee categories for a given roll. I solved that with an enumeration of categories, each defining an predicated against a given roll is tested.

public Set<Category> possibleCategories() {
return Stream.of(Category.values())
.filter(category -> category.isPossibleCategoryFor(dice))
.collect(Collectors.toSet());
}

And here the definition of the single categories and the check if a dice matches the category:

enum Category {
ONES(hasAtLeastOne(ONE)),
TWOS(hasAtLeastOne(TWO)),
THREES(hasAtLeastOne(THREE)),
FOURS(hasAtLeastOne(FOUR)),
FIVES(hasAtLeastOne(FIVE)),
SIXES(hasAtLeastOne(SIX)),
THREE_OF_A_KIND(atLeastOneDieOccursAtLeastXTimes(3)),
FOUR_OF_A_KIND(atLeastOneDieOccursAtLeastXTimes(4)),
FULL_HOUSE(atLeastOneDieOccursAtLeastXTimes(3)
.and(anotherDieOccursAtLeastYTimes(2))),
SMALL_STRAIGHT(dicesInARow(4)),
LARGE_STRAIGHT(dicesInARow(5)),
YAHTZEE(allDiceAreEqual()),
CHANCE(matchesForEveryDice());

private Predicate<List<Dice>> rule;

private Category(Predicate<List<Dice>> rule) {
this.rule = rule;
}

public boolean isPossibleCategoryFor(Dice dice) {
return rule.test(dice);
}
...
}

Of course you could implement something like this with earlier Java versions as well. Only the method implementations are different.

Here I struggled again, when I wrote the method creating possible straights:

private static Set<Set<Dice>> possibleStraights(int numberOfDice) {
return IntStream.range(0, Dice.values().length - numberOfDice)
.mapToObj(startDice -> IntStream.range(startDice, startDice + numberOfDice)
.mapToObj(die -> Dice.values()[die]).collect(Collectors.toSet()))
.collect(Collectors.toSet());
}

I fear everyone of you thought “WTF?” at the first sight. Although I chose an self-explanatory name (at least I hope so), it’s not clear, what the code does. You may hope, that it really creates a set of possible straights. But after I extract a method it seems more readable again.

private static Set<Set<Dice>> possibleStraights(int numberOfDice) {
return IntStream.range(0, Dice.values().length - numberOfDice)
.mapToObj(startDice -> createStraightStartingAt(startDice, numberOfDice))
.collect(Collectors.toSet());
}

private static Set<Dice> createStraightStartingAt(int startDice, int numberOfDice) {
return IntStream.range(startDice, startDice + numberOfDice)
.mapToObj(die -> Dice.values()[die]).collect(Collectors.toSet());
}

You can find the code on GitHub.

Conclusion

I really think my feeling that the Java 8 code is less readable and maintainable, is just because these Java syntax is quite new. (And maybe I don’t trust other developers, that they are familiar with these new features.)

I am quite sure, that the code is as easy to read and maintain as the “old-fashioned” java style. And in a few month it will be completely common. But we should always think twice, if streams and lambdas are the best possible solution for our problem. Just because we have a hammer, not everything is a nail.

But what do you think? Do you think the code is less cleaner because of the Java 8 syntax?