blog
Ottorino Bruni  

Simplify Your C# Code: Replace If-Else with the Strategy Pattern in .NET

What Are Design Patterns?

Design patterns are proven solutions to common software design problems. They are like pre-packaged “recipes” that allow us to solve common problems efficiently and in a reusable way. They have been identified and cataloged over the years by experienced programmers, and knowledge of them is considered essential for any developer. Design patterns are categorized into three main groups: Creational, Structural, and Behavioral. For more information and the history of design patterns, you can read here.

What are Design Patterns for?

  • Improve code readability: By making the code more structured and predictable, they make it easier to understand for other developers as well.
  • Increase code reusability: Patterns can be applied in different parts of the code, reducing duplication and facilitating maintenance.
  • Facilitate collaboration: A common language based on patterns facilitates communication between developers and teamwork.
  • Promote good programming practices: Patterns encourage the adoption of well-tested and high-quality solutions.

What Is the Strategy Pattern?

The Strategy Pattern falls under the Behavioral category. It is used to define a family of algorithms, encapsulate each one, and make them interchangeable.

The Strategy Pattern is particularly useful when you need to replace complex if-else or switch-case logic. Instead of hard-coding different behaviors in one class, you can extract these behaviors into separate strategy classes. This makes your code cleaner, easier to maintain, and more adaptable to future changes.

In essence, the Strategy Pattern helps you follow the Open/Closed Principle, which states that a class should be open for extension but closed for modification. By using this pattern, you can introduce new strategies without modifying the existing code.

Strategy Pattern

What is the Strategy Pattern for?

  • Makes the code more flexible: Allows you to change the algorithm used at runtime, without having to modify the client code.
  • Improves testability: Each algorithm is encapsulated in a separate class, making unit testing easier.
  • Increases cohesion: Each class is responsible for a single algorithm, improving code readability.
  • Avoids long if-else chains: Replaces long and complex conditional structures with a more elegant and maintainable approach.

Example: Sending Messages with Strategy Pattern

Disclaimer: This example is intended for educational purposes. While it provides a good starting point for learning, there are always better ways to write and optimize code and applications. Use this as a base and continually strive to follow best practices and improve your implementations.

Before starting, make sure you have the following installed:

  • .NET SDK: Download and install the .NET SDK if you haven’t already.
  • Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
  • C# Extension for VSCode: Install the C# extension for VSCode to enable C# support.

If-Else Approach: Sending Messages

In many cases, we end up writing or maintaining a codebase where a specific business logic is filled with if-else statements that continue to grow as new cases are added. This can quickly make the code harder to manage and maintain. This section showcases an approach with if-else statements for message sending. As new methods for sending messages are added, the code becomes cluttered and harder to maintain.

public class MessageSender
{
    public void SendMessage(string message, string method)
    {
        if (method == "Console")
        {
            // Logic to send message via Console
            Console.WriteLine($"Sending message via Console: {message}");
        }
        else if (method == "Email")
        {
            // Logic to send message via Email
            Console.WriteLine($"Sending message via Email: {message}");
        }
        else if (method == "SMS")
        {
            // Logic to send message via SMS
            Console.WriteLine($"Sending message via SMS: {message}");
        }
        // Additional conditions would be needed for any new message methods
    }
}

// Usage
var messageSender = new MessageSender();
messageSender.SendMessage("Hello, Console!", "Console");
messageSender.SendMessage("Hello, Email!", "Email");
messageSender.SendMessage("Hello, SMS!", "SMS");

Example: Sending Messages with Strategy Pattern

Disclaimer: This example is intended for educational purposes. While it provides a good starting point for learning, there are always better ways to write and optimize code and applications. Use this as a base and continually strive to follow best practices and improve your implementations.

Prerequisites

Before diving into the example, if you have read the previous article, How to Implement Dependency Injection in .NET: C# Console App Guide Using Visual Studio Code, we will use the code we’ve already written there.

Make sure you have the following installed:

  • .NET SDK: Download and install the .NET SDK if you haven’t already.
  • Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
  • C# Extension for VSCode: Install the C# extension for VSCode to enable C# support.

Strategy Pattern: Improved Approach

The Strategy Pattern offers a more flexible and maintainable solution for sending messages with different methods. Here’s how it works:

Step 1: Define the Strategy Interface

We define an interface called IMessageService that outlines the contract for sending messages. This interface acts as a blueprint for concrete implementations.

public interface IMessageService
{
    void SendMessage(string message);
}

Step 2: Implement Concrete Strategies

We create concrete classes that implement the IMessageService interface. Each class represents a specific way of sending messages (e.g., Console, Email, SMS).

public class ConsoleMessageService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"ConsoleMessageService: {message}");
    }
}

public class EmailMessageService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"EmailMessageService: Sending '{message}' via email.");
    }
}

public class SmsMessageService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"SmsMessageService: Sending '{message}' via SMS.");
    }
}
Implement Concrete Strategies

Step 3: Dynamic Strategy Handling with MessageProcessor

In your updated MessageProcessor class, you have introduced a SetStrategy method that allows changing the message sending strategy at runtime. This is a great enhancement as it gives you the flexibility to switch strategies without needing to create new instances of MessageProcessor

public class MessageProcessor
{
    private IMessageService _currentStrategy;

    // Constructor to initialize the strategy
    public MessageProcessor(IMessageService messageService)
    {
        _currentStrategy = messageService;
    }

    // Method to change the strategy at runtime
    public void SetStrategy(IMessageService strategy)
    {
        _currentStrategy = strategy;        
    }

    // Method to process the message using the current strategy
    public void ProcessMessage(string message)
    {
        _currentStrategy.SendMessage(message);
    }
}
Dynamic Strategy Handling with MessageProcessor

Step 4: Configure the Dependency Injection Container (Optional)

In this example, we utilize Dependency Injection (DI) to manage the creation and injection of concrete message services.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;

class Program
{
    static void Main(string[] args)
    {
        var host = CreateHost(args);

        var serviceProvider = host.Services;

        var consoleMessageService = serviceProvider.GetRequiredService<ConsoleMessageService>();
        var emailMessageService = serviceProvider.GetRequiredService<EmailMessageService>();
        var smsMessageService = serviceProvider.GetRequiredService<SmsMessageService>();

        // Initialize with one strategy
        var processor = new MessageProcessor(consoleMessageService);
        processor.ProcessMessage("Console Message!");

        // Switch strategies and use the processor
        processor.SetStrategy(emailMessageService);
        processor.ProcessMessage("Email Message!");
        
        processor.SetStrategy(smsMessageService);
        processor.ProcessMessage("SMS Message!");
    }

    static IHost CreateHost(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices(ConfigureServices)
            .Build();

    static void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<ConsoleMessageService>();
        services.AddTransient<EmailMessageService>();
        services.AddTransient<SmsMessageService>();
        services.AddTransient<MessageProcessor>();
    }
}
Configure the Dependency Injection Container

In the Strategy Pattern approach, each message sending method is encapsulated in its own class. This makes it easy to add new methods without modifying existing code, adhering to the Open/Closed Principle. The MessageProcessor class can switch between different message sending strategies at runtime, and you can add new strategies simply by creating new classes and configuring them in the Dependency Injection container.

Run the Example

Here the full code:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;

public interface IMessageService
{
    void SendMessage(string message);
}

public class ConsoleMessageService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"ConsoleMessageService: '{message}'!");
    }
}

public class EmailMessageService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"EmailMessageService: '{message}' via email!");
    }
}

public class SmsMessageService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"SmsMessageService: '{message}' via sms!");
    }
}

public class MessageProcessor
{
    private IMessageService _currentStrategy;

    public MessageProcessor(IMessageService messageService)
    {
        this._currentStrategy = messageService;
    }

    public void SetStrategy(IMessageService strategy)
    {
        this._currentStrategy = strategy;        
    }

    public void ProcessMessage(string message)
    {
        this._currentStrategy.SendMessage(message);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var host = CreateHost(args);

        var serviceProvider = host.Services;

        var consoleMessageService = serviceProvider.GetRequiredService<ConsoleMessageService>();
        var emailMessageService = serviceProvider.GetRequiredService<EmailMessageService>();
        var smsMessageService = serviceProvider.GetRequiredService<SmsMessageService>();

        // Initialize with one strategy
        var processor = new MessageProcessor(consoleMessageService);
        processor.ProcessMessage("Console Message!");

        // Switch strategies and use the processor
        processor.SetStrategy(emailMessageService);
        processor.ProcessMessage("Email Message!");
        
        processor.SetStrategy(smsMessageService);
        processor.ProcessMessage("SMS Message!");
    }

    static IHost CreateHost(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices(ConfigureServices)
            .Build();

    static void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<ConsoleMessageService>();
        services.AddTransient<EmailMessageService>();
        services.AddTransient<SmsMessageService>();
        services.AddTransient<MessageProcessor>();
    }
}

Conclusion: The Strategy Pattern: A Powerful Tool, Used Wisely

The Strategy Pattern offers a powerful way to improve code flexibility, maintainability, and testability. By encapsulating different algorithms in separate classes, it allows you to easily switch between strategies at runtime, making your code more adaptable to changing requirements.

However, like any design pattern, it’s essential to use the Strategy Pattern judiciously. Overusing it can introduce unnecessary complexity and overhead, especially for simple scenarios. Consider the following points:

  • Weigh the benefits against the costs: Evaluate whether the increased flexibility and maintainability outweigh the potential overhead and learning curve.
  • Avoid overusing the pattern: For simple scenarios, a straightforward if-else statement might be sufficient.
  • Design strategies carefully: Ensure that strategies are well-defined and have clear responsibilities to minimize code duplication and maintainability issues.
  • Consider performance implications: If performance is critical, profile your application to assess the impact of using the Strategy Pattern.

However, it’s important to use this pattern when it truly fits your needs. Here are some potential drawbacks:

  • Overhead: Can add unnecessary complexity with extra classes for simple cases.
  • Learning Curve: Might be difficult for new developers to understand which strategy to use and when.
  • Potential Duplication: Overlapping code among strategies can lead to maintenance issues.

In conclusion, the Strategy Pattern is a valuable tool in your design toolkit, but it’s essential to use it thoughtfully and consider the trade-offs involved. By applying it judiciously, you can create more flexible, maintainable, and testable code.

If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.

Thanks for reading!

Leave A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.