Using the Strategy Pattern with Dependency Injection

August 12, 2024 | 6 minutes read

In software development, design patterns are essential tools that help solve common problems efficiently. One frequently used pattern is the Strategy Pattern, which allows you to select an algorithm at runtime. When combined with Dependency Injection (DI), this pattern becomes even more powerful, enabling you to create flexible, maintainable, and testable applications.

In this blog post, we’ll explore how to implement the Strategy Pattern with Dependency Injection in .NET. By the end of this post, you’ll have a solid understanding of how to leverage these concepts to build adaptable and scalable applications in .NET.

What is the Strategy Pattern?

The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one as a separate class, and make them interchangeable. The algorithm to be used is determined at runtime, which makes this pattern particularly useful when you have multiple variations of an operation.

  1. Strategy Interface: The Strategy Interface is the contract for all algorithms in this familty of algorithms. It can be an interface or abstract class that defines all the methods that the concrete strategies must implement. This is needed to ensure that all the algorithms are interchangeable.

  2. Concrete Strategies: The Concrete Strategies are the different classes that implement the Strategy Interface. Each class represents a specific implementation of an algorithm.

  3. Context: The Context maintains the reference to the strategy object. The Context delegates the execution of the algorithm to the selected strategy.

How to implement the Strategy Design Pattern?

Let’s say you’re building an application that needs to export data in different formats such as Excel, CSV, and PDF. Instead of writing a bunch of if-else statements, you can use the Strategy Pattern to keep your code clean and maintainable.

Step 1: Define the Strategy Interface

First, define the strategy interface. This interface is the blueprint for all concrete strategy classes.

public interface IExportStrategy
{
    void Export();
} 

Step 2: Implement Concrete Strategies

Next, implement the concrete strategies that implement the IExportStrategy interface.

ExcelExportConcreteStrategy.cs

public class ExcelExportConcreteStrategy : IExportStrategy
{
    public void Export() 
    {
        Console.WriteLine("Export to excel");
    }
} 

CsvExportConcreteStrategy.cs

public class CsvExportConcreteStrategy : IExportStrategy
{
    public void Export() 
    {
        Console.WriteLine("Export to csv");
    }
} 

PDFExportConcreteStrategy.cs

public class PDFExportConcreteStrategy : IExportStrategy
{
    public void Export() 
    {
        Console.WriteLine("Export to PDF");
    }
} 

Step 3: Create the Context class

The Context class holds a reference to a strategy and delegates the execution of the algorithm to the selected strategy.

public class ExportContext
{
    private readonly IExportStrategy _exportStrategy;

    public ExportContext(IExportStrategy exportStrategy)
    {
        _exportStrategy = exportStrategy;
    }

    public void Export() 
    {
        _exportStrategy.Export();
    }
} 

Step 4: Selecting a Strategy at runtime

Now, let’s create a service that selects a strategy at runtime based on user input.

public class ExportService
{
    public void Execute(string exportType)
    {
        IExportStrategy exportStrategy;

        switch (exportType.ToLower())
        {
            case "excel":
                exportStrategy = new ExcelExportConcreteStrategy();
                break;
            case "csv":
                exportStrategy = new CsvExportConcreteStrategy();
                break;
            case "pdf":
                exportStrategy = new PDFExportConcreteStrategy();
                break;
            default:
                throw new ArgumentException("Strategy implementation not found");
        }

        var context = new ExportContext(exportStrategy);
        context.Export();
    }
}

Introducing Dependency Injection

While the Strategy Pattern is powerful on its own, combining it with Dependency Injection (DI) adds even more flexibility. DI allows you to supply an object’s dependencies from the outside, rather than hard-coding them within the object.

Why Combine Strategy Pattern with Dependency Injection?

  1. Decoupling: DI separates the creation of strategies from their usage, making your code more modular.
  2. Testability: DI makes it easy to replace strategies with mocks during testing.
  3. Runtime Flexibility: DI frameworks allow you to change strategies based on configuration files, environment variables, or user input.

How do we implement the Strategy Pattern with Dependency Injection?

Step 1: Define the Strategy Interface

We’ll start by updating our strategy interface to include one (or more) identifier(s) for each strategy.

public interface IExportStrategy
{
    ExportTypeEnum ExportTypeId { get; }

    void Export();
} 

Step 2: Define an enum for the different export types

Create an enum to identify the different export strategies. This way, we get rid of the magic strings.

public enum ExportTypeEnum
{
    Unknown = 0,
    Excel = 1,
    Csv = 2,
    PDF = 3
} 

Step 3: Implement Concrete Strategies

Now, update your concrete strategies to include the ExportTypeId.

ExcelExportConcreteStrategy.cs

public class ExcelExportConcreteStrategy : IExportStrategy
{
    public ExportTypeEnum ExportTypeId => ExportTypeEnum.Excel;

    public void Export() 
    {
        Console.WriteLine("Export to excel");
    }
} 

CsvExportConcreteStrategy.cs

public class CsvExportConcreteStrategy : IExportStrategy
{
    public ExportTypeEnum ExportTypeId => ExportTypeEnum.Csv;

    public void Export() 
    {
        Console.WriteLine("Export to csv");
    }
} 

PDFExportConcreteStrategy.cs

public class PDFExportConcreteStrategy : IExportStrategy
{
    public ExportTypeEnum ExportTypeId => ExportTypeEnum.PDF;

    public void Export() 
    {
        Console.WriteLine("Export to PDF");
    }
} 

Step 4: Register and inject dependencies using .NET DI

In a .NET application, you can register and resolve dependencies using the built-in DI framework.

Program.cs

builder.Services.AddScoped<IExportStrategy, ExcelExportConcreteStrategy>();
builder.Services.AddScoped<IExportStrategy, CsvExportConcreteStrategy>();
builder.Services.AddScoped<IExportStrategy, PDFExportConcreteStrategy>();
builder.Services.AddScoped<ExportContext>();
builder.Services.AddScoped<IExportService, ExportService>();

Step 5: Use DI to resolve and execute strategies

With DI, you can inject all the strategies into your context class, allowing it to select the appropriate strategy at runtime.

public class ExportContext
{
    private readonly IEnumerable<IExportStrategy> _exportStrategies;

    public ExportContext(IEnumerable<IExportStrategy> exportStrategies)
    {
        _exportStrategies = exportStrategies;
    }

    public void Export(ExportTypeEnum exportTypeId) 
    {
        var strategy = _exportStrategies
                            .FirstOrDefault(s => s.ExportTypeId == exportTypeId);

        if (strategy == null)
        {
            throw new ArgumentException($"Strategy implementation not found for {exportTypeId}");
        }

        strategy.Export();
    }
}

Step 6: Selecting a strategy at runtime

The real power of combining DI with the Strategy Pattern comes when you dynamically select the strategy based on user input or other runtime conditions. The Context class accepts an identifier in the method. In this example the exportTypeId in the Export method. It is then the Conetxt class that selects the appropriate strategy.

public class ExportService : IExportService
{
    private readonly ExportContext _context;

    public ExportService(ExportContext context)
    {
        _context = context;
    }

    public void Export(ExportTypeEnum exportTypeId)
    {
        _context.Export(exportTypeId);
    }
}

Adding an new strategy implementation

When you need to add a new export type:

  • Create the new strategy implementation.
  • Register it in the dependency container.
  • Add the new type to the enum.

And that’s it! This approach makes it easy to extend your application without modifying existing code.

What are the benefits of this approach?

  • Flexibility: You can easily add or swap strategies without changing the core logic.
  • Testability: Strategies can be mocked or stubbed for unit testing, improving test coverage and reliability.
  • Separation of Concerns: Each strategy encapsulates its own logic, making the code easier to manage.
  • Runtime Selection: The appropriate strategy can be selected based on runtime conditions.

Conclusion

Combining the Strategy Pattern with Dependency Injection in .NET is a powerful approach to building flexible, scalable, and maintainable applications. By separating the “what” from the “how,” this pattern promotes clean, modular code that is easy to extend and test.

However, as with any design pattern, it’s important to weigh the benefits against the potential complexities it introduces. While the Strategy Pattern provides a robust framework for handling multiple algorithms, it can also lead to increased complexity if not managed properly. Dependency Injection adds another layer of flexibility, enabling dynamic selection of strategies and further decoupling your code.

In summary, the Strategy Pattern combined with Dependency Injection can greatly enhance your application’s architecture, making it more adaptable to change. The result will be a more modular, testable, and future-proof application that can easily grow with your needs.

If you have any questions or comments on this approach, feel free to leave them below!

comments powered by Disqus

You may also like

Introduction to Microsoft Orleans

One of the frameworks in the .NET ecosystem that doesn’t get as much …

Read More

New Pluralsight course released

I’m excited to announce the release of my new Pluralsight course, Working with …

Read More