The selection control mechanisms are present in almost all the modern-day programming languages. In selection control structures, the conditional statements like “if-else” and “switch” perform computations or actions based on whether the boolean condition they are operating on evaluates to true or false. It is quite obvious to understand and some of you can relate to the reason why most coders like to use a huge number of “if-else”, and “switch” statements and their nested versions in their code. The reason is – it is one of the first things that developers are taught and it is easy to implement them when there is a need to change the control flow of the program execution based on the value of the variable/expression. But this approach comes with a trade-off. For two-way or small multi-way code branching strategies, using if-else/switch is alright. But when the complexity is high, the number of conditions being evaluated is more, the business logic involves a lot of code or there are chances of adding more conditions/flows in the future, it is always better to come out of standard conditionals and implement a better design to handle such requirements.
While reading through a lot of code, I often observe that these conditional statements are unnecessarily added in many places where they are not required. In most of these cases, it results in a big, unmaintainable, difficult-to-refactor, unreadable chunk of code which will give nightmares to anyone reading, maintaining, or using it. The cyclomatic complexity of the source code also increases manifold if we keep on using these conditionals and their nested versions. More complexity means the possibility of introducing more bugs into the code.
So what are the different solutions or alternatives we can use to stay away from this problem?
Let us consider the below code where I have used Java as the programming language.
Here, the static method “calculate” takes 3 parameters (num1, num2, and operation). Based on the value passed to the string type “operation” variable, the expressions inside the if-else statements are evaluated and action is performed, before returning the result to the calling main function.
The equivalent “switch-case” code can be presented as:
Now, let’s discuss how this block can be refactored with the help of other programming language constructs and good design patterns.
Using Enums
The first alternative is by using “Enum” and it is probably the easiest one to implement. “Enum” or “enumeration” is a class type and is essentially a collection of named constants that define a new data type and its legal values. In the code below, as you can see, we have created an enum named “Operator” which has an abstract method named “calculate” (at the bottom). It also has the named constants (ADD, SUBTRACT, MULTIPLY and DIVIDE) which implement the abstract “calculate” method. This is required because if an enum class has an abstract method, then each instance (i.e. each constant) of the enum class must implement it. Using an abstract method is useful when we need a different implementation of a method for each instance/constant of an enum and thus it is being used here. The valueOf() method of enum “Operator” is called in the main method of the program to obtain an instance of the “Operator” enum for a given string value e.g. “Add”
Using the Factory method design pattern
The “Factory method design pattern” is a creational design pattern in which an interface is defined to create an object but the subclass will decide which class needs to be initiated. In the example below, we have created an interface “Operation” having the method “calculate” inside it. This interface is implemented by the individual classes (“Addition”, “Subtraction”, “Multiplication”, “Division”) each one of which has its implementation of the “calculate” method. A factory class “OperatorFactory” is also created which takes the responsibility of initializing the classes, with the help of a static map variable (“operator”) inside a static block. The “OperatorFactory” class provides the objects and hides the actual implementation details. From the main calling class, the created objects are accessed using the common “Operation” interface type returned by the “getOperation()” method of the “OperatorFactory” class.
Using the Command pattern
The “Command design pattern”, also sometimes referred to as the “Producer-Consumer pattern”, is a behavioral design pattern that helps us to achieve the decoupling between the sender/producer and the receiver/consumer. The main aim of this design pattern is to encapsulate all the data in an object for performing a given command/action. In the code below, you can see that we have created a “Command” interface for the classes like “Add”, “Subtract” etc. each of which provides the individual implementation of the “execute()” method.
Using the Rule Engine
In the Rule Engine concept, the rule engine evaluates the business rules for each logic to be processed and returns the results based on the input. In the code example below, we have defined a rule interface (“IRule”) with the two abstract methods – “evaluate” and “runRule”. The classes “AddRule”, “SubtractRule”, “MultiplyRule” and “DivideRule” implements the “evaluate()” and the “runRule()” method. The rule engine part in the main calling “Sample” class defines a list of rules whose elements are the objects of the “AddRule”, “SubtractRule”, “MultiplyRule” and “DivideRule” classes. While iterating through this list, the rules are evaluated and for different conditions, the rules are applied.