DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Refactoring Complex Conditions: Clean Code Solutions for Nested If Statements

Nested if statements can make code cumbersome and challenging to read. In this article, we'll explore practical techniques to refactor nested if blocks for better readability, maintainability, and scalability. Along the way, you'll learn different approaches with clear, fully implemented examples.


Scenario: Nested If Statements

Imagine you have the following code to manage user access logic. At first glance, it seems simple enough:

if (user != null)
{
    if (user.IsActive)
    {
        if (user.Role == "Admin")
        {
            if (user.HasPermission("View"))
            {
                Console.WriteLine("Access granted");
            }
            else
            {
                Console.WriteLine("Permission denied");
            }
        }
        else
        {
            Console.WriteLine("Role not authorized");
        }
    }
    else
    {
        Console.WriteLine("User is not active");
    }
}
else
{
    Console.WriteLine("User not found");
}
Enter fullscreen mode Exit fullscreen mode

Why Refactor?

While the code above works, it suffers from several issues:

  1. Reduced Readability

    Deeply nested if statements make it difficult to follow the flow of logic at a glance.

  2. Increased Cognitive Overload

    Multiple layers of conditions require the developer to mentally track each layer, leading to confusion.

  3. Poor Maintainability

    Extending or modifying the logic becomes error-prone as more conditions are added.

  4. Testing Complexity

    Each nested branch requires extensive testing to cover all possible scenarios.

  5. Violation of Clean Code Principles

    Nested if blocks go against principles like early exit and single responsibility, which promote clear and concise logic.


Refactoring Options

We'll explore six methods to refactor nested if statements into cleaner, more maintainable code:

  1. Guard Clauses
  2. Switch Expressions
  3. Strategy Pattern
  4. Specification Pattern
  5. Polymorphism
  6. Using a Dictionary for Conditions

1. Refactoring with Guard Clauses

Guard clauses simplify logic by exiting early when a condition is not met, avoiding unnecessary nesting.

Example:

public class User
{
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public string Role { get; set; }
    public List<string> Permissions { get; set; } = new List<string>();

    public bool HasPermission(string permission) => Permissions.Contains(permission);
}

public class Program
{
    public static void Main()
    {
        User user = new User
        {
            Name = "John Doe",
            IsActive = true,
            Role = "Admin",
            Permissions = new List<string> { "View", "Edit" }
        };

        CheckAccess(user);
    }

    public static void CheckAccess(User user)
    {
        if (user == null)
        {
            Console.WriteLine("User not found");
            return;
        }

        if (!user.IsActive)
        {
            Console.WriteLine("User is not active");
            return;
        }

        if (user.Role != "Admin")
        {
            Console.WriteLine("Role not authorized");
            return;
        }

        if (!user.HasPermission("View"))
        {
            Console.WriteLine("Permission denied");
            return;
        }

        Console.WriteLine("Access granted");
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Refactoring with Switch Expressions

Switch expressions provide a concise way to handle multiple conditions.

Example:

public class User
{
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public string Role { get; set; }
    public List<string> Permissions { get; set; } = new List<string>();

    public bool HasPermission(string permission) => Permissions.Contains(permission);
}

public class Program
{
    public static void Main()
    {
        User user = new User
        {
            Name = "John Doe",
            IsActive = true,
            Role = "Admin",
            Permissions = new List<string> { "View" }
        };

        var result = (user != null, user?.IsActive, user?.Role, user?.HasPermission("View")) switch
        {
            (false, _, _, _) => "User not found",
            (true, false, _, _) => "User is not active",
            (true, true, "Admin", false) => "Permission denied",
            (true, true, "Admin", true) => "Access granted",
            _ => "Role not authorized"
        };

        Console.WriteLine(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Refactoring with Strategy Pattern

The Strategy Pattern encapsulates conditions into reusable classes, making the logic modular and testable.

Example:

(Implementation already provided in the previous article content)


4. Refactoring with Specification Pattern

Specifications represent conditions as standalone rules that can be combined and reused.

Example:

(Implementation already provided in the previous article content)


5. Refactoring with Polymorphism

Polymorphism lets you define different behaviors for different types of users, reducing conditionals entirely.

Example:

(Implementation already provided in the previous article content)


6. Refactoring with a Dictionary for Conditions

This approach uses a dictionary to map conditions to their corresponding outcomes, simplifying the logic further.

Example:

public class User
{
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public string Role { get; set; }
    public List<string> Permissions { get; set; } = new List<string>();

    public bool HasPermission(string permission) => Permissions.Contains(permission);
}

public class Program
{
    public static void Main()
    {
        User user = new User
        {
            Name = "John Doe",
            IsActive = true,
            Role = "Admin",
            Permissions = new List<string> { "Edit" }
        };

        var accessConditions = new Dictionary<Func<User, bool>, string>
        {
            { u => u == null, "User not found" },
            { u => !u.IsActive, "User is not active" },
            { u => u.Role != "Admin", "Role not authorized" },
            { u => !u.HasPermission("View"), "Permission denied" }
        };

        string result = accessConditions.FirstOrDefault(kvp => kvp.Key(user)).Value ?? "Access granted";

        Console.WriteLine(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Refactoring nested if statements improves readability, maintainability, and scalability. We’ve explored six powerful techniques to achieve this:

  1. Guard Clauses
  2. Switch Expressions
  3. Strategy Pattern
  4. Specification Pattern
  5. Polymorphism
  6. Dictionary for Conditions

Each method has its strengths. Choose the one that best suits your scenario and coding style. Clean code not only makes your life easier but also ensures your codebase is robust and future-proof.

Top comments (0)