Conditional Chain Pattern

Level: Intermediate.
Language: Java.

Recently, I had to refactor a piece of code in the project I’m working on, which involved replacing a block similar to the following pseudocode:

void aFunctionalBlock(Params params) {
    if (conditionEvaluable()) {
        // do something here;
        return;
    }
    if (anotherConditionEvaluable()) {
        // do something here;
        return;
    }
    if (anotherConditionEvaluable()) {
        // do something here;
        return;
    }
    // default behavior;
}

Although this structure is readable, imagine it extended with around 50 more lines, it wouldn’t just be less readable, but maintaining it could also become an issue.

Note that using a switch/case structure isn’t an option here because each case is evaluated with a specific function.

This is where the Conditional Chain pattern comes into play, which can be considered part of the family of patterns such as the Strategy pattern and Chain-of-Responsibility pattern.

Before diving in, let’s take a first step towards the solution, as follows

void aFunctionalBlock(Params params) {
    if (conditionEvaluableA()) {
        functionA();
        return; // not ugly, right?
    }
    if (conditionEvaluableB()) {
        functionB();
        return;
    }
    if (conditionEvaluableC()) {
        functionC();
        return;
    }
    defaultBehavior();
}
void functionA() {
    // do something here;
}
void functionB() {
    // do something here;
}
void functionC() {
    // do something here;
}
void defaultBehavior() {
    // do something here;
}

Now, we need to replace this with a mechanism that allows chaining, isolating, and ensuring a single execution in a more semantic way. For this, I'll use a custom implementation that I'll provide at the end of this article.

Whether.is(conditionEvaluableA()).then(functionA())
        .inCase(conditionEvaluableB()).then(functionB())
        .inCase(conditionEvaluableC()).then(functionC())
        .otherwise(defaultBehavior());

This block not only replaces a switch-case structure in a more semantic way but also ensures a single execution, similar to the switch/case block, while avoiding the need for the developer to write it imperatively.

How Does It Work?

Basically, an object is initiated by a class method, in this case is creating an immutable object.

public static Whether is(boolean condition) {
    return new Whether(condition);
}

Then, each operator (and, or, inCase) evaluates its current condition along with that of its predecessor. If the evaluation of the new state is different, a new object is created to delegate handling of this new chained state. The basic form of the statement follows this pattern:

Chain.operator().then()

Let’s take the and operator as an example:

public Whether and(boolean condition) {
    if (parentCondition && condition) {
        return new Whether(true);
    }
    return this;
}

As shown, this operator evaluates its current condition and that of its predecessor. If the evaluation is positive, a new immutable object is created, and the new state is chained.

Finally, it’s time to decide if it’s possible to handle the new state, which in this example are then and otherwise.

Let's use then as an example:

public Whether then(Runnable action) {
    if (parentCondition && !alreadyRun) {
        action.run();
        return new Whether(true, true);
    }
    return this;
}

As you can see, this method, responsible for executing the strategy, first checks if the chained condition is valid, then returns a new immutable object, changing the state of alreadyRun to prevent future executions.

Unit Test Examples

Here are some examples of unit tests demonstrating the behavior of this utility:

@Test
void switchCase2() {
    final Runnable runnable = mock(Runnable.class);
    doNothing().when(runnable).run();

    Whether.is(false).then(Assertions::fail)
            .inCase(true).then(runnable)
            .inCase(true).then(Assertions::fail)
            .inCase(true).then(Assertions::fail)
            .otherwise(Assertions::fail);

    verify(runnable, times(1)).run();
}

@Test
void conditionsJoinedByAndWithDefaultAction3() {
    final Runnable firstAction = mock(Runnable.class);
    doNothing().when(firstAction).run();
    final Runnable secondAction = mock(Runnable.class);
    doNothing().when(secondAction).run();

    Whether.is(true).then(firstAction)
            .and(false).then(Assertions::fail)
            .otherwise(Assertions::fail);

    verify(firstAction, times(1)).run();
}

For more information, you can check the next link.

Resources: