There are several options you can go for. One option is a continuation passing style.
This doesn't look all that good in Java, but you can do something similar.
// This is pseudo code, intended to illustrate the concept.
cpsMethod(Arg... args, ClosureOverFunctionSoItIsNullary continuation) {
// do stuff
continuation.call();
}
So basically, the method gets what's supposed to happen next passed into it. There are some downsides to this approach in Java, namely that you don't have tail-call optimization, so you can get a stack overflow, and perhaps more importantly, it looks very different from normal Java.
// Illustrative pseudo code
return doSomething(() -> doSomethingElse(() -> doAgainSomethingElse(param1, param2, () -> doSomethingWhichDoesntReturn())));
This removes the ifs, or rather, put the test inside every method, which now has to decide if it's going to continue, or if it's going to just return Status.ABORTED.
You could of course make this thing prettier by putting the handling outside and just take the methods as producers, give in a Predicate/hardcode the test, and just offer varargs:
private continuationPasser(Supplier<Status> first, Supplier<Status>... rest) {
Objects.requireNonNull(first);
Status status = first.get();
for(Supplier<T> continuation : methods) {
status = continuation.get();
if(status == Status.ABORTED) {
return status;
}
}
}
Dirt simple code, does exactly what you'd expect, and now your call on top will go from:
public Status execute() {
Status status = doSomething();
if (status != Status.ABORTED) {
status = doSomethingElse();
}
if (status != Status.ABORTED) {
status = doAgainSomethingElse(param1, param2);
}
if (status != Status.ABORTED) {
doSomethingWhichDoesntReturn(param3);
}
//etc.
return status;
}
To something like:
public Status execute() {
return continuationPasser(
this::doSomething,
this::doSomethingElse,
() -> doAgainSomethingElse(arg1, arg2);
() -> doSomethingWhichDoesntReturn(arg3));
Except for, you know, the last one doesn't return anything.
If it's trivial to make it return something, then you could just do that. If that's not trivial, you can just change the type from a Supplier to a Function<Status, T>, and you can pass in the last status if you want.
But that's an option. Take a functional idea and make it work. This has the benefit of being very clear if you know what continuation passing is. You could generalize the idea to take in a predicate too, if you'd like. Another way to do this would be to change the continuationPasser a bit, to make it pass in the previous result, and let the methods themselves decide what they want to do.
Then continutationPasser can look like this:
continuationPasser(Function<Status, Status> first, Function<Status, Status>... rest) {
Objects.requireNonNull(first);
Status status = first.apply(Status.SOME_REASONABLE_VALUE_LIKE_NOT_STARTED);
// You could use some reduce function here if you want to.
// The choice of a loop here is just my personal preference.
for(Function<Status, Status> fun : rest) {
status = rest.apply(status);
}
return status;
}
This makes the continuation passer even simple.
You start off by applying the first function, to get a starting value. Then you just for-each over the rest of them. And they can just start with checking for the ABORTED status and exit early. You'll still have the ifs, but your main running code will look positively neat now.
You can always wrap your methods in something like:
Function<Status, Status> runIfNotAborted(Supplier<Status> supplier) {
return (Status s) -> s == ABORTED? ABORTED : supplier.get();
}
Function<Status, Status> returnPreviousStatus(Runnable code) {
return (s) -> {
code.run();
return s;
}
}
And now you don't even have to change your methods. (But if you were to do this style that might be a better option if it was available.)
public Status execute() {
return continuationPasser(
runIfNotAborted(this::doSomething),
runIfNotAborted(this::doSomethingElse),
runIfNotAborted(() -> doAgainSomethingElse(arg1, arg2)),
runIfNotAborted(returnPreviousStatus(() -> doSomethingWhichDoesntReturn(arg3)));
And now it's quite clear what's going on. We're building functions on top of functions, in what looks a bit like a functional decorator-pattern.
This is a very general idea, and you can do this more specialized or generalize it more if you want to. But be careful or you'll write a framework to not have to write an if/else. Jenkins uses this idea for its pipelines, but has a bit more stuff in it to pass along the environment as well for example.