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

Here are the implementations for the Strategy Pattern, Specification Pattern, and Polymorphism in detail:


3. Refactoring with Strategy Pattern

The Strategy Pattern encapsulates each condition into a separate strategy, making the logic reusable, modular, and testable.

Implementation

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 interface IAccessStrategy
{
    bool CanAccess(User user);
    string Message { get; }
}

public class UserNotFoundStrategy : IAccessStrategy
{
    public string Message => "User not found";
    public bool CanAccess(User user) => user == null;
}

public class UserNotActiveStrategy : IAccessStrategy
{
    public string Message => "User is not active";
    public bool CanAccess(User user) => user != null && !user.IsActive;
}

public class RoleNotAuthorizedStrategy : IAccessStrategy
{
    public string Message => "Role not authorized";
    public bool CanAccess(User user) => user != null && user.IsActive && user.Role != "Admin";
}

public class PermissionDeniedStrategy : IAccessStrategy
{
    public string Message => "Permission denied";
    public bool CanAccess(User user) => user != null && user.IsActive && user.Role == "Admin" && !user.HasPermission("View");
}

public class AccessGrantedStrategy : IAccessStrategy
{
    public string Message => "Access granted";
    public bool CanAccess(User user) => user != null && user.IsActive && user.Role == "Admin" && user.HasPermission("View");
}

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

        var strategies = new List<IAccessStrategy>
        {
            new UserNotFoundStrategy(),
            new UserNotActiveStrategy(),
            new RoleNotAuthorizedStrategy(),
            new PermissionDeniedStrategy(),
            new AccessGrantedStrategy()
        };

        var applicableStrategy = strategies.FirstOrDefault(s => s.CanAccess(user));
        Console.WriteLine(applicableStrategy?.Message ?? "Unknown error");
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Refactoring with Specification Pattern

The Specification Pattern represents conditions as reusable specifications, allowing combinations and modular rules.

Implementation

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 interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
}

public class IsUserActiveSpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User user) => user != null && user.IsActive;
}

public class IsAdminSpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User user) => user != null && user.Role == "Admin";
}

public class HasPermissionSpecification : ISpecification<User>
{
    private readonly string _permission;
    public HasPermissionSpecification(string permission) => _permission = permission;

    public bool IsSatisfiedBy(User user) => user != null && user.HasPermission(_permission);
}

public class AccessSpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User user) =>
        new IsUserActiveSpecification().IsSatisfiedBy(user) &&
        new IsAdminSpecification().IsSatisfiedBy(user) &&
        new HasPermissionSpecification("View").IsSatisfiedBy(user);
}

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

        var accessSpec = new AccessSpecification();

        if (accessSpec.IsSatisfiedBy(user))
            Console.WriteLine("Access granted");
        else
            Console.WriteLine("Access denied");
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Refactoring with Polymorphism

Polymorphism involves creating different subclasses for each user type and defining behaviors specific to each.

Implementation

public abstract class User
{
    public abstract bool CanAccess();
    public abstract string Message { get; }
}

public class GuestUser : User
{
    public override bool CanAccess() => false;
    public override string Message => "User not found";
}

public class InactiveUser : User
{
    public override bool CanAccess() => false;
    public override string Message => "User is not active";
}

public class AdminUser : User
{
    private readonly List<string> _permissions;

    public AdminUser(List<string> permissions)
    {
        _permissions = permissions;
    }

    public override bool CanAccess() => _permissions.Contains("View");
    public override string Message => CanAccess() ? "Access granted" : "Permission denied";
}

public class Program
{
    public static void Main()
    {
        User user = new AdminUser(new List<string> { "Edit" });
        Console.WriteLine(user.Message);
    }
}
Enter fullscreen mode Exit fullscreen mode

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)