As promised in my last Java 8 post, I want to throw newer Java functionality at some of the old Gang of Four Design Patterns to end up with some good examples for both, while including a taste of the difficulty that can come with real world problems. In terms of complexity, I may have failed in this case, because the Java 8 feature I wish to partner with the Visitor pattern fits the problem space like a glove. If you haven’t guessed already, it’s ParallelStreams. Worry not, however, I’m going to turn it up to 12 in the end (Java 12 that is) and show you how this functionality can deal with some awkward user requirements.
High Level Overview
It’s not my goal to educate on the basics, but to illustrate functionality and usability. However, I don’t want to leave anyone behind so very briefly I’ll cover; what is the Visitor pattern, and what are ParallelStreams.
Visitor is a design pattern used to perform a high level operation on a structure of any form. It can be used in compilers and interpreters. Here, you can see that if we traverse any code segment, how any ‘word’ is interpreted is completely based on its context (e.g. the line ‘helloWorld()’ is only valid if the function ‘helloWorld’ has previously been declared).
Streams are a method of performing operations on a Java collection, in a functional manner, allowing us to make some tasks much simpler. ParallelStreams perform the same job, but they create a new thread for each object in the collection. I hope you can see the overlap there. There are common keywords. However, I wanted to see how that plays out in practice.
The Example
My thoughts are a hybrid of eBay and Amazon where you can put a collection of items in your shopping cart, with an offer amount for each. When you, the customer, click a button, the code then either accepts, rejects or makes a counter offer. Of course this requires each of the applicable items to be sent to a back-end server with the offer amount, to make the decision. Also there are vouchers which can be purchased, whose price is set, and for which no back-end interaction is required while pricing the shopping cart. The main goal of a voucher is to show how my visitor can behave differently based on the class of the object it is passed into.
The Visitor
As a simple matter of following the instructions in the Gang of Four book, I referred to my two ‘Nodes’ as ‘ShoppingCartElements’ which implement an interface ensuring that they could accept my visitor interface, which I named ‘ShoppingCartVisitor’. The concrete classes call their associated method of the visitor, passing in a reference to themselves. The ‘PricingVisitor’ calls the back end (read sleeps for 3 seconds, read slow back end) when dealing with a biddable item and just returns the price for a voucher.
public class PricingVisitor implements ShoppingCartVisitor {
public int price(BiddableItemElement biddableItemElement) {
//Call pricing engine to get a counter offer, and accept or a reject
ThreadUtils.simulateNetworkCall();
return biddableItemElement.getOfferPrice() + 100;
}
public int price(VoucherElement voucherElement) {
return voucherElement.getPrice();
}
}
The client simply makes a list of ShoppingCartElements, three normal items and one voucher, and loops over them passing the concrete visitor into each. Using my classic/legacy method of timing and logging things, the finished code tells me ‘That took 9002ms’.
Adding ParallelStream
So, the goal we are truly after here is to price each of my items in parallel, AKA in separate threads. Hey look this is exactly what ParallelStreams does. The structure of the design pattern does not need to change, just the loop that iterates over our ShoppingCartElements. The syntax for streams is a bit alien the first time you look at it, but you grow used to it. The biggest difference is you can’t keep a running total as a variable declared outside the stream, like you would with a traditional loop. Instead you need a collector. Here’s my annotated solution:
int total = shoppingCart.parallelStream() // Sets up the parallel stream
.map(element -> element.accept(visitor)) // Passes the visitor to each element
.collect(Collectors.summingInt(Integer::intValue)); // Adds all the results together
Adding Java 12 Tees
And now the code tells me ‘That took 3033ms’. Yet again we succeed! There are alternative ways to achieve this, such as with the mapToInt and sum methods, which would be worth researching if our goal was deeper than creating a proof of concept.
At this point I think to myself, that was easy, and I imagine myself marching into the boss’s office to declare that I have solved the website’s shopping cart issue, and expecting champagne. However, my imagination has a lot of real world experience, and the boss says ‘great, but now we also want the price of the cheapest item in the cart’. My heart sinks, I’ve just learned about streams and I have no idea how to do that… Time to Google like mad. Well it turns out if we are using Java 12 or higher, we can combine our summingInt collector with a minBy collector, using a teeing collector, and store them in a cheap and cheerful object to be returned by the stream at no extra cost (the code says 3026ms… Which is actually a saving. Count that as optimisations in the JVM between 8 and 12). Again, here’s the annotated code:
CartTotalResponse response = shoppingCart.parallelStream()
.map(element -> element.accept(visitor))
.collect(Collectors.teeing( // Split to use 2 collectors
Collectors.summingInt(Integer::intValue), // Get The Total
Collectors.minBy(Integer::compareTo), // Get the smallest
(sum, min) ->
new CartTotalResponse(sum, min.orElse(0)))); // Create the response
Findings
Our ParallelStream made short work of the network latency problem, and has obvious application anywhere multithreading does. Streams in general have their own ups and downs, such as their speed versus a standard loop, but there are plenty of articles on these topics. Suffice to say you should still be defaulting to a for loop, if there is not an advantage to be gained from streaming. I’d also mention how long and messy our stream code might grow in a real world application where more and more requirements are added over time.
Well that’s all I’ve got for today. I should really thank the patterns people from the Codurance Meetups, for inspiring this series. Next time I’m planning to throw off the shackles of design patterns and have a go at using Java 8 functionality to race sorting algorithms. If that doesn’t excite you then you are probably perfectly normal. See you there!