Streamlining Conditionals:
Harnessing Data
Fri May 26 2023
The conventional method of representing a branching for deriving values or executing certain logics is by using an if-else
statements to determine when to do what.
However, if-else
statements can quickly build up to ladders and create code that is very hard to understand and maintain.
This blog is to share an approach that we came up with a systematic approach to avoid if-else
else.
Avoiding if-else ladders are not a new thought, for example, the book "Clean Code" talks about avoiding if else using polymorphism. But that solution is not practical in all the use cases, and we might end up in abusing polymorphism with that approach.
The approach that we came up with is also not a silver-bullet, but a practical data-driven approach which can be used when you can express your logic as tables.
Whatever the conditional logic that you have start by expressing the logic in tabular form.
For example,
The statement,
Can be expressed as data-table like,
Signal Color | Action to be taken |
---|---|
Red | Stop |
Yellow | Prepare to stop |
Green | Go |
Now that we have defined our conditionals in tabular format, lets see how can we express it as code.
When you have a 2-column table, the simple way to encode the data would be a Map
.
For example, the above signal table can be expressed as map like this,
var signalColorActions = Map.of( "Red", "Stop", "Yellow", "Prepare to stop", "Green", "Go" ); // And using it would be, signalColorActions.getOrDefault(signalColor, "Unknown signal color");
Which would be better than the equivalent counterpart with if-else ladder,
if ("Red".equals(signalColor)) { return "Stop"; } else if ("Yellow".equals(signalColor)) { return "Prepare to stop"; } else if ("Green".equals(signalColor)) { return "Go"; } else { return "Unknown signal color"; }
If your conditional table is not straightforward key-value mapping, you would still achieve the same result with the strategy of encoding
data table as Map
with the key as Predicate
, where the evaluation is about iterating through each key set and
returning/evaluating the value when the corresponding key is evaluated to true.
For example,
For the data table,
Percentage (%) | Grade |
---|---|
90-100 | A |
80-89 | B |
70-79 | C |
60-69 | D |
Below 60 | F |
The corresponding Map
can be,
Map<Predicate<Float>, String> grades = new LinkedHashMap<>(); grades.put(score -> score >= 90, "A"); grades.put(score -> score >= 80, "B"); grades.put(score -> score >= 70, "C"); grades.put(score -> score >= 60, "D"); grades.put(score -> score < 60, "F");
And the code to figure out the grade can be,
grades.entrySet().stream() .filter(entry -> entry.getKey().test(score)) .map(Map.Entry::getValue) .findFirst() .orElse("Invalid score");
Note: If the order of the evaluation of the predicates matters, you can go ahead with LinkedHashMap.
Everything is fine until now when you have a 2-column table representation of your conditional logic. It would soon be problematic when you have multiple conditions to check for. In such cases, with Map
approach, you might have to create nested maps, which would create complexity and defeats the purpose of the effort that we're working towards.
In such situations in which the Map
approach is not enough, see if you can encode your tabular representation into bit more sophisticated data (for ex, as JSON).
For example, The logic,
if condition1 and condition2 then return value1 if condition1 and condition3 then return value2 if condition1 and condition3 and condition4 then return value3 if condition2 then return value4
Can be expressed in tabular format as,
Condition 1 | Condition 2 | Condition 3 | Condition 4 | Return Value |
---|---|---|---|---|
True | True | Don't Care | Don't Care | Value 1 |
True | False | True | True | Value 3 |
True | False | True | False | Value 2 |
False | True | Don't Care | Don't Care | Value 4 |
Don't Care | False | False | Don't Care | No conditions met |
And when you need to encode it in code, you can express it in JSON as,
{ "value1" : { "condition1" : true, "condition2" : true }, "value2" : { "condition1" : true, "condition3" : true }, "value3" : { "condition1" : true, "condition3" : true, "condition4" : true }, "value4" :{ "*" : true } }
And the equivalent code that evaluates can be,
var conditions = loadJson(); for (Map.Entry<String, Map<String, Boolean>> entry : conditions.entrySet()) { String value = entry.getKey(); Map<String, Boolean> conditionMap = entry.getValue(); if (allConditionsMatch(conditionMap, condition1, condition2, condition3, condition4)) { return value; } }
Note: Instead of imperative way of evaluating the JSON, you can also leverage on libraries that supports JQ so that the code that evaluates can also be made declarative.
If you're a situation where you find yourself in a position that you're unable to express the conditionals in a consistent way within the JSON, you can see if you turn to small DSL utilities like j-s-exp.
A more comprehensive example for this could be found in our earlier blog post.
You can turn to formal rule-engine libraries that implements Decision Tables, which would be a straightforward mapping from data representation that you have done in step#1.
For example, libraries like Drools have features out-of-the-box that allows you to define and use Decision Tables from excel and use it.
Separation of concerns: The "What" (business logic expressed as Map/JSON) and "How" (code leveraging the data) part of the code is separated out. So that all your business logic is encoded in code without other logics, and there would be a one-to-one mapping between your data representation and business spec.
Flexibility: You can add, modify, or remove conditions without changing the code. This allows for easier customization and adaptation to evolving requirements.
Testability: Since the conditions and actions are externalized, you can create targeted test cases, ensuring comprehensive test coverage without the complexity of testing nested conditions.
Readability and Maintainable Code: Since the "What" and "How" parts are separated, when there is a change in business logic just change the data. When you want to optimize or bug fix, it would mostly be touching code that leverages the data representation.
Overall, this thought process helped us to write much cleaner and maintainable code. If you have similar thought process and stuck on simplifying your code, do talk to us. More than happy to discuss and help.
We would love to hear from you! Reach us @
info@techconative.com