Often a complex, multi-step calculation requires your code to react to many circumstances. Typically you use exception handling to separate the normal control flow from error handling:
Option<BigDecimal> result = Option.none(); try { result = compute(); } catch (WhateverException e) { // Perhaps rethrow. } if (result.isEmpty()) { // Perhaps retry, or set a dummy value, or some other fancy stuff. }
Here we’re using an Option
to encapsulate an optional value.
Exception handling code like this not only gets complicated but becomes really hard to read. Imagine a three or four (or more!) step process where each step can fail:
Step1 step1 = null; try { step1 = step1(); } catch (Step1Exception e) { // do stuff, or maybe not } Step2 step2 = null; try { step2 = step2(step1); // needs step1 } catch (Step2Exception e) { // do stuff, or maybe not } // more steps
Now imagine that some of these operations may throw a RuntimeException
, which you need to handle because sub-step 4a needs to, and you can see the syntactical (is that a word?) pain piling up. You can’t escape the pain, but you can manage it a bit.
Let’s introduce a new type, Computation
, that makes code like this more fluid, type-safe, and readable. We’ll rewrite the first example to rethrow the exception if we got one, and supply a dummy value if there’s no actual result:
Computation<BigDecimal> computation = ... // described below BigDecimal result = computation.throwIfFailure().getOrElse(BigDecimal.ZERO);
Here’s the signature for a Computation
:
public abstract class Computation<T> implements Iterable<T> { // Boring methods: public abstract boolean isFailure(); public abstract boolean isSuccess(); public abstract Exception getFailure(); // Null if success (maybe make this return Option<Exception>?) // Sweet, fluid methods: public abstract Option<T> toOption(); // Option.none() if a failure public abstract Option<T> throwIfFailure() throws Exception; // Like toOption(), but throws if a failure }
Just like Option
, Computation
has two concrete subtypes: Result
and Failure
. The implementation of the abstract Computation
methods in the subtypes is straightforward. (Bonus: Why is Computation
an Iterable
? So you can use a nice Java syntax hack and put your Computation
in a for
loop which will only run the block if the Computation
is a Result
.)
Some nice factory methods for Computation
do the dirty work of getting rid of the try/catch blocks in your code:
// Gives you a Result if no Exception, Failure otherwise Computation<Integer> computation = Computation.compute(new Callable<Integer>() { @Override public Integer call() throws Exception { // do stuff } }); // Adapt a Future<T> into a Computation<T>. ExecutorService executor = ... Computation<Integer> async = Computation.compute(executor.submit(...));
After getting your code into shape like this, you can next separate your computations into clean, isolated, composable Callable
implementations which make testing much simpler.
Note that Computation
is just like a Future
but presents an alternative, perhaps smoother interface. Computation
is also very similar to Scalaz’s Validation
and Lift’s Box
, but without monadic niceness of Scala.