An overview of the

Ifelse is an important part of any programming language. But we wrote a lot of nested IF statements, which made our code more complex and difficult to maintain.

Next, let’s explore how to simplify the writing of ifelse statements in code.

A case study

We often encounter business logic that involves many conditions, and each logic needs to be handled differently. Take the Calculator class for example. We’ll have a method that takes two numbers and an operator as input and returns the result based on the operation:

public int calculate(int a, int b, String operator) {
    int result = Integer.MIN_VALUE;
 
    if ("add".equals(operator)) {
        result = a + b;
    } else if ("multiply".equals(operator)) {
        result = a * b;
    } else if ("divide".equals(operator)) {
        result = a / b;
    } else if ("subtract".equals(operator)) {
        result = a - b;
    }
    return result;
}
Copy the code

We can also implement this using the switch statement:

public int calculateUsingSwitch(int a, int b, String operator) {
    switch (operator) {
    case "add":
        result = a + b;
        break;
    // other cases    
    }
    return result;
}
Copy the code

In typical development, if statements can become larger and more complex. In addition, switch statements are not appropriate when there are complex conditions.

Another side effect of having nested decision structures is that they become unmanageable. For example, if we need to add a new operator, we must add a new if statement and implement the operation.

refactoring

We can use design patterns to achieve what we want.

The factory pattern

Many times, we encounter ifelse structures and end up performing similar operations in each branch. This provides the opportunity to extract factory methods that return objects of a given type and perform operations based on concrete object behavior.

For our example, let’s define an Operation interface with a single apply method:

public interface Operation {
    int apply(int a, int b);
}
Copy the code

This method takes two numbers as input and returns the result. Let’s define a class that performs the addition:

public class Addition implements Operation {
    @Override
    public int apply(int a, int b) {
        returna + b; }}Copy the code

We will now implement a factory class that returns an instance of Operation based on the given operator:

public class OperatorFactory {
    static Map<String, Operation> operationMap = new HashMap<>();
    static {
        operationMap.put("add", new Addition());
        operationMap.put("divide", new Division());
        // more operators
    }
 
    public static Optional<Operation> getOperation(String operator) {
        returnOptional.ofNullable(operationMap.get(operator)); }}Copy the code

Now, in the Calculator class, we can query the factory to get the operations and apply the source number:

public int calculateUsingFactory(int a, int b, String operator) {
    Operation targetOperation = OperatorFactory
      .getOperation(operator)
      .orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
    return targetOperation.apply(a, b);
}
Copy the code

In this example, we have seen how to delegate responsibility to a loosely coupled object provided by the factory class. But it is possible that nested if statements are simply moved to the factory class, which defeats our purpose.

Alternatively, we can maintain an object repository in the Map that can be queried for quick lookups. As we can see, OperatorFactor # operationMap serves our purpose. We can also initialize maps and configure them for lookup at run time.

Use enumerated

In addition to using maps, we can also use enUms to mark specific business logic. We can then use them in nested IF statements or switch case statements. Alternatively, we can use them as factories for objects and develop policies to execute the associated business logic.

This reduces the number of nested IF statements and delegates responsibility to a single Enum value.

Let’s see how we implement it. First, we need to define our enumeration:

public enum Operator {
    ADD, MULTIPLY, SUBTRACT, DIVIDE
}
Copy the code

As you can see, these values are labels for different operators that will be further used in calculations. We can always choose to use these values as different conditions in nested IF statements or switch cases, but let’s devise an alternative way of delegating logic to the Enum itself.

We will define methods and evaluate them for each Enum value. Such as:

ADD {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
},
// other operators
 
public abstract int apply(int a, int b);
Copy the code

Then, in the Calculator class, we can define a method to perform the operation:

public int calculate(int a, int b, Operator operator) { return operator.apply(a, b); }

We can now call this method by converting the String value to Operator using the Operator # valueOf() method:

@Test
public void test() {
    Calculator calculator = new Calculator();
    int result = calculator.calculate(3, 4, Operator.valueOf("ADD"));
    assertEquals(7, result);
}
Copy the code

Command mode

In the previous discussion, we saw an instance of a correct business object that uses a factory class to return a given operator. Later, the business object is used to perform calculations in the calculator.

We can also design a Calculator# calculate method to accept commands that can be executed on input. This would be another way to replace nested if statements.

Let’s start by defining our Command interface:

public interface Command {
    Integer execute();
}
Copy the code

Next, let’s implement an AddCommand:

public class AddCommand implements Command {
    // Instance variables
 
    public AddCommand(int a, int b) {
        this.a = a;
        this.b = b;
    }
 
    @Override
    public Integer execute() {
        returna + b; }}Copy the code

Finally, let’s introduce a new method in Calculator that accepts and executes Command:

public int calculate(Command command) {
    return command.execute();
}
Copy the code

Next, we can call the calculation by instantiating AddCommand and sending it to the Calculator# calculate method:

@Test
public void test() {
    Calculator calculator = new Calculator();
    int result = calculator.calculate(new AddCommand(3, 7));
    assertEquals(10, result);
}
Copy the code

The rule engine

When we ended up writing a lot of nested IF statements, each condition described a business rule that had to be evaluated to handle the correct logic. The rules engine captures this complexity from the main code. A RuleEngine evaluates rules and returns results based on input.

Let’s demonstrate an example by designing a simple RuleEngine that processes Expression through a set of rules and returns the result of the selected rule. First, we’ll define a Rule interface:

public interface Rule {
    boolean evaluate(Expression expression);
    Result getResult();
}
Copy the code

Second, let’s implement a RuleEngine:

public class RuleEngine {
    private static List<Rule> rules = new ArrayList<>();
 
    static {
        rules.add(new AddRule());
    }
 
    public Result process(Expression expression) {
        Rule rule = rules
          .stream()
          .filter(r -> r.evaluate(expression))
          .findFirst()
          .orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule"));
        returnrule.getResult(); }}Copy the code

The RuleEngine takes a presentation object and returns the result. Now, let’s design the Expression class as a set of operators containing two Integer objects that will be applied:

public class Expression {
    private Integer x;
    private Integer y;
    private Operator operator;        
}
Copy the code

Finally, let’s define a custom AddRule class that evaluates only when the ADD operation is specified:

public class AddRule implements Rule {
    @Override
    public boolean evaluate(Expression expression) {
        boolean evalResult = false;
        if (expression.getOperator() == Operator.ADD) {
            this.result = expression.getX() + expression.getY();
            evalResult = true;
        }
        return evalResult; }}Copy the code

We will now call RuleEngine using Expression:

@Test
public void test() {
    Expression expression = new Expression(5, 5, Operator.ADD);
    RuleEngine engine = new RuleEngine();
    Result result = engine.process(expression);
 
    assertNotNull(result);
    assertEquals(10, result.getValue());
}
Copy the code

conclusion

These design patterns can be used as an alternative to our ifELSE statement, depending on your actual business scenario.