Table of Contents
- Initial problem — coupled code
- Step 1 — Create the ISalesTaxStrategy interface
- Step 2 — Implement concrete strategies
- Step 3 — Refactor Context (Order)
- Step 4 — Connect everything in Program
- Results obtained after refactoring
- Interface IInvoiceStrategy
- InvoiceStrategy base abstract class
- Concrete billing strategies
- Order Integration
- IShippingStrategy interface
- Concrete shipping strategies
- Final order — the three strategies combined
- Program.cs — dynamic selection at runtime
- Complete project structure (Demo 5)
- Advantages and trade offs of Pattern Strategy
- Summary and conclusion
1. Course Overview
This course focuses on the Strategy Pattern, one of the most commonly used design patterns. The course is designed for C# developers who want to understand how this pattern works and how to apply it in their .NET applications.
Course objectives
At the end of this course, you will be able to:
- Understand what the Strategy Pattern is and what its characteristics are
- Identify the advantages and trade-offs of using this pattern
- Implement the Strategy Pattern in new and existing solutions
- Identify and leverage existing implementations of the pattern in the .NET framework, in your own applications, and in third-party NuGet packages
Prerequisites
- Be familiar with C# syntax
- Know how to build and run .NET applications
- No prior knowledge of design patterns is required
2. Introduction to Pattern Strategy
The Strategy Pattern is probably the pattern you will encounter most often in your career as a developer. It is crucially important to know how to apply it, how to identify it and how to exploit it in your applications.
This pattern belongs to the family of behavioral design patterns (behavioral design patterns). It defines a family of algorithms, encapsulates each of them and makes them interchangeable. The Strategy Pattern allows you to change the algorithm of an object independently of the clients that use it.
2.1 The three characteristics of the pattern
The Strategy Pattern is recognized by three fundamental components:
┌──────────────────────────────────────────────────┐
│ CONTEXT │
│ (ex : Order) │
│ │
│ - Contient une référence à IStrategy │
│ - Délègue l'exécution à la stratégie active │
└────────────────────┬─────────────────────────────┘
│ référence
▼
┌──────────────────────────────────────────────────┐
│ << interface >> │
│ IStrategy │
│ │
│ + ExecuteAlgorithm(...) │
└────────────────────┬─────────────────────────────┘
│ implémente
┌──────────┼──────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ConcreteStratA│ │ConcreteStratB│ │ConcreteStratC│
│ │ │ │ │ │
│ + Execute() │ │ + Execute() │ │ + Execute() │
└──────────────┘ └──────────────┘ └──────────────┘
| Component | Role | Example in the course |
|---|---|---|
| Background | Class that has a reference to the active policy. She delegates work to this strategy without knowing the implementation details. | Order |
| Strategy (interface) | Contract defining how to interact with any concrete strategy. | ISalesTaxStrategy, IInvoiceStrategy, IShippingStrategy |
| Concrete Strategy | Concrete implementation of a particular algorithm. | SwedenSalesTaxStrategy, USAStateSalesTaxStrategy |
2.2 Concrete examples in the real world
Example 1 — Calculating sales tax
The context is the order (Order). It has a reference to ISalesTaxStrategy. It calls GetTax() on the strategy without worrying about the internal logic.
The interface ISalesTaxStrategy exposes the GetTaxFor(Order order) method.
The concrete strategies are:
SwedenSalesTaxStrategy— calculates Swedish VAT (25% overall, or by item type)USAStateSalesTaxStrategy— calculates tax based on US state
Calculating sales tax in the United States is very different from calculating VAT in Sweden. It therefore makes sense to decouple these two algorithms from the context. This is one of the main goals of any pattern: to make applications more extensible, easier to maintain, and more testable.
Example 2 — Invoice creation
Context Order has a reference to IInvoiceStrategy. The interface exposes Generate(Order order).
The concrete strategies allow the invoice to be delivered by:
- Email (
EmailInvoiceStrategy) - Text file on disk (
FileInvoiceStrategy) - Print On Demand Service (
PrintOnDemandInvoiceStrategy)
The selection of the strategy is done at execution (runtime), based on the customer’s preferences at the time of the order. The context does not need to know about the PDF, email, or print implementation. He just needs to know that the active strategy will allow him to generate an invoice.
Key Principle: The policy can be selected at runtime based on user input, without changing the context class.
3. First Look: Calculating Sales Tax
3.1 Initial problem — coupled code
The demo application is a console application simulating an order processing system. The objective is to calculate the tax on an order taking into account the destination country.
Here is the initial problem: all the tax calculation logic is coupled directly into the GetTax() method of the Order class. This approach has several flaws:
// AVANT REFACTORISATION — Code couplé dans Order
public decimal GetTax()
{
var destination = ShippingDetails.DestinationCountry.ToLowerInvariant();
if (destination == "sweden")
{
if (destination == ShippingDetails.OriginCountry.ToLowerInvariant())
{
return TotalPrice * 0.25m;
}
return 0;
}
if (destination == "us")
{
switch (ShippingDetails.DestinationState.ToLowerInvariant())
{
case "la": return TotalPrice * 0.095m;
case "ny": return TotalPrice * 0.04m;
case "nyc": return TotalPrice * 0.045m;
default: return 0m;
}
}
return 0m;
}
Problems with this approach:
- Method contains all logic for Sweden AND USA
- Adding a new country requires modifying the
Orderclass - It is difficult to write tests for each calculation individually
- The code becomes longer and longer as you add countries
- Class
Orderviolates the Single Responsibility Principle (SRP)
3.2 Step 1 — Create the ISalesTaxStrategy interface
The first step is to define the contract (the interface) for all sales tax policies. We create a Strategies/SalesTax folder in the business layer.
// Business/Strategies/SalesTax/ISalesTaxStrategy.cs
using Strategy_Pattern_First_Look.Business.Models;
namespace Strategy_Pattern_First_Look.Business.Strategies.SalesTax
{
public interface ISalesTaxStrategy
{
public decimal GetTaxFor(Order order);
}
}
This interface requires any concrete implementation to expose a GetTaxFor method which takes a command as a parameter and returns a decimal amount.
3.3 Step 2 — Implement concrete strategies
We extract the calculation code from Order to dedicated classes.
SwedenSalesTaxStrategy
// Business/Strategies/SalesTax/SwedenSalesTaxStrategy.cs
using Strategy_Pattern_First_Look.Business.Models;
namespace Strategy_Pattern_First_Look.Business.Strategies.SalesTax
{
public class SwedenSalesTaxStrategy : ISalesTaxStrategy
{
public decimal GetTaxFor(Order order)
{
var destination = order.ShippingDetails.DestinationCountry.ToLowerInvariant();
var totalPrice = order.TotalPrice;
if (destination == order.ShippingDetails.OriginCountry.ToLowerInvariant())
{
return totalPrice * 0.25m;
}
return 0;
}
// Version améliorée — taux différenciés par type d'article
public decimal GetTaxForBetter(Order order)
{
decimal totalTax = 0m;
foreach (var item in order.LineItems)
{
switch (item.Key.ItemType)
{
case ItemType.Food:
totalTax += (item.Key.Price * 0.06m) * item.Value;
break;
case ItemType.Literature:
totalTax += (item.Key.Price * 0.08m) * item.Value;
break;
case ItemType.Service:
case ItemType.Hardware:
totalTax += (item.Key.Price * 0.25m) * item.Value;
break;
}
}
return totalTax;
}
}
}
VAT rate in Sweden:
- Power supply: 6%
- Literature: 8%
- Services and equipment: 25%
USAStateSalesTaxStrategy
// Business/Strategies/SalesTax/USAStateSalesTaxStrategy.cs
using Strategy_Pattern_First_Look.Business.Models;
namespace Strategy_Pattern_First_Look.Business.Strategies.SalesTax
{
public class USAStateSalesTaxStrategy : ISalesTaxStrategy
{
public decimal GetTaxFor(Order order)
{
var totalPrice = order.TotalPrice;
switch (order.ShippingDetails.DestinationState.ToLowerInvariant())
{
case "la": return totalPrice * 0.095m;
case "ny": return totalPrice * 0.04m;
case "nyc": return totalPrice * 0.045m;
default: return 0m;
}
}
}
}
Tax rate by state (USA):
- Los Angeles (LA): 9.5%
- New York (NY): 4%
- New York City (NYC): 4.5%
3.4 Step 3 — Refactor Context (Order)
The Order class now exposes a property of type ISalesTaxStrategy. Its GetTax() method delegates the calculation to the active strategy:
// Business/Models/Order.cs — après refactorisation
public class Order
{
public Dictionary<Item, int> LineItems { get; } = new Dictionary<Item, int>();
public IList<Payment> SelectedPayments { get; } = new List<Payment>();
public IList<Payment> FinalizedPayments { get; } = new List<Payment>();
public decimal AmountDue => TotalPrice - FinalizedPayments.Sum(payment => payment.Amount);
public decimal TotalPrice => LineItems.Sum(item => item.Key.Price * item.Value);
public ShippingStatus ShippingStatus { get; set; } = ShippingStatus.WaitingForPayment;
public ShippingDetails ShippingDetails { get; set; }
// Propriété exposant la stratégie — le cœur du pattern
public ISalesTaxStrategy SalesTaxStrategy { get; set; }
public decimal GetTax()
{
if (SalesTaxStrategy == null)
{
return 0m; // ou throw new InvalidOperationException()
}
return SalesTaxStrategy.GetTaxFor(this);
}
}
Important point: The Order class knows nothing about calculation logic. She delegates this responsibility entirely to the strategy assigned to her. It is the total decoupling between context and concrete implementations.
3.5 Step 4 — Connect everything in Program
At the entry point of the application, we assign the appropriate strategy depending on the destination of the command:
// Program.cs — après refactorisation
using Strategy_Pattern_First_Look.Business.Models;
using Strategy_Pattern_First_Look.Business.Strategies.SalesTax;
using System;
namespace Strategy_Pattern_First_Look
{
class Program
{
static void Main(string[] args)
{
var order = new Order
{
ShippingDetails = new ShippingDetails
{
OriginCountry = "Sweden",
DestinationCountry = "Sweden"
},
// Sélection de la stratégie basée sur la destination
SalesTaxStrategy = new SwedenSalesTaxStrategy()
};
// Ajout d'articles de différents types
order.LineItems.Add(
new Item("CSHARP_SMORGASBORD", "C# Smorgasbord", 100m, ItemType.Literature), 1);
Console.WriteLine($"Total taxe : {order.GetTax()} SEK");
}
}
}
Result: With
SwedenSalesTaxStrategy(rate per item type), the total tax is 33 Swedish crowns instead of 50 with the fixed rate of 25%.
3.6 Results obtained after refactoring
By applying the Strategy Pattern, we obtained:
| Before | After |
|---|---|
Calculation logic coupled in Order | Each strategy is in its own class |
| Difficult to test (everything is mixed) | Each strategy can be tested in isolation |
| Changing Sweden’s logic risks affecting the USA | Policy-isolated changes |
Add country = modify Order | Add a country = create a new class |
Order code grows indefinitely | Order keeps it simple and clean |
Key Benefits:
- Extensibility: you can add new strategies without modifying existing ones
- Single responsibility: each strategy is responsible for a single algorithm
- Testability: we can test
SwedenSalesTaxStrategyindependently ofUSAStateSalesTaxStrategy - Decoupling: the
Ordercontext class is decoupled from concrete implementations
4. Alternative approach: Pass the strategy as a method parameter
So far the strategy is exposed as a property on the context (Order.SalesTaxStrategy). The Strategy pattern also offers an alternative approach: pass the strategy directly into method parameter.
public decimal GetTax(ISalesTaxStrategy strategy = null)
{
// Utilise la stratégie passée en paramètre, ou celle définie sur la propriété
var activeStrategy = strategy ?? SalesTaxStrategy;
if (activeStrategy == null)
{
return 0m;
}
return activeStrategy.GetTaxFor(this);
}
Usage:
// Avec la stratégie par défaut (définie sur la propriété)
var tax1 = order.GetTax();
// En passant une stratégie différente pour ce calcul spécifique
var tax2 = order.GetTax(new SwedenSalesTaxStrategy());
var tax3 = order.GetTax(new USAStateSalesTaxStrategy());
Important implication: This approach demonstrates that each time a method accepts an interface as a parameter, it essentially exploits the Strategy Pattern. This means that you do not have to name your strategies with the suffix “Strategy” to exploit this pattern. You probably already use it in your applications without knowing it.
Conclusion: This pattern is extremely powerful because it allows you to change the behavior of the
Orderobject for tax calculation at any time during runtime, depending on the behavior of the application.
5. Example: Creating invoices (Invoice)
This example illustrates how to introduce the Strategy Pattern from the beginning for a new feature, rather than refactoring existing code.
The goal is to allow the application to send invoices in different ways:
- By email (
EmailInvoiceStrategy) - As file on disk (
FileInvoiceStrategy) - Via a print-on-demand service (
PrintOnDemandInvoiceStrategy)
Best practice: Introducing the Strategy Pattern from the start of a new feature facilitates future extension. In 6 months, it will be easy to add new delivery methods (SMS, push notification, etc.) without changing existing code.
5.1 IInvoiceStrategy Interface
// Business/Strategies/Invoice/IInvoiceStrategy.cs
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
public interface IInvoiceStrategy
{
public void Generate(Order order);
}
}
The Generate method returns nothing (void) because depending on the implementation, it can send an email, call an API or write to the file system — no return is necessary.
5.2 InvoiceStrategy base abstract class
All billing policies share a common functionality: generating invoice text. We therefore create a basic abstract class to avoid code duplication.
// Business/Strategies/Invoice/InvoiceStrategy.cs
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
using System;
namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
public abstract class InvoiceStrategy : IInvoiceStrategy
{
// Méthode abstraite — chaque sous-classe doit l'implémenter
public abstract void Generate(Order order);
// Fonctionnalité partagée : génère la représentation textuelle de la facture
public string GenerateTextInvoice(Order order)
{
var invoice = $"INVOICE DATE: {DateTimeOffset.Now}{Environment.NewLine}{Environment.NewLine}";
invoice += $"ID|NAME|PRICE|QUANTITY{Environment.NewLine}";
foreach (var item in order.LineItems)
{
invoice += $"{item.Key.Id}|{item.Key.Name}|{item.Key.Price}|{item.Value}{Environment.NewLine}";
}
invoice += Environment.NewLine + Environment.NewLine;
var tax = order.GetTax(); // Utilise la SalesTaxStrategy de l'Order !
var total = order.TotalPrice + tax;
invoice += $"TAX TOTAL: {tax}{Environment.NewLine}";
invoice += $"TOTAL: {total}{Environment.NewLine}";
return invoice;
}
}
}
Interesting observation: The
GenerateTextInvoicemethod callsorder.GetTax(), which itself uses theSalesTaxStrategy. We therefore have an aggregation of strategies: the invoicing strategy depends indirectly on the tax strategy.
Hierarchy structure:
IInvoiceStrategy (interface)
└── InvoiceStrategy (abstract class — partage GenerateTextInvoice)
├── EmailInvoiceStrategy
└── FileInvoiceStrategy
IInvoiceStrategy (interface)
└── PrintOnDemandInvoiceStrategy (implémente directement l'interface — pas de texte)
5.3 Concrete billing strategies
EmailInvoiceStrategy
Send the invoice by email via SmtpClient (here with SendGrid):
// Business/Strategies/Invoice/EmailInvoiceStrategy.cs
using System;
using System.Net;
using System.Net.Mail;
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
public class EmailInvoiceStrategy : InvoiceStrategy
{
public override void Generate(Order order)
{
using (SmtpClient client = new SmtpClient("smtp.sendgrid.net", 587))
{
NetworkCredential credentials = new NetworkCredential("USERNAME", "PASSWORD");
client.Credentials = credentials;
MailMessage mail = new MailMessage("YOUR EMAIL", "YOUR EMAIL")
{
Subject = "We've created an invoice for your order",
Body = GenerateTextInvoice(order) // Utilise la méthode de la classe de base
};
client.Send(mail);
Console.WriteLine("Invoice for order sent over e-mail");
}
}
}
}
FileInvoiceStrategy
Writes the invoice to a text file on local disk:
// Business/Strategies/Invoice/FileInvoiceStrategy.cs
using System;
using System.IO;
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
public class FileInvoiceStrategy : InvoiceStrategy
{
public override void Generate(Order order)
{
var filename = $"invoice_{Guid.NewGuid()}.txt";
using (var stream = new StreamWriter(filename))
{
stream.Write(GenerateTextInvoice(order)); // Utilise la méthode de la classe de base
stream.Flush();
Console.WriteLine($"Invoice for order saved to {filename}");
}
}
}
}
PrintOnDemandInvoiceStrategy
Calls an external HTTP API to print and mail the invoice. This strategy does not inherit from the abstract class InvoiceStrategy because it does not need the textual representation — it sends the serialized object directly to the API:
// Business/Strategies/Invoice/PrintOnDemandInvoiceStrategy.cs
using System;
using System.Net.Http;
using Newtonsoft.Json;
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice
{
public class PrintOnDemandInvoiceStrategy : IInvoiceStrategy
{
public void Generate(Order order)
{
using (var client = new HttpClient())
{
var content = JsonConvert.SerializeObject(order);
client.BaseAddress = new Uri("https://pluralsight.com");
client.PostAsync("/print-on-demand", new StringContent(content));
Console.WriteLine($"Invoice sent for printing");
}
}
}
}
Note: The
/print-on-demandAPI is purely fictitious in the context of this course. It illustrates the concept of integration with an external “print-on-demand” service.
5.4 Integration into the order (Order)
The Order class now exposes an IInvoiceStrategy property and a FinalizeOrder() method:
// Dans Order.cs — ajout de la stratégie de facturation
public IInvoiceStrategy InvoiceStrategy { get; set; }
public void FinalizeOrder()
{
if (SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Invoice) &&
AmountDue > 0 &&
ShippingStatus == ShippingStatus.WaitingForPayment)
{
// Génère la facture selon la stratégie définie
InvoiceStrategy.Generate(this);
ShippingStatus = ShippingStatus.ReadyForShippment;
}
else if (AmountDue > 0)
{
throw new Exception("Unable to finalize order");
}
// ... puis appelle la stratégie d'expédition
}
Use in program:
var order = new Order
{
ShippingDetails = new ShippingDetails { ... },
SalesTaxStrategy = new SwedenSalesTaxStrategy(),
InvoiceStrategy = new FileInvoiceStrategy() // ou EmailInvoiceStrategy, etc.
};
// Finalise la commande — génère la facture selon la stratégie
order.FinalizeOrder();
Future extensibility: Simply create a new class implementing IInvoiceStrategy to add a delivery method (e.g. SmsInvoiceStrategy). The Order class and all other strategies remain unchanged.
6. Example: Different carriers (Shipping Providers)
This example adds the management of different carriers. When an order is finalized, we want to ship it via the carrier chosen by the user: DHL, FedEx, PostNord (Swedish Post), UPS, or USPS (US Post).
6.1 IShippingStrategy Interface
// Business/Strategies/Shipping/IShippingStrategy.cs
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
namespace Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Shipping
{
public interface IShippingStrategy
{
void Ship(Order order);
}
}
6.2 Concrete shipping strategies
DhlShippingStrategy
public class DhlShippingStrategy : IShippingStrategy
{
public void Ship(Order order)
{
using (var client = new HttpClient())
{
// TODO: Implement DHL Shipping Integration
Console.WriteLine("Order is shipped with DHL");
}
}
}
FedexShippingStrategy
public class FedexShippingStrategy : IShippingStrategy
{
public void Ship(Order order)
{
using (var client = new HttpClient())
{
// TODO: Implement Fedex Shipping Integration
Console.WriteLine("Order is shipped with Fedex");
}
}
}
SwedishPostalServiceShippingStrategy (PostNord)
public class SwedishPostalServiceShippingStrategy : IShippingStrategy
{
public void Ship(Order order)
{
using (var client = new HttpClient())
{
// TODO: Implement PostNord Shipping Integration
Console.WriteLine("Order is shipped with PostNord");
}
}
}
UnitedStatesPostalServiceShippingStrategy (USPS)
public class UnitedStatesPostalServiceShippingStrategy : IShippingStrategy
{
public void Ship(Order order)
{
using (var client = new HttpClient())
{
// TODO: Implement USPS Shipping Integration
Console.WriteLine("Order is shipped with USPS");
}
}
}
UpsShippingStrategy
public class UpsShippingStrategy : IShippingStrategy
{
public void Ship(Order order)
{
using (var client = new HttpClient())
{
// TODO: Implement UPS Shipping Integration
Console.WriteLine("Order is shipped with UPS");
}
}
}
Observation: Each carrier communicates with a different API via HTTP. The Strategy Pattern is particularly beneficial here because each API has its own requirements (authentication, data format, package weight, etc.). These details remain encapsulated in each policy.
6.3 Final order — the three strategies combined
The Order class now exposes three strategies:
// Business/Models/Order.cs — version finale complète
public class Order
{
public Dictionary<Item, int> LineItems { get; } = new Dictionary<Item, int>();
public IList<Payment> SelectedPayments { get; } = new List<Payment>();
public IList<Payment> FinalizedPayments { get; } = new List<Payment>();
public decimal AmountDue => TotalPrice - FinalizedPayments.Sum(payment => payment.Amount);
public decimal TotalPrice => LineItems.Sum(item => item.Key.Price * item.Value);
public ShippingStatus ShippingStatus { get; set; } = ShippingStatus.WaitingForPayment;
public ShippingDetails ShippingDetails { get; set; }
// Les trois stratégies
public ISalesTaxStrategy SalesTaxStrategy { get; set; }
public IInvoiceStrategy InvoiceStrategy { get; set; }
public IShippingStrategy ShippingStrategy { get; set; }
public decimal GetTax()
{
if (SalesTaxStrategy == null)
{
return 0m;
}
return SalesTaxStrategy.GetTaxFor(this);
}
public void FinalizeOrder()
{
if (SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Invoice) &&
AmountDue > 0 &&
ShippingStatus == ShippingStatus.WaitingForPayment)
{
InvoiceStrategy.Generate(this);
ShippingStatus = ShippingStatus.ReadyForShippment;
}
else if (AmountDue > 0)
{
throw new Exception("Unable to finalize order");
}
// Expédition via la stratégie choisie
ShippingStrategy.Ship(this);
}
}
Associated data models:
public class ShippingDetails
{
public string Receiver { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string PostalCode { get; set; }
public string DestinationCountry { get; set; }
public string DestinationState { get; set; }
public string OriginCountry { get; set; }
public string OriginState { get; set; }
}
public enum ShippingStatus
{
WaitingForPayment,
ReadyForShippment,
Shipped
}
public enum PaymentProvider
{
Paypal,
CreditCard,
Invoice
}
public class Payment
{
public decimal Amount { get; set; }
public PaymentProvider PaymentProvider { get; set; }
}
public class Item
{
public string Id { get; }
public string Name { get; }
public decimal Price { get; }
public ItemType ItemType { get; set; }
public Item(string id, string name, decimal price, ItemType type)
{
Id = id;
Name = name;
Price = price;
ItemType = type;
}
}
public enum ItemType
{
Service,
Food,
Hardware,
Literature
}
6.4 Program.cs — dynamic selection at runtime
In the final application (Demo 5), the program asks the user to choose their preferences and dynamically selects the appropriate strategies:
// Program.cs — sélection interactive des stratégies
// (version Array.Sort pour démonstration finale)
using Strategy_Pattern_Using_different_shipping_providers.Business.Models;
using Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Invoice;
using Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.SalesTax;
using Strategy_Pattern_Using_different_shipping_providers.Business.Strategies.Shipping;
using System;
using System.Collections.Generic;
namespace Strategy_Pattern_Using_different_shipping_providers
{
class Program
{
static void Main(string[] args)
{
// Scénario : sélection d'une stratégie selon l'origine
var orders = new[] {
new Order { ShippingDetails = new ShippingDetails { OriginCountry = "Sweden" } },
new Order { ShippingDetails = new ShippingDetails { OriginCountry = "USA" } },
new Order { ShippingDetails = new ShippingDetails { OriginCountry = "Sweden" } },
new Order { ShippingDetails = new ShippingDetails { OriginCountry = "USA" } },
new Order { ShippingDetails = new ShippingDetails { OriginCountry = "Singapore" } }
};
Print(orders);
Console.WriteLine();
Console.WriteLine("Sorting..");
Console.WriteLine();
// Array.Sort avec une stratégie de comparaison (IComparer)
Array.Sort(orders, new OrderAmountComparer());
Print(orders);
}
static void Print(IEnumerable<Order> orders)
{
foreach (var order in orders)
{
Console.WriteLine(order.ShippingDetails.OriginCountry);
}
}
}
}
Strategy selection logic (interactive version):
// Sélection de la SalesTaxStrategy selon l'origine
ISalesTaxStrategy GetSalesTaxStrategy(string origin, string destination)
{
if (origin.ToLower() == "sweden")
return new SwedenSalesTaxStrategy();
if (origin.ToLower() == "usa")
return new USAStateSalesTaxStrategy();
throw new NotSupportedException($"No strategy for country: {origin}");
}
// Sélection de l'InvoiceStrategy selon le choix utilisateur
IInvoiceStrategy GetInvoiceStrategy(int invoiceOption)
{
switch (invoiceOption)
{
case 1: return new EmailInvoiceStrategy();
case 2: return new FileInvoiceStrategy();
case 3: return new PrintOnDemandInvoiceStrategy();
default: throw new NotSupportedException($"Unknown option: {invoiceOption}");
}
}
// Sélection de la ShippingStrategy selon le transporteur choisi
IShippingStrategy GetShippingStrategy(int provider)
{
switch (provider)
{
case 1: return new SwedishPostalServiceShippingStrategy(); // PostNord
case 2: return new DhlShippingStrategy();
case 3: return new UnitedStatesPostalServiceShippingStrategy(); // USPS
case 4: return new FedexShippingStrategy();
case 5: return new UpsShippingStrategy();
default: throw new NotSupportedException($"Unknown provider: {provider}");
}
}
Execution example (Sweden → Sweden, PostNord, invoice on file):
Total tax: 25 SEK
Order is shipped with PostNord
Invoice for order saved to invoice_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.txt
7. Existing implementation in .NET: Array.Sort
The final module of the course illustrates that the Strategy Pattern is already present everywhere in the .NET framework. The example uses Array.Sort with an IComparer<T>.
7.1 IComparer<T> as comparison strategy
Array.Sort is a method that accepts an IComparer<T> as an optional parameter. This is precisely the Strategy Pattern: we inject a comparison strategy to modify the sorting behavior at runtime.
// Stratégie de comparaison par montant total
public class OrderAmountComparer : IComparer<Order>
{
public int Compare(Order x, Order y)
{
var xTotal = x.TotalPrice;
var yTotal = y.TotalPrice;
if (xTotal == yTotal) return 0;
else if (xTotal > yTotal) return 1;
return -1;
}
}
// Stratégie de comparaison par pays d'origine (première lettre)
public class OrderOriginComparer : IComparer<Order>
{
public int Compare(Order x, Order y)
{
var xDest = x.ShippingDetails.OriginCountry.ToLowerInvariant();
var yDest = y.ShippingDetails.OriginCountry.ToLowerInvariant();
if (xDest == yDest) return 0;
else if (xDest[0] > yDest[0]) return 1;
return -1;
}
}
Usage:
// Tri par montant
Array.Sort(orders, new OrderAmountComparer());
// Tri par pays d'origine
Array.Sort(orders, new OrderOriginComparer());
Result of alphabetical sorting (by first letter of country of origin):
Avant tri: Sweden, USA, Sweden, USA, Singapore
Après tri: Singapore, Sweden, Sweden, USA, USA
Key Insight: Even though the code does not use the words “Strategy” or “strategy”, it is indeed the Strategy Pattern. Whenever you pass an interface to a method to modify its behavior, you are using the Strategy Pattern.
Other examples of the Strategy Pattern in .NET
The pattern is omnipresent in the .NET framework:
| .NET Component | Strategy interface | Behavior replaced |
|---|---|---|
Array.Sort() | ICompare<T> | Comparison logic |
LINQ OrderBy() | ICompare<T> | Sorting criteria |
HttpClient | HttpMessageHandler | Sending HTTP requests |
| Dependency Injection | Any interface | Implementing a service |
IEqualityComparer<T> | IEqualityComparer<T> | Equality and hash |
8. Complete project structure (Demo 5)
Here is the complete architecture of the final project (Demo 5 — Array.Sort, full version):
Strategy_Pattern_Using_different_shipping_providers/
│
├── Program.cs
│
└── Business/
├── Models/
│ └── Order.cs ← Contexte (Context)
│ ├── Item.cs (interne)
│ ├── ShippingDetails.cs (interne)
│ ├── Payment.cs (interne)
│ └── Enums: ShippingStatus, PaymentProvider, ItemType
│
└── Strategies/
├── SalesTax/
│ ├── ISalesTaxStrategy.cs ← Interface
│ ├── SwedenSalesTaxStrategy.cs ← Stratégie concrète
│ └── USAStateSalesTaxStrategy.cs ← Stratégie concrète
│
├── Invoice/
│ ├── IInvoiceStrategy.cs ← Interface
│ ├── InvoiceStrategy.cs ← Classe abstraite de base
│ ├── EmailInvoiceStrategy.cs ← Stratégie concrète
│ ├── FileInvoiceStrategy.cs ← Stratégie concrète
│ └── PrintOnDemandInvoiceStrategy.cs ← Stratégie concrète
│
└── Shipping/
├── IShippingStrategy.cs ← Interface
├── DhlShippingStrategy.cs ← Stratégie concrète
├── FedexShippingStrategy.cs ← Stratégie concrète
├── SwedishPostalServiceShippingStrategy.cs ← Stratégie concrète
├── UnitedStatesPostalServiceShippingStrategy.cs ← Stratégie concrète
└── UpsShippingStrategy.cs ← Stratégie concrète
Simplified class diagram:
Order (Context)
├── ISalesTaxStrategy ─── SwedenSalesTaxStrategy
│ └── USAStateSalesTaxStrategy
│
├── IInvoiceStrategy ─── InvoiceStrategy (abstract)
│ │ ├── EmailInvoiceStrategy
│ │ └── FileInvoiceStrategy
│ └── PrintOnDemandInvoiceStrategy
│
└── IShippingStrategy ─── DhlShippingStrategy
├── FedexShippingStrategy
├── SwedishPostalServiceShippingStrategy
├── UnitedStatesPostalServiceShippingStrategy
└── UpsShippingStrategy
9. Advantages and trade offs of Pattern Strategy
Advantages
1. Extensibility without modification
It is possible to add new policies without modifying existing policies or the context. This principle is consistent with the Open/Closed principle (open to extension, closed to modification).
Ajouter GermanyVATStrategy ?
→ Créer la classe, implémenter ISalesTaxStrategy
→ Rien d'autre à modifier !
2. Single Responsibility Principle (SRP)
Each strategy is responsible for a single algorithm. The Order class is only responsible for managing the order.
3. Improved testability
Each strategy can be tested totally isolated:
// Test unitaire de SwedenSalesTaxStrategy
[Test]
public void SwedenSalesTaxStrategy_ShouldCalculateCorrectTax()
{
var strategy = new SwedenSalesTaxStrategy();
var order = new Order { /* ... */ };
var tax = strategy.GetTaxFor(order);
Assert.AreEqual(25m, tax);
}
We can also inject mocked strategies to test the context:
// Mock de la stratégie pour tester Order
var mockStrategy = new Mock<ISalesTaxStrategy>();
mockStrategy.Setup(s => s.GetTaxFor(It.IsAny<Order>())).Returns(10m);
var order = new Order { SalesTaxStrategy = mockStrategy.Object };
Assert.AreEqual(10m, order.GetTax());
4. Dynamic selection at runtime
The active policy can be chosen based on user behavior, database data, configuration, or any other runtime criteria.
5. Cleaner code in context
The Order class remains simple and readable. She delegates complex algorithms to strategies and focuses on her role: managing an order.
Compromise
1. Increased complexity
Introducing Strategy Pattern creates more classes and files. For simple algorithms that are not likely to change, this may be over-engineering.
2. Clients should be aware of available policies
The caller (the Program, a service, a controller) must know and instantiate the correct strategy. This can be mitigated by dependency injection or a Factory pattern.
3. Additional indirection
There is a layer of indirection between the caller and the actual algorithm. This can make debugging slightly more complex for developers unfamiliar with the pattern.
General rule: Always make sure that the code you add makes working with the application easier and does not complicate it unnecessarily. The Strategy Pattern is an excellent tool, but like any pattern, it must be applied thoughtfully.
10. Summary and conclusion
What you learned
This course covered the following aspects of the Strategy Pattern:
| Concept | Description |
|---|---|
| Definition | Behavioral pattern allowing to define a family of interchangeable algorithms |
| Components | Context, Interface (Strategy), Concrete Strategies |
| Refactoring | How to extract coupled algorithms to decoupled strategies |
| New feature | How to start a new feature by applying the pattern from the start |
| Alternative approach | Pass strategy as method parameter |
| In .NET | Array.Sort + IComparer<T> is an implementation of the Strategy Pattern |
Key Takeaways
-
The Strategy Pattern is everywhere. If you’ve ever written code that accepts an interface to change a behavior, you’ve used the Strategy Pattern — even without naming it.
-
Context does not know details. The context class (
Order) knows nothing about how the tax is calculated, how the invoice is generated, or how the package is shipped. She delegates everything to strategies. -
Names are not required. You are not required to name your classes with the “Strategy” suffix. If you accept an interface to replace a behavior, that’s Strategy Pattern.
-
Strong coupling is the enemy. Coupling algorithms directly in context makes code difficult to test, difficult to extend, and difficult to maintain.
-
Start with the interface. When implementing a new feature that might have multiple implementations, start by defining the interface. This forces you to think about the contract before the implementation details.
Pattern application checklist
Before applying the Strategy Pattern, ask yourself these questions:
- Are there several variations of the same algorithm?
- Should the active algorithm be able to change at runtime?
- Is the algorithm code bloating in the context class?
- Do you want to isolate the algorithm logic for easier testing?
- Do you plan to add new variations of the algorithm in the future?
If you answered yes to one or more of these questions, the Strategy Pattern is probably the right approach.
Application in a real project
Contexte réel → Appliquer Strategy Pattern
Calcul de prix/taxes par région → ITaxStrategy
Envoi de notifications → INotificationStrategy (Email, SMS, Push)
Export de données → IExportStrategy (CSV, Excel, PDF)
Authentification → IAuthStrategy (JWT, OAuth, Basic)
Paiement → IPaymentStrategy (Stripe, PayPal, Virement)
Mise en cache → ICacheStrategy (Memory, Redis, File)
Compression de fichiers → ICompressionStrategy (ZIP, GZIP, LZ4)
“My name is Filip Ekberg, and thank you so much for taking this course. We’ve covered this amazing pattern and how to apply that in your applications. Hopefully by now, you get some ideas of how you can refactor your applications, and this will take your development experience to a whole new level.”
Search Terms
c-sharp · design · patterns · strategy · testing · architecture · c# · .net · development · pattern · concrete · strategies · interface · order · advantages · application · calculating · context · dynamic · invoice · runtime · sales · selection · shipping