Yet another way to deal with nested if/else conditions
Today I’ll show you how I used the Fluent Interface pattern to eliminate the verbosity of the nested if/else statements when I needed to validate some conditions before actually creating the object.
You can learn more about Fluent Interface from these links:
- https://java-design-patterns.com/patterns/fluentinterface/
- https://belief-driven-design.com/fluent-interfaces-b11b901f1eb/
Beginning of the story
In almost every project we face such situations that we need to validate multiple logical conditions with “if/else”s before calling the actual business method. As you would also agree, we may end up with a lot of nested if/else cases that make the code harder to read and maintain and debug and, and…
Fortunately, there are tons of suggestions on the internet to work around this issue, like we can bubble up those conditions to the top of the actual business code (guard pattern):
if (!someCondition){
// log some info
return;
}
if (!anotherReasonableCondition){
// log some info
return;
}
// run the business code
Moreover, we can play with some AOP to even externalize this validation part from the code.
But in addition to all of those fancy things, I’m going to tell you one more way that you can consider on your next project (don’t refactor the current one 😛). Before proceeding, I want also to note that I changed the real code from production for brevity and will continue over an “apple picking” example.
Initial Version
Initially, we were just “picking an apple” without any edge cases and life was easier to live…
Apple apple = pickTheApple();
if (apple != null) {
appleBucket.add(apple);
}
So, to make a long story short, one day I woke up and realized that we are already doing a lot of validations before “picking the apple” and at that time our code looked like this:
if (hasApplesOnTheTree) {
if (!applesAreGreen) {
if (hasAccessToPickTheApples) {
Apple apple = pickTheApple();
if (apple != null) {
appleBucket.add(apple);
}
} else {
log.error("You don't have access to pick the apples.");
}
} else {
log.error("Apples aren't ripe yet.");
}
} else {
log.error("There aren't any apples on the tree.");
}
In addition to that, we were not just picking one apple, there were a lot of apples to pick and for each of them, I had to implement nearly same amount of steps: validate the conditions and pick the apple, validate different conditions and pick another apple and so and on…
And initially, I just imagined a solution that would look way better, I mean more compact and…you know..more like “functional style”:
new ConditionalApplePicker()
.assertTrue(hasApplesOnTheTree)
.assertFalse(applesAreGreen)
.assertTrue(hasAccessToPickTheApples)
.execute(() -> pickTheApple())
.ifPresent(appleBucket::add);
Implementation
So, as you may have guessed, we’re going to create a builder-like ConditionalApplePicker class and add our assertion methods which will validate the given parameters:
public class ConditionalApplePicker {
private boolean conditionsAreMet = true;
public ConditionalApplePicker assertTrue(boolean assertion) {
if (!assertion) {
this.conditionsAreMet = false;
}
return this;
}
public ConditionalApplePicker assertFalse(boolean assertion) {
if (assertion) {
this.conditionsAreMet = false;
}
return this;
}
public Optional<Apple> execute(Supplier<Apple> supplier) {
if (!conditionsAreMet) {
return Optional.empty();
}
return Optional.ofNullable(supplier.get());
}
}
And of course, the execute method at the end, instead of the build. Well, it’s more like executes the given Supplier instead of just creating an object.
And that’s it. Simple and straightforward.
But wait!
What about the “else” cases? So yes, we can define an additional behavior when one of the conditions is not met, just like the “else” cases. And after refactoring a bit, the improved version might look like this:
public class ConditionalApplePicker {
private boolean conditionsAreMet = true;
private Runnable elseCaseRunnable;
public ConditionalApplePicker assertTrue(boolean assertion,
Runnable elseCaseRunnable) {
if (!assertion && conditionsAreMet) {
this.conditionsAreMet = false;
this.elseCaseRunnable = elseCaseRunnable;
}
return this;
}
public ConditionalApplePicker assertFalse(boolean assertion,
Runnable elseCaseRunnable) {
if (assertion && conditionsAreMet) {
this.conditionsAreMet = false;
this.elseCaseRunnable = elseCaseRunnable;
}
return this;
}
public Optional<Apple> execute(Supplier<Apple> supplier) {
if (!conditionsAreMet) {
if (elseCaseRunnable != null) {
elseCaseRunnable.run();
}
return Optional.empty();
}
return Optional.ofNullable(supplier.get());
}
}
As you can observe, we added Runnable as a second parameter to the assert methods. I chose Runnable because in my case I don’t need to pass or return anything, just to log some information, but you’re free to play on it.
So, with those changes the code will look like this:
new ConditionalApplePicker()
.assertTrue(hasApplesOnTheTree, () -> log.error("There aren't any apples on the tree."))
.assertFalse(applesAreGreen, () -> log.error("Apples aren't ripe yet."))
.assertTrue(hasAccessToPickTheApples, () -> log.error("You don't have access to pick the apples."))
.execute(() -> pickTheApple())
.ifPresent(appleBucket::add);
Conclusion
So, for my situation, this workaround saved me from a lot of lines of the if/else validations and I ended up with more compact code. In fact, the production code was, indeed, more complex than what I showed you today, but I tried to give you a starting point so that you can extend it and make it fit your cases.
And if this article could ring a bell for you…let’s clap together 👏