DEV Community

Cover image for Hands-On: Strategy Design Pattern in .NET Core
Hamsheed Salamut
Hamsheed Salamut

Posted on

Hands-On: Strategy Design Pattern in .NET Core

About Patterns

Design patterns were introduced in Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides’s seminal work Design Patterns: Elements of Reusable Object Oriented Software.

Design patterns provide a high-level language of discourse for programmers to describe a system and to discuss solutions to common problems. The proper and intelligent usage of patterns will guide a developer into designing a system that conforms to well-established prior practices, without stifling innovation.

The Strategy Pattern

The Strategy Pattern is one of the original patterns described in the famous Design Patterns book. It is a behavioral design pattern that allows us to implement different functionalities in separate classes and make their objects interchangeable.

Put simply, we have a main Context object that holds a reference towards a Strategy object and delegates it by executing its corresponding functionality.

Without any further ado, let's dive into the Strategy Pattern Design implementation.

This post is organized into the following sections:

  • Strategy Design Pattern Structure
  • Implementation of the Strategy Pattern
  • Common usage of the Strategy Pattern
  • Takeaways and Final Thoughts

Strategy Design Pattern Structure

As emphasized above, the strategy design pattern consists of a Context object which holds a reference towards the Strategy object. For an exhaustive implementation, we need the Strategy object - Interface to define a mechanism for the Context object to execute the strategy and the Concrete Strategies objects which implement the Strategy Interface.

The Participants

Alt Text

  • The Strategy declares an interface which is implemented by all supported algorithms.
  • The ConcreteStrategy objects implement the algorithm defined by the Strategy.
  • The Context maintains a reference to a Strategy object, and uses that reference to invoke the algorithm defined by a particular ConcreteStrategy.

Implementation of the Strategy Pattern

For this article, let's model this pattern by talking about some different ways to cook food.

A Delicious Example

Alt Text

Being an eat-meater, there's often more than one way to cook meats under safe eating temperatures.

For instance, a meat can be roasted, deep-fried or broiled. Each of these methods will get the item cooked, just via different processes. These processes, in object-oriented code can each be their own class.

In our example, we will assume that we will ask the user what method they would like to use to cook their food, and then implement that method using the Strategy design pattern.

To start, let's write up the Strategy participant, the Interface ICookStrategyService.

    public interface ICookStrategyService
    {
        void Cook(string food);
    }
Enter fullscreen mode Exit fullscreen mode

Each strategy implements the method Cook() which indicates how a food item shall be cooked. Let's implement a few of those strategies - also referred to as ConcreteStrategy participants.

    public class GrillingService : ICookStrategyService
    {
        public void Cook(string food)
        {
            Console.WriteLine($"Cooking {food} by grilling it");
        }
    }

    public class RoastingService : ICookStrategyService
    {
        public void Cook(string food)
        {
            Console.WriteLine($"Cooking {food} by roasting it");
        }
    }

    public class DeepFryingService : ICookStrategyService
    {
        public void Cook(string food)
        {
            Console.WriteLine($"Cooking {food} by deep frying it");
        }
    }
Enter fullscreen mode Exit fullscreen mode

In a typical ASP.NET Core application, we shall leverage the built-in dependency injection mechanism to register the services as follows:

services.AddTransient<ICookingStrategyService, GrillingService>();
services.AddTransient<ICookingStrategyService, RoastingService >();
services.AddTransient<ICookingStrategyService, DeepFryingService>();
Enter fullscreen mode Exit fullscreen mode

We would then inject these instances in the constructor of a class to use these services. However, when we ought to run the application, we will observe that all three parameters are instances of type DeepFryingService because DeepFryingService has been injected last.

To overcome this limitation, we will use an IEnumerable collection of services to register the services and a delegate to retrieve a specific service instance.

Use a delegate to retrieve a specific service instance

Consider the following enum that contains integer constants that correspond to the three types Grill, Roast, and Fry.

public enum CookingType
{
   Grill,
   Roast,
   Fry
}
Enter fullscreen mode Exit fullscreen mode

We then declare a shared delegate:

public delegate ICookingStrategyService ServiceResolver(CookingType cookingType);

Enter fullscreen mode Exit fullscreen mode

The ConcreteStrategies are then registered as Transient services in the Startup.cs file.

services.AddTransient<GrillingService>();
services.AddTransient<RoastingService>();
services.AddTransient<DeepFryingService>();
Enter fullscreen mode Exit fullscreen mode

Return instances based on service type at runtime

The following code snippet shows how we can return instances of the GrillingService, RoastingService, and DeepFryingService classes depending on the service type selected at runtime.

services.AddScoped<ServiceResolver>(serviceProvider => key =>
{
      switch (key)
      {
         case CookingType.Grill:
              return serviceProvider.GetRequiredService<GrillingService>();
         case CookingType.Roast:
              return serviceProvider.GetRequiredService<RoastingService>();
         case CookingType.Fry:
              return serviceProvider.GetRequiredService<DeepFryingService>();
          default:
              throw new KeyNotFoundException();
       }
});
Enter fullscreen mode Exit fullscreen mode

When to use Strategy Pattern

  • Where different variations for some functionalities in an object and we want to switch from one variation to another in a runtime. Furthermore, if we have business applications that may have many different possible strategies in play, the strategy pattern should be the right choice for us.

Takeaways and Final Thoughts

The strategy pattern achieves decoupling by encapsulating variable parts of a behavior behind an abstract interface. This abstraction can make working with a family of related algorithms easier, and it lets us postpone introduction of new ConcreteStrategies. In addition, the strategy pattern allows for different behaviors to be implemented and tested individually.

Happy Coding! 💻

Top comments (3)

Collapse
 
marcosbelorio profile image
Marcos Belorio

Nice explanation, thanks!

Collapse
 
awesomevirendra profile image
awesomevirendra

I really like to know how it can be used from controller?

Collapse
 
evandro_nascimento_e1f9e9 profile image
Evandro Nascimento

Do I call from ConcreteStrategies? Can you post an example of how to use this implementation?