What is the Strategy Design Pattern?
The Strategy design pattern is a behavioral design pattern that enables an object to alter its behavior when its internal state changes. It does this by encapsulating the behavior as an object and allowing the object to be swapped in and out at runtime.
The purpose of using the Strategy pattern is to allow an object to change its behavior dynamically by changing the object it uses to perform a task. This can be useful in situations where the object needs to perform the same task in multiple ways, depending on the context or the internal state of the object.
Here’s a simple example of how the Strategy pattern might be used:
Imagine you have a shopping cart system that needs to calculate the total price of all the items in the cart. The total price may depend on the type of user (e.g., regular, premium, or VIP), and the discount that applies to them. Instead of having a long, complex method that calculates the total price based on all these different factors, you can use the Strategy pattern to encapsulate the different discount calculation behaviors into separate strategy objects. Then, you can simply pass the appropriate strategy object to the shopping cart, and it will use the correct behavior to calculate the total price.
This has several benefits:
- It makes the code easier to read and understand, because the logic for each discount calculation is separated from the main code.
- It makes the code easier to maintain and extend, because you can add new discount calculation behaviors by creating new strategy objects, without changing the existing code.
- It makes the code more flexible and adaptable, because you can easily change the discount calculation behavior at runtime by simply swapping out the strategy object.
// The interface for the discount calculation strategy
public interface IDiscountStrategy
{
decimal CalculateDiscount(decimal price);
}
// A concrete strategy for calculating a regular user's discount
public class RegularDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal price)
{
return price * 0.9m; // 10% discount
}
}
// A concrete strategy for calculating a premium user's discount
public class PremiumDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal price)
{
return price * 0.8m; // 20% discount
}
}
// A concrete strategy for calculating a VIP user's discount
public class VIPDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal price)
{
return price * 0.5m; // 50% discount
}
}
// The shopping cart class, which uses a discount strategy to calculate the total price
public class ShoppingCart
{
private IDiscountStrategy _discountStrategy;
private List<decimal> _items = new List<decimal>();
public ShoppingCart(IDiscountStrategy discountStrategy)
{
_discountStrategy = discountStrategy;
}
public void AddItem(decimal price)
{
_items.Add(price);
}
public decimal CalculateTotal()
{
decimal total = _items.Sum();
return _discountStrategy.CalculateDiscount(total);
}
}
// Example usage
IDiscountStrategy strategy = new PremiumDiscountStrategy();
ShoppingCart cart = new ShoppingCart(strategy);
cart.AddItem(100);
cart.AddItem(50);
decimal total = cart.CalculateTotal();
// total is 130, because the total price (150) is discounted by 20%
In this example, the IDiscountStrategy
interface defines the behavior for calculating a discount. The RegularDiscountStrategy
, PremiumDiscountStrategy
, and VIPDiscountStrategy
classes are concrete implementations of this behavior for different types of users.
The ShoppingCart
class uses a IDiscountStrategy
object to calculate the total price of all the items in the cart. The strategy object is passed to the ShoppingCart
constructor, and it is used by the CalculateTotal
method to apply the appropriate discount to the total price.
Finally, the example shows how to create a ShoppingCart
object with a particular discount strategy, and how to use it to calculate:
// Create a ShoppingCart object with a particular discount strategy
IDiscountStrategy strategy = new PremiumDiscountStrategy();
ShoppingCart cart = new ShoppingCart(strategy);
// Add some items to the cart
cart.AddItem(100);
cart.AddItem(50);
// Calculate the total price of the items in the cart
decimal total = cart.CalculateTotal();
// The total is 120, because the total price (150) is discounted by 20%
// Now suppose we want to change the discount strategy to a VIP discount
IDiscountStrategy newStrategy = new VIPDiscountStrategy();
cart.SetDiscountStrategy(newStrategy);
// Calculate the total price again, using the new discount strategy
total = cart.CalculateTotal();
// The total is 75, because the total price (150) is discounted by 50%
In this example, we create a ShoppingCart
object with a PremiumDiscountStrategy
, which applies a 20% discount to the total price. Then we add some items to the cart and calculate the total price, which is 120.
Next, we want to change the discount strategy to a VIPDiscountStrategy
, which applies a 50% discount. To do this, we create a new VIPDiscountStrategy
object and pass it to the SetDiscountStrategy
method of the ShoppingCart
object. This method can be implemented like this:
public void SetDiscountStrategy(IDiscountStrategy discountStrategy)
{
_discountStrategy = discountStrategy;
}
Finally, we calculate the total price again, using the new discount strategy. This time, the total is 75, because the total price is discounted by 50%.
Let’s make a different example which the famous duck sample:
// The interface for the fly behavior
public interface IFlyBehavior
{
void Fly();
}
// A concrete fly behavior for ducks that can fly
public class FlyBehavior : IFlyBehavior
{
public void Fly()
{
Console.WriteLine("I'm flying!");
}
}
// A concrete fly behavior for ducks that can't fly
public class NoFlyBehavior : IFlyBehavior
{
public void Fly()
{
Console.WriteLine("I can't fly.");
}
}
// The interface for the quack behavior
public interface IQuackBehavior
{
void Quack();
}
// A concrete quack behavior for ducks that quack
public class QuackBehavior : IQuackBehavior
{
public void Quack()
{
Console.WriteLine("Quack!");
}
}
// A concrete quack behavior for ducks that don't quack
public class NoQuackBehavior : IQuackBehavior
{
public void Quack()
{
Console.WriteLine("I don't quack.");
}
}
// The base Duck class
public abstract class Duck
{
protected IFlyBehavior FlyBehavior;
protected IQuackBehavior QuackBehavior;
public Duck(IFlyBehavior flyBehavior, IQuackBehavior quackBehavior)
{
FlyBehavior = flyBehavior;
QuackBehavior = quackBehavior;
}
public void SetFlyBehavior(IFlyBehavior flyBehavior)
{
FlyBehavior = flyBehavior;
}
public void SetQuackBehavior(IQuackBehavior quackBehavior)
{
QuackBehavior = quackBehavior;
}
public abstract void Display();
public void PerformFly()
{
FlyBehavior.Fly();
}
public void PerformQuack()
{
QuackBehavior.Quack();
}
}
// A concrete Duck class for Mallard ducks
public class MallardDuck : Duck
{
public MallardDuck() : base(new FlyBehavior(), new QuackBehavior())
{
}
public override void Display()
{
Console.WriteLine("I'm a mallard duck.");
}
}
// A concrete Duck class for Rubber ducks
public class RubberDuck : Duck
{
public RubberDuck() : base(new NoFlyBehavior(), new QuackBehavior())
{
}
public override void Display()
{
Console.WriteLine("I'm a rubber duck.");
}
}
// Create a MallardDuck object
MallardDuck mallard = new MallardDuck();
// Display the MallardDuck
mallard.Display(); // prints "I'm a mallard duck."
// Make the MallardDuck fly and quack
mallard.PerformFly(); // prints "I'm flying!"
mallard.PerformQuack(); // prints "Quack!"
// Create a RubberDuck object
RubberDuck rubber = new RubberDuck();
// Display the RubberDuck
rubber.Display(); // prints "I'm a rubber duck."
// Make the RubberDuck fly and quack
rubber.PerformFly(); // prints "I can't fly."
rubber.PerformQuack(); // prints "Quack!"
// Now suppose we want to change the RubberDuck's quack behavior
IQuackBehavior newQuackBehavior = new NoQuackBehavior();
rubber.SetQuackBehavior(newQuackBehavior);
// Make the RubberDuck quack again
rubber.PerformQuack(); // prints "I don't quack."
In this example, we create a MallardDuck
object and a RubberDuck
object. The MallardDuck
has the default fly and quack behaviors, while the RubberDuck
has the default quack behavior but a custom fly behavior that makes it unable to fly.
We display each duck and make it fly and quack. The MallardDuck
is able to fly and quack, while the RubberDuck
is unable to fly but is able to quack.
Then, we change the RubberDuck
's quack behavior to a custom behavior that makes it unable to quack. When we make the RubberDuck
quack again, it prints a message indicating that it can't quack.
You can check out the youtube channel of Christopher Okhravi for detailed examination of this sample.
And of course you can check out the book of O`Reilly Media’s Head First Design Patterns.
Here are some real-world scenarios that you can use the strategy design pattern:
- Calculating shipping costs: Suppose you have an online store that ships items to customers. The cost of shipping an item may depend on the weight of the item, the distance to the destination, and the shipping method (e.g., standard, express, or next-day). Instead of implementing a complex method that calculates the shipping cost based on all these different factors, you can use the Strategy pattern to encapsulate the different calculation behaviors into separate strategy objects. Then, you can simply pass the appropriate strategy object to the shopping cart, and it will use the correct behavior to calculate the shipping cost.
- Handling payment methods: Suppose you have a payment system that supports multiple payment methods (e.g., credit card, debit card, PayPal, etc.). The process for handling each payment method may be different, depending on the specific details of the payment method. Instead of implementing a long, complex method that handles all these different payment methods, you can use the Strategy pattern to encapsulate the different payment handling behaviors into separate strategy objects. Then, you can simply pass the appropriate strategy object to the payment system, and it will use the correct behavior to handle the payment.
- Sorting a list of items: Suppose you have a list of items that you want to sort in different ways (e.g., by name, by date, by price, etc.). Instead of implementing multiple sorting methods, you can use the Strategy pattern to encapsulate the different sorting behaviors into separate strategy objects. Then, you can simply pass the appropriate strategy object to the list, and it will use the correct behavior to sort the items.
- Different behaviors for game characters: Suppose you have a game that has different characters on it. You can define different attacking or defending behaviors for different characters. You can simply define the behavior that character has and it will use that behavior when you attack or defend.
Here is the example of how you implement game character behaviors on C#:
// The interface for the character behavior
public interface ICharacterBehavior
{
void PerformBehavior();
}
// A concrete character behavior for characters that attack
public class AttackBehavior : ICharacterBehavior
{
public void PerformBehavior()
{
Console.WriteLine("Attacking!");
}
}
// A concrete character behavior for characters that defend
public class DefendBehavior : ICharacterBehavior
{
public void PerformBehavior()
{
Console.WriteLine("Defending!");
}
}
// A concrete character behavior for characters that heal
public class HealBehavior : ICharacterBehavior
{
public void PerformBehavior()
{
Console.WriteLine("Healing!");
}
}
// The base Character class
public abstract class Character
{
protected ICharacterBehavior Behavior;
public Character(ICharacterBehavior behavior)
{
Behavior = behavior;
}
public void SetBehavior(ICharacterBehavior behavior)
{
Behavior = behavior;
}
public abstract void Display();
public void PerformBehavior()
{
Behavior.PerformBehavior();
}
}
And yours concrete classes look like this:
// A concrete Character class for warriors
public class Warrior : Character
{
public Warrior() : base(new AttackBehavior())
{
}
public override void Display()
{
Console.WriteLine("I'm a warrior.");
}
}
// A concrete Character class for mages
public class Mage : Character
{
public Mage() : base(new HealBehavior())
{
}
public override void Display()
{
Console.WriteLine("I'm a mage.");
}
}
// A concrete Character class for tanks
public class Tank : Character
{
public Tank() : base(new DefendBehavior())
{
}
public override void Display()
{
Console.WriteLine("I'm a tank.");
}
}
// Example usage
Warrior warrior = new Warrior();
warrior.Display(); // prints "I'm a warrior."
warrior.PerformBehavior(); // prints "Attacking!"
Mage mage = new Mage();
mage.Display(); // prints "I'm a mage."
mage.PerformBehavior(); // prints "Healing!"
Tank tank = new Tank();
tank.Display(); // prints "I'm a tank."
tank.PerformBehavior(); // prints "Defending!"
// Now suppose we want to change the warrior's behavior to defend
ICharacterBehavior newBehavior = new DefendBehavior();
warrior.SetBehavior(newBehavior);
// Make the warrior perform its behavior again
warrior.PerformBehavior(); // prints "Defending!"
In this example, we create three different character classes: Warrior
, Mage
, and Tank
. Each character class has a default behavior that is appropriate for its role (e.g., Warrior
has an AttackBehavior
, Mage
has a HealBehavior
, and Tank
has a DefendBehavior
).
We create an instance of each character class and display and perform the behavior for each character. The Warrior
attacks, the Mage
heals, and the Tank
defends.
Then, we change the Warrior
's behavior to DefendBehavior
, and make the Warrior
perform its behavior again. This time, the Warrior
defends instead of attacking.
Disadvantages
Like any design pattern, the Strategy pattern has some potential disadvantages that you should consider when deciding whether to use it in your code:
- Increased complexity: Using the Strategy pattern can add a certain level of complexity to your code, as you need to define multiple strategy classes and ensure that they work correctly together. This can make your code more difficult to understand and maintain.
- Increased number of classes: The Strategy pattern involves creating multiple strategy classes to encapsulate different behaviors. This can lead to an increase in the overall number of classes in your codebase, which can make it more difficult to manage.
- Violation of the single responsibility principle: The single responsibility principle states that a class should have only one reason to change. By encapsulating multiple behaviors into a single strategy class, the Strategy pattern can violate this principle.
- Lack of flexibility: Depending on how you implement the Strategy pattern, it may not be easy to change the behavior of an object at runtime. For example, if you use inheritance to implement the pattern, it can be difficult to change the behavior of an object after it has been created.
- Overuse: As with any design pattern, it’s important to use the Strategy pattern only when it is the best solution to a problem. Overusing the pattern can make your code more complex and difficult to understand.
Conclusion
In conclusion, the Strategy pattern is a design pattern that allows you to define a set of interchangeable behaviors for an object and switch between them at runtime. This can be useful in situations where you need to perform a similar operation in different ways depending on the context.
Some examples of where the Strategy pattern might be useful include:
- Calculating shipping costs for a shopping cart
- Handling payment methods in a payment system
- Sorting a list of items in different ways
- Changing the behavior of game characters at runtime
On the other hand, the Strategy pattern may not be the best solution in situations where you only need to perform a single operation, or where the operation is very simple and doesn’t warrant the additional complexity of the pattern.
As with any design pattern, it’s important to consider the trade-offs of using the Strategy pattern and decide whether it is the right solution for your specific problem.
Thanks for reading! If you found the article helpful, you can clap and follow. So you will notified of new articles.
# Reference
It was created with the help of ChatGPT AI.
Book of O`Reilly’s Head First Design Pattern.
Thanks for Christopher Okhravi to explain that incredibly: https://www.youtube.com/@ChristopherOkhravi