Since switches and if-statements are the most common control flow statements in many languages and are widely used throughout the code, removing the unnecessarily used ones will have a huge impact on the general readability of the code.
Use Null-Object Pattern
Always try not to return null references, instead practice the Null-Object Pattern and return Null-Objects that are neutral to operations and processes.
Suppose a module is responsible for specific calculations for a person’s properties on which taxes are paid.
The snippet below does the same using Null-Object Pattern. Notice how applying this pattern removes the necessity for excessive null checks in the second revision of the ‘PropertyTaxCalculator’.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
classPropertyTaxCalculator{
voidcalculate(String personId){
List<Property> properties =
getPerson(personId)
.getTaxPayments(2012)
.get(PROPERTY_TAX)
.getProperties();
for(Property property : properties) {
// do the main calculations here
}
}
}
Use Strategy Pattern
Strategy Pattern is another tool in the belt to get rid of conditional statements. The following pseudo-code demonstrates an example where an operation happens on an object based on a specific condition (the client ‘type’ in our example).
1
2
3
4
5
6
7
8
9
10
11
12
classClient{
Type type;
voidprocess(Object object){
if (type == TypeA) {
operationA(object);
} elseif (type == TypeB) {
operationB(object);
} elseif (type == TypeC) {
operationC(object);
}
}
}
Since the client has a certain type at each moment, we can move the logic in each leg of the conditionals to a different strategy implementation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
classClient{
IStrategy strategy;
voidprocess(Object object){
strategy.process(object);
}
}
interfaceIStrategy{
voidprocess(Object object);
}
classStrategyAimplementsIStrategy{
publicvoidprocess(Object object){
operationA(object);
}
}
classStrategyBimplementsIStrategy{
publicvoidprocess(Object object){
operationB(object);
}
}
classStrategyCimplementsIStrategy{
publicvoidprocess(Object object){
operationC(object);
}
}
// a newly added condition
classStrategyDimplementsIStrategy{
publicvoidprocess(Object object){
operationD(object);
}
}
It might look like using the strategy we ended up with twice as much code, however, the separation of concerns into different classes, encourages single responsibility and at the same time it makes them more readable and more testable. Besides, note that new conditions can be added without the need for modifying the client code.
Use Factory-Method Pattern
In case of encountering repeated conditionals that are used for object instantiation, the Factory-Method Pattern might be useful.
The pseudo-code below might look a bit long but it basically defines two behaviors each of which needs a ‘Product’ which is instantiated conditionally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
classClient{
Type type;
voidaction1(){
IProduct product = null;
if (type == TypeA) {
product = new ProductA();
} elseif (type == TypeB) {
product = new ProductB();
} elseif (type == TypeC) {
product = new ProductC();
}
operation1(product);
}
voidaction2(){
IProduct product = null;
if (type == TypeA) {
product = new ProductA();
} elseif (type == TypeB) {
product = new ProductB();
} elseif (type == TypeC) {
product = new ProductC();
}
operation2(product);
}
}
Such repeating conditional object instantiation is a sign of need for refactoring into Factory-Method Pattern as suggested below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
classClient{
Type type;
voidaction1(){
operation1(ProductFactory.create(type));
}
voidaction2(){
operation2(ProductFactory.create(type));
}
}
classProductFactory{
static IProduct create(Type type){
switch (type) {
case TypeA: returnnew ProductA();
case TypeB: returnnew ProductB();
case TypeC: returnnew ProductC();
default: returnnew NullProduct();
}
}
}
Conclusion
Although it might not be necessary nor feasible to get rid of many of the conditionals (such as when comparing primitive objects or enums), most unnecessary ones can be replaced by polymorphism and patterns which will improve code readability, testability and advocates separation of concerns.