Skip to content

Avoid using If Else statement using Design Patterns

Introduction#

This articles will show you a way to avoid writing if-else branches in Java by introducing an elegant solution with the Strategy Design Pattern.

Example#

Consider a scenario where a system needs to calculate a path from point a to point b using a Path finding algorithm. Considering the number of path find algorithms available, this quick can become an issue in the number of if-else statements we have to write. Example:

package com.javadesignpatterns.blog.avoidifElse;

import java.util.List;

public class PathFinder {

    public List<String> findPath(String algorithm, String start, String end) { //(1)!
        if (algorithm.equals("BFS")) { //(2)!
            return bfs(start, end);
        } else if (algorithm.equals("DFS")) {
            return dfs(start, end);
        } else if (algorithm.equals("Dijkstra")) {
            return dijkstra(start, end);
        } else if (algorithm.equals("A*")) {
            return aStar(start, end);
        } else {
            throw new IllegalArgumentException("Unknown algorithm: " + algorithm);
        }
    }

    private List<String> bfs(String start, String end) { //(3)!
        // Implement BFS algorithm
        System.out.println("Executing BFS...");
        return List.of("Start -> ... -> End (BFS Path)");
    }

    private List<String> dfs(String start, String end) {
        // Implement DFS algorithm
        System.out.println("Executing DFS...");
        return List.of("Start -> ... -> End (DFS Path)");
    }

    private List<String> dijkstra(String start, String end) {
        // Implement Dijkstra's algorithm
        System.out.println("Executing Dijkstra...");
        return List.of("Start -> ... -> End (Dijkstra Path)");
    }

    private List<String> aStar(String start, String end) {
        // Implement A* algorithm
        System.out.println("Executing A*...");
        return List.of("Start -> ... -> End (A* Path)");
    }

    public static void main(String[] args) {
        PathFinder pathFinder = new PathFinder();

        List<String> path1 = pathFinder.findPath("BFS", "A", "B");
        System.out.println("Path found using BFS: " + path1);

        List<String> path2 = pathFinder.findPath("DFS", "A", "B");
        System.out.println("Path found using DFS: " + path2);

        List<String> path3 = pathFinder.findPath("Dijkstra", "A", "B");
        System.out.println("Path found using Dijkstra: " + path3);

        List<String> path4 = pathFinder.findPath("A*", "A", "B");
        System.out.println("Path found using A*: " + path4);
    }

}
  1. The PathFinder has one method to calculate a path based on an algorith and the inputs required for that algorithm
  2. For each algorithm we must add an if-else statement
  3. Each implementation of the algorithm has its own method

The output looks like this:#

Executing BFS...
Path found using BFS: [Start -> ... -> End (BFS Path)]
Executing DFS...
Path found using DFS: [Start -> ... -> End (DFS Path)]
Executing Dijkstra...
Path found using Dijkstra: [Start -> ... -> End (Dijkstra Path)]
Executing A*...
Path found using A*: [Start -> ... -> End (A* Path)]

Issues#

  • Each algorithm requires a new if-else statement, which is not ideal if you plan to support a wide range of algorithms
  • All the algorithms are part of the PathFinder class, which means its harder to identify the logic linked to one algorithm

If you want to improve you Java skills:

Visit the Javanauts community

Solution With Design Patterns#

What are Design Patterns?

Design patterns are reusable solutions to common programming problems. They provide a structured approach to solving problems and promote code reuse, flexibility, and maintainability. Java Design Patterns offer a set of best practices for designing and implementing software applications. They provide a framework for developers to create code that is easy to understand, maintain, and extend.

One of the most common Java Design Patterns used to replace if-else statements is the Strategy Pattern. The Strategy Pattern allows developers to define a family of algorithms, encapsulate each one, and make them interchangeable. This pattern promotes code reuse, flexibility, and maintainability. By using the Strategy Pattern, developers can easily add new algorithms without modifying existing code. This approach reduces code complexity and improves the overall quality of the software.

ℹ️ You can find all design pattern described in our catalog.

Strategy Pattern to Replace Conditional Logic#

Defining Strategy Pattern#

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from clients that use it. The Strategy Pattern involves two main entities: the Context and the Strategy. The Context is responsible for executing the algorithm, while the Strategy represents the algorithm itself.

Applying Strategy Pattern#

The Strategy Pattern can be used to replace conditional logic in situations where the behavior of an object needs to change dynamically at runtime. By encapsulating the behavior in a separate Strategy object, the Context can switch between different strategies without having to modify its code. This makes the code more modular and easier to maintain.

Strategy Pattern Example#

A better approach would be to use the Strategy Pattern to encapsulate the pricing logic for each product type in a separate Strategy object. The Context object would then be responsible for selecting the appropriate Strategy object based on the product type and invoking its pricing method.

Here is an example implementation of the Strategy Pattern for the product pricing scenario:

package com.javadesignpatterns.blog.avoidifElse.solution;


import java.util.List;

interface PathFindingStrategy { //(1)!
    List<String> findPath(String start, String end);
}

class BFSPathFindingStrategy implements PathFindingStrategy { //(2)!
    public List<String> findPath(String start, String end) {
        // Implement BFS algorithm
        System.out.println("Executing BFS...");
        return List.of("Start -> ... -> End (BFS Path)");
    }
}

class DFSPathFindingStrategy implements PathFindingStrategy {
    public List<String> findPath(String start, String end) {
        // Implement DFS algorithm
        System.out.println("Executing DFS...");
        return List.of("Start -> ... -> End (DFS Path)");
    }
}

class DijkstraPathFindingStrategy implements PathFindingStrategy {
    public List<String> findPath(String start, String end) {
        // Implement Dijkstra's algorithm
        System.out.println("Executing Dijkstra...");
        return List.of("Start -> ... -> End (Dijkstra Path)");
    }
}

class AStarPathFindingStrategy implements PathFindingStrategy {
    public List<String> findPath(String start, String end) {
        // Implement A* algorithm
        System.out.println("Executing A*...");
        return List.of("Start -> ... -> End (A* Path)");
    }
}

public class PathFinder {

    private PathFindingStrategy strategy; //(3)!

    public void setPathFindingStrategy(PathFindingStrategy strategy) { //(4)!
        this.strategy = strategy;
    }

    public List<String> findPath(String start, String end) { //(5)!
        return strategy.findPath(start, end);
    }

    public static void main(String[] args) {
        PathFinder pathFinder = new PathFinder();

        pathFinder.setPathFindingStrategy(new BFSPathFindingStrategy());
        System.out.println("Path found using BFS: " + pathFinder.findPath("A", "B"));

        pathFinder.setPathFindingStrategy(new DFSPathFindingStrategy());
        System.out.println("Path found using DFS: " + pathFinder.findPath("A", "B"));

        pathFinder.setPathFindingStrategy(new DijkstraPathFindingStrategy());
        System.out.println("Path found using Dijkstra: " + pathFinder.findPath("A", "B"));

        pathFinder.setPathFindingStrategy(new AStarPathFindingStrategy());
        System.out.println("Path found using A*: " + pathFinder.findPath("A", "B"));
    }

}
  1. Create a new interface to encapsulate the path finding algorithm
  2. Create new implementations for each algorithm
  3. Store the strategy inside the PathFinder
  4. Allow an each "change" of algorithm
  5. The PathFinder still exposes the find path method, but delegates to the implementation algorithm

In this example, the PathFindingStrategy interface defines the algorithm behavior, and the BFSPathFindingStrategy and DijkstraPathFindingStrategy classes implement the behavior for each specific algorithm, respectively. The PathFinder class uses the PathFindingStrategy interface to calculate the path based on the specific algorithm. This approach allows for easy addition of new algorithm strategies without having to modify existing code.

Overall, the Strategy Pattern provides a flexible and modular approach to replacing conditional logic in Java programs. By encapsulating behavior in separate Strategy objects, the code becomes easier to maintain and modify, and can adapt to changing requirements at runtime.

Using an Enum to replace IF-Else blocks#

In the example above, we still need to set the specific PathFinderStrategy before calling the findPath method. A more elegant solution involves creating an Enum listing all the available PathFinding algorithms together with their strategy:

package com.javadesignpatterns.blog.avoidifElse.solution.factory;

public enum PathFindingStrategyType {

    A_STAR(new AStarPathFindingStrategy()), //(1)!
    BFS(new BFSPathFindingStrategy()),
    DFS(new DFSPathFindingStrategy()),
    DIJKSTRA(new DijkstraPathFindingStrategy());

    private PathFindingStrategy strategy; //(2)!

    PathFindingStrategyType(PathFindingStrategy strategy) { //(3)!
        this.strategy = strategy;
    }

    public PathFindingStrategy getStrategy() {
        return strategy;
    }
}
  1. List all the algorithms and pass the Strategy in the constructor.
  2. Keep the strategy as an internal reference linked to the enum.
  3. The enum constructor now forces a strategy

The PathFinder implementation becomes a bit more elegant by passing the desired algorithm type as parameter of the findPath method:

package com.javadesignpatterns.blog.avoidifElse.solution.factory;

import java.util.List;

public class PathFinder {

    public List<String> findPath(PathFindingStrategyType type, String start, String end) { //(5)!
        return type.getStrategy().findPath(start, end); //(1)!
    }

    public static void main(String[] args) {
        PathFinder pathFinder = new PathFinder();

        System.out.println("Path found using BFS: " + pathFinder.findPath(PathFindingStrategyType.BFS, "A", "B"));
        System.out.println("Path found using DFS: " + pathFinder.findPath(PathFindingStrategyType.DFS, "A", "B"));
        System.out.println("Path found using Dijkstra: " + pathFinder.findPath(PathFindingStrategyType.DIJKSTRA, "A", "B"));
        System.out.println("Path found using A*: " + pathFinder.findPath(PathFindingStrategyType.A_STAR, "A", "B"));
    }

}
  1. Ask the Enum for its strategy and execute it

The resulting PathFinder still works as expected:

Executing BFS...
Path found using BFS: [Start -> ... -> End (BFS Path)]
Executing DFS...
Path found using DFS: [Start -> ... -> End (DFS Path)]
Executing Dijkstra...
Path found using Dijkstra: [Start -> ... -> End (Dijkstra Path)]
Executing A*...
Path found using A*: [Start -> ... -> End (A* Path)]

Conclusion#

In conclusion, there are various design patterns that can be used to replace if-else statements in Java. The implementation of design patterns can help improve the readability, maintainability, and scalability of code.

One such pattern is the Strategy pattern, which allows for dynamic selection of algorithms at runtime. This pattern can be useful when there are multiple algorithms that can be used to solve a problem and the selection of the algorithm depends on the context.

Another pattern is the Factory pattern, which provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. This pattern can be useful when there is a need to create objects based on a specific set of conditions.

Using a Map can also be a good alternative to if-else statements, as it allows for easy mapping of keys to values. This can be useful when there is a need to perform different operations based on specific inputs.

Finally, it is important to handle exceptions properly when replacing if-else statements with design patterns. Exceptions should be thrown and handled appropriately to ensure that the program runs smoothly and errors are handled gracefully.

Overall, it is important to choose the right design pattern based on the specific requirements of the program. By doing so, developers can write cleaner, more efficient, and more maintainable code.