Clean&Reactoring Entry Created: 05 Feb 2026 Updated: 05 Feb 2026

Strategies to Eliminate If-Else Chains in C#

In software engineering, excessive if-else or switch statements—often referred to as "Arrow Code" due to their indentation shape—are a major source of technical debt. While conditional logic is necessary, relying on it for core business flow violates the Open/Closed Principle (OCP).

When a new requirement arrives (e.g., a new payment method or a new user type), you shouldn't have to modify existing, tested methods. You should be able to extend the system by adding new code. This article explores three robust C# patterns to replace branching logic: Interface Injection, Delegate Dictionaries, and Polymorphism.

Problem Definition

The core problem isn't the if keyword itself, but the coupling it creates. When a central method controls logic for every variation of a type, that method becomes a "God Method."

Symptoms of Bad Branching:

  1. High Cyclomatic Complexity: Hard to test every path.
  2. Fragility: A change in one branch might accidentally break another.
  3. Merge Conflicts: Multiple developers modifying the same massive switch block.

Solution 1: Interface Injection (Strategy Pattern)

The Strategy Pattern is ideal when you have a family of algorithms (behaviors) and you want to make them interchangeable. Instead of the class deciding how to do something based on an enum or string, it delegates the task to an interface.

Scenario: Notification System

❌ The Bad Code

The NotificationService knows too much about the specific implementation details of every provider.

public class NotificationService
{
public void SendNotification(string type, string message)
{
// Violation: Open/Closed Principle.
// To add "PushNotification", we must modify this class.
if (type == "Email")
{
Console.WriteLine($"Sending Email: {message}");
// Complex email logic...
}
else if (type == "SMS")
{
Console.WriteLine($"Sending SMS: {message}");
// Complex SMS logic...
}
}
}

✅ The Good Code

We define an INotifier interface. The NotificationService relies on abstraction, not implementation.

public interface INotifier
{
void Send(string message);
}

public class EmailNotifier : INotifier
{
public void Send(string message)
{
// Encapsulated Email Logic
Console.WriteLine($"📧 Email: {message}");
}
}

public class SmsNotifier : INotifier
{
public void Send(string message)
{
// Encapsulated SMS Logic
Console.WriteLine($"📱 SMS: {message}");
}
}

// The service is now "dumb" in a good way.
// It doesn't know *how* to send, only *that* it can send.
public class NotificationService
{
private readonly INotifier _notifier;

// Dependency Injection
public NotificationService(INotifier notifier)
{
_notifier = notifier;
}

public void Notify(string message) => _notifier.Send(message);
}

Solution 2: Delegates & Dictionary (Dispatch Table)

If the logic is lightweight (e.g., simple calculations, returning strings, parsing), creating a full class/interface structure might be overkill. A Dictionary mapping a key to a Func or Action delegate provides $O(1)$ lookup performance and cleaner syntax.

Scenario: Order Status Messaging

❌ The Bad Code

A classic switch statement that grows vertically with every new status.

public string GetStatusMessage(string statusCode)
{
switch (statusCode)
{
case "Pending":
return "Your order is being processed.";
case "Shipped":
return "Your order is on the way.";
case "Delivered":
return "The package has arrived.";
case "Cancelled":
return "Order was cancelled.";
default:
throw new ArgumentException("Unknown status");
}
}

✅ The Good Code

We replace the control flow with a data structure. This is often called a "Dispatch Table."

public class StatusMessageProvider
{
// Map the status key to a function that returns a string
private readonly Dictionary<string, Func<string>> _messages = new()
{
{ "Pending", () => "Your order is being processed." },
{ "Shipped", () => "Your order is on the way." },
{ "Delivered", () => "The package has arrived." },
{ "Cancelled", () => "Order was cancelled." }
};

public string GetMessage(string statusCode)
{
if (_messages.TryGetValue(statusCode, out var getMessageFunc))
{
return getMessageFunc(); // Execute the delegate
}

throw new ArgumentException("Unknown status");
}
}

Solution 3: Polymorphism

Polymorphism is the cornerstone of Object-Oriented Programming. Instead of asking an object "What type are you?" and then acting on its behalf, you tell the object "Do your job."

Scenario: Employee Bonus Calculation

❌ The Bad Code

This code violates the Single Responsibility Principle. The Payroll class shouldn't handle the math for every employee type.

public class Payroll
{
public decimal CalculateBonus(object employee)
{
// Type checking leads to brittle code
if (employee is Manager manager)
{
return manager.Salary * 0.20m;
}
else if (employee is Developer dev)
{
return dev.Salary * 0.10m; // Developers deserve more, ideally!
}
else if (employee is Intern intern)
{
return 500m; // Flat bonus
}
return 0;
}
}

✅ The Good Code

Push the behavior down into the domain objects.

C#


public abstract class Employee
{
public decimal Salary { get; set; }
public abstract decimal CalculateBonus();
}

public class Manager : Employee
{
public override decimal CalculateBonus() => Salary * 0.20m;
}

public class Developer : Employee
{
public override decimal CalculateBonus() => Salary * 0.10m;
}

public class Intern : Employee
{
public override decimal CalculateBonus() => 500m;
}

// The Payroll class is now closed for modification
public class Payroll
{
public decimal ProcessBonus(Employee employee)
{
// Polymorphism handles the decision
return employee.CalculateBonus();
}
}

Conclusion

Eliminating if-else chains is not just a stylistic choice; it is a structural improvement that leads to maintainable .NET applications.

  1. Use Interfaces (Strategy) when you need to swap entire business workflows, often combined with Dependency Injection.
  2. Use Delegates (Dictionary) when you have simple inputs mapping to simple actions or values, avoiding the overhead of creating multiple classes.
  3. Use Polymorphism when the behavior is intrinsic to the object itself (Domain-Driven Design).

By adopting these patterns, you move from imperative, procedural checks to declarative, object-oriented architecture.

Share this lesson: