Strategy Pattern via Dependency Injection.

Quang PhanQuang Phan
6 min read

In my last article, I discussed how to implement a Strategy Pattern to calculate the rental pricing for different book types using the Func Delegate feature. In this article, I will demonstrate how to achieve the same functionality by replacing the Func Delegate with standard Dependency Injection.

Using Dependency Injection (DI) versus a Func delegate for implementing the Strategy pattern in software design each has its benefits, and the choice between them often depends on the specific requirements of our application, including factors like complexity, testability, and scalability. Here's a comparison of the benefits of using Dependency Injection over a Func delegate for the Strategy pattern:

Dependency Injection Benefits:

  1. Decoupling: DI helps in decoupling the classes from their dependencies, making the system more modular and flexible. It allows the application components to be more loosely coupled, which is beneficial for maintaining and extending the application.

  2. Testability: With DI, it's easier to replace implementations with mock objects for testing because dependencies are provided externally. This improves the testability of the application since you can inject different strategies for testing without changing the class's code that uses these strategies.

  3. Manageability: DI frameworks often provide advanced features like lifetime management, instance control (singleton, transient, scoped), and lazy initialization. These features can be very useful for managing dependencies across the application, especially as it grows in complexity.

  4. Configuration and Extensibility: Using DI allows for the configuration of dependencies externally (e.g., via configuration files or in code), making the system more flexible and extensible. You can change the strategy implementations without modifying the consuming classes.

  5. Integration with Frameworks: Many modern frameworks and platforms natively support DI, making it a natural choice for applications built on these frameworks. This integration can simplify development and ensure consistency in how dependencies are managed across the application.

Func Delegate Benefits:

  1. Simplicity: For simple scenarios, using a Func delegate might be quicker and easier, especially when the strategy does not have any dependencies of its own. It can be a straightforward way to implement strategy selection without the overhead of setting up a DI framework.

  2. Inline Implementation: Func delegates allow for inline implementations using lambda expressions. This can be convenient for simple strategies or when the strategy logic is short and won't be reused elsewhere.

  3. Less Overhead: Without the need for a DI framework or container, using Func delegates can reduce the overhead in terms of both performance and complexity for very simple applications or strategies.

In summary, Dependency Injection is generally more beneficial for medium to large applications with complex dependencies, where the benefits of decoupling, testability, and manageability outweigh the simplicity of using Func delegates. However, for small applications or when the strategies are simple and unlikely to change, using Func delegates might be a more straightforward approach.

In the previous implementation, our API controller endpoint calls BookService's GetByIdAsync method. Within this method, we calculate the rental price on the fly before returning the book object to the client who requested it.

In that approach we used a Func delegate, we first retrieve the appropriate strategy class from the service container. This strategy class corresponds to the type of book we are working with and contains the detailed implementation of how to calculate the discount for that specific book type.

// Select the appropriate discount strategy based on the book type
IDiscountStrategy discountStrategy = book.Type switch
{
    BookType.AudioBook => new AudioBookDiscountStrategy(),
    BookType.PaperBack => new PaperBackDiscountStrategy(),
    _ => new DefaultDiscountStrategy()
};

Func<Book, decimal> rentalCostCalculationFunc = b => discountStrategy.CalculateDiscount(b);

// Apply the rental cost calculation
book = SetRentalCost(book, rentalCostCalculationFunc);

Finally, we rely on the Func delegate to invoke and finally set the book's rental cost.

In this Dependency Injection approach we will have the following changes:

  • Move the logic for retrieving the strategy class from the service container into its own class called DiscountRateFactory. This follows the standard factory pattern and makes our code more decoupled and easier to test.
using library_system.Entities;
using library_system.Services;

namespace library_system.Factories
{
    public interface IDiscountRateFactory
    {
        IDiscountStrategy GetStrategy(BookType bookType);
    }

    public class DiscountRateFactory(IServiceProvider serviceProvider)  : IDiscountRateFactory
    {
        private readonly IServiceProvider _serviceProvider = serviceProvider;

        public IDiscountStrategy GetStrategy(BookType bookType)
        {
            // Example of selecting a strategy based on the book type
            // This can be extended to use more complex logic or DI to resolve strategies
            return bookType switch
            {
                BookType.AudioBook => _serviceProvider.GetService<AudioBookDiscountStrategy>(),
                BookType.PaperBack => _serviceProvider.GetService<PaperBackDiscountStrategy>(),
                _ => _serviceProvider.GetService<DefaultDiscountStrategy>(),
            };
        }
    }
}
  • Instead of calculating the discount logic via the Func delegate, we move that logic into its own class and abstract it through IDiscountStrategy. This allows us to implement more complex logic in our strategy class if needed and allow it readily to be used with Dependency Injection anywhere in our application.
using library_system.Entities;

namespace library_system.Services
{
    public interface IDiscountStrategy
    {
        decimal CalculateDiscount(Book book);
    }

    public class AudioBookDiscountStrategy : IDiscountStrategy
    {
        public decimal CalculateDiscount(Book book)
        {
            //hardcode return here for simplicity but this method can 
            //be as complex as you want.
            //ex.  The discount rate can be retrieved via an external asynchronous API call.
            return 0.9m;
        }
    }

    public class PaperBackDiscountStrategy : IDiscountStrategy
    {
        public decimal CalculateDiscount(Book book)
        {
            //hardcode return here for simplicity but this method can 
            //be as complex as you want.
            //ex.  The discount rate can be retrieved via an external asynchronous API call.
            return 0.9m;
        }
    }

    public class DefaultDiscountStrategy : IDiscountStrategy
    {
        public decimal CalculateDiscount(Book book) => 1.0m; // No discount
    }
}
  • We update the BookService class to dependency injected IDiscountRateFactory object which we can use to look up for the correct discount strategy object. The method itself is also updated to use dependency injection instead of a Func delegate.
public class BookService(
    IUnitOfWork unitOfWork, 
    BookFactoryResolver bookFactoryResolver,
    IDiscountRateFactory discountRateFactory) : IBookService
{
    private readonly IUnitOfWork _unitOfWork = unitOfWork;
    private readonly BookFactoryResolver _bookFactoryResolver = bookFactoryResolver;
    private readonly IDiscountRateFactory _discountRateFactory = discountRateFactory;

        public async Task<Book?> GetByIdAsync(int id)
        {
            var book = await _unitOfWork.Books.GetByIdAsync(id);

            if (book == null) return null;

            var discountStrategy = _discountRateFactory.GetStrategy(book.Type);

            book.RentalPrice = discountStrategy.CalculateDiscount(book);

            return book;
        }
}
  • Finally, we must register all these new classes and interface with the application's service container in our program.cs file.
builder.Services.AddTransient<IDiscountRateFactory, DiscountRateFactory>();
builder.Services.AddTransient<AudioBookDiscountStrategy>();
builder.Services.AddTransient<PaperBackDiscountStrategy>();
builder.Services.AddTransient<DefaultDiscountStrategy>();

The flow diagram including all the layers of this logic when making a request can be shown in this swim lane diagram:

When testing with Postman, we are still getting similar results for the discount pricing of specific book types, just as we did with the implementation using the Func delegate.

In summary, in this article, I demonstrate how to implement the Strategy Pattern for calculating rental pricing for different book types using Dependency Injection (DI) instead of the Func delegate. I discuss the benefits of using DI, such as improved decoupling, testability, manageability, configuration, and extensibility. The article showcasing a separate DiscountRateFactory class and individual IDiscountStrategy implementations. This approach enhances the application's flexibility and maintainability while producing similar results to the Func delegate method.

Code Reference: Github code.

1
Subscribe to my newsletter

Read articles from Quang Phan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Quang Phan
Quang Phan