Imagine you are developing the checkout page for an online store where you sell goods and services, and you have been instructed to include a feature that will let customers select from a variety of shipping options. Each shipping method may use a different logic to determine shipping costs, and there may be instances where various developers must use various algorithms. Writing a single class that contains all the calculations and logic would be a bad idea. You must divide your code in a way that makes it simple for various developers to maintain it and simple to scale should more shipping methods ever be added. In this tutorial, I’ll demonstrate how to use a design pattern called the Strategy Pattern to implement several algorithms independently and switch between them as needed.
What is a Strategy Pattern?
A behavioral design pattern called the strategy pattern (also referred to as the policy pattern) enables us to implement a family of algorithms (or strategies) into distinct classes and then switch from one algorithm (or strategy) to another during runtime. Developers can isolate the code, internal logic, and dependencies of different algorithms using this pattern, making the application code simple to scale and maintain. By switching from one algorithm to another at runtime, this pattern also enables us to change the behavior of the application.
Pros of Strategy Pattern
- Different algorithms’ implementation specifics can be separated from the business logic that employs them.
- At runtime, we can change from one algorithm to another.
- Algorithms can be added or removed without changing the context.
Cons of Strategy Pattern
- You don’t need to overcomplicate your code by adding new classes and interfaces that go along with this pattern if you only have a few algorithms that rarely change.
- To choose the most appropriate strategy, the client needs to understand how various strategies differ from one another.
Setting Up an ASP.NET Core Demo App
When dealing with multiple algorithms, strategy patterns can be used in a variety of real-world use cases, including
- An online store checkout page with various shipping options.
- A checkout page for an online store that accepts various forms of payment.
- A program that produces images in a variety of formats.
- A program that authenticates users through OpenID, OAuth, and Basic authentication.
In order to demonstrate how a Strategy pattern can help us switch from one shipping method to another shipping method dynamically, I’ll implement a sample eCommerce checkout page that will let the user select one of the shipping methods.
Create the ShippingMethod class below in a new ASP.NET Core MVC 5 Web Application.
ShippingMethod.cs
public
class
ShippingMethod
{
public
int
Id {
get
;
set
; }
public
string
Name {
get
;
set
; }
}
public
class
CheckoutModel
{
public
int
SelectedMethod {
get
;
set
; }
public
decimal
OrderTotal {
get
;
set
; }
public
decimal
FinalTotal {
get
;
set
; }
public
List<ShippingMethod> ShippingMethods {
get
;
set
; }
}
private
List<ShippingMethod> GetShippingMethods()
{
return
new
List<ShippingMethod>()
{
new
ShippingMethod()
{
Id = 1,
Name=
"Free Shipping ($0.00)"
},
new
ShippingMethod() {
Id = 2,
Name=
"Local Shipping ($10.00)"
},
new
ShippingMethod() {
Id = 3,
Name=
"Worldwide Shipping ($50.00)"
}
};
}
private
CheckoutModel GetOrderDetails()
{
var
model =
new
CheckoutModel()
{
OrderTotal = 100.00m,
ShippingMethods = GetShippingMethods()
};
return
model;
}
public
IActionResult Index()
{
var
model = GetOrderDetails();
return
View(model);
}
@model CheckoutModel
@{
ViewData["Title"] = "Home Page";
}
<
br
/>
<
div
>
<
h3
class
=
"display-4 m-3 text-center"
>Checkout</
h3
>
<
br
/>
@using (Html.BeginForm("Index", "Home", null, FormMethod.Post))
{
@Html.HiddenFor(x => x.OrderTotal);
<
table
cellpadding
=
"0"
cellspacing
=
"0"
class
=
"table"
>
<
tr
>
<
th
>Order Total: </
th
>
</
tr
>
<
tr
>
<
th
>Shipping Method: </
th
>
<
td
>
@Html.DropDownListFor(x => x.SelectedMethod,
new SelectList(Model.ShippingMethods, "Id", "Name"),
"- Select -",
new { @class = "form-control" })
</
td
>
<
td
class
=
"text-left"
>
<
input
class
=
"btn btn-primary"
type
=
"submit"
value
=
"Calculate"
/>
</
td
>
</
tr
>
@if (Model.FinalTotal > 0)
{
<
tr
>
<
th
>Final Total: </
th
>
</
tr
>
}
</
table
>
}
</
div
>
Getting Started with Strategy Pattern
Declaring a common interface that will be implemented by all the algorithms is the first step in putting the Strategy pattern into practice. Let’s call the interface for our test app the IShippingStrategy interface. The interface only has one method, CalculateFinalTotal, that receives as a parameter the orderTotal and returns the final sum.
IShippingStragegy.cs
public
interface
IShippingStrategy
{
decimal
CalculateFinalTotal(
decimal
orderTotal);
}
public
class
FreeShippingStrategy : IShippingStrategy
{
public
decimal
CalculateFinalTotal(
decimal
orderTotal)
{
return
orderTotal;
}
}
public
class
LocalShippingStrategy : IShippingStrategy
{
public
decimal
CalculateFinalTotal(
decimal
orderTotal)
{
return
orderTotal + 10;
}
}
WorldwideShippingStrategy.cs
public
class
WorldwideShippingStrategy : IShippingStrategy
{
public
decimal
CalculateFinalTotal(
decimal
orderTotal)
{
return
orderTotal + 50;
}
}
Continue implementing the Strategy pattern by declaring the IShippingContext interface below.
IShippingContext.cs
public
interface
IShippingContext
{
void
SetStrategy(IShippingStrategy strategy);
decimal
ExecuteStrategy(
decimal
orderTotal);
}
public
class
ShippingContext : IShippingContext
{
private
IShippingStrategy _strategy;
public
ShippingContext()
{ }
public
ShippingContext(IShippingStrategy strategy)
{
this
._strategy = strategy;
}
public
void
SetStrategy(IShippingStrategy strategy)
{
this
._strategy = strategy;
}
public
decimal
ExecuteStrategy(
decimal
orderTotal)
{
return
this
._strategy.CalculateFinalTotal(orderTotal);
}
}
Using Strategy Pattern in ASP.NET Core
With the help of the aforementioned ShippingContext class, we are now prepared to use various shipping algorithms in our HomeController, but first we must register it with the.NET Core DI Container. This can be done by including the following line in the Startup.cs file’s ConfigureService method.
public
void
ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddScoped<IShippingContext, ShippingContext>();
}
The IShippingContext must then be added to our HomeController.
public
class
HomeController : Controller
{
private
readonly
IShippingContext _shippingContext;
public
HomeController(IShippingContext shippingContext)
{
_shippingContext = shippingContext;
}
}
The Index action method, which will be invoked when the user clicks the Calculate button, can now be put into practice. Take note of how we are utilizing the SetStrategy method to associate the proper strategy with the user-selected shipping method. Once the strategy is chosen, the ExecuteStrategy will automatically determine the final cost using the appropriate shipping strategy.
[HttpPost]
public IActionResult Index(CheckoutModel model)
{
model.ShippingMethods = GetShippingMethods();
switch (model.SelectedMethod)
{
case 1:
_shippingContext.SetStrategy(new FreeShippingStrategy());
break;
case 2:
_shippingContext.SetStrategy(new LocalShippingStrategy());
break;
case 3:
_shippingContext.SetStrategy(new WorldwideShippingStrategy());
break;
}
model.FinalTotal = _shippingContext.ExecuteStrategy(model.OrderTotal);
return View(model);
}
Start the demo app and experiment with calculating the final sum by choosing various shipping options. Your final order total will reflect this.
The following is the complete source code of our HomeController.
HomeController.cs
public
class
HomeController : Controller
{
private
readonly
IShippingContext _shippingContext;
public
HomeController(IShippingContext shippingContext)
{
_shippingContext = shippingContext;
}
public
IActionResult Index()
{
var
model = GetOrderDetails();
return
View(model);
}
[HttpPost]
public
IActionResult Index(CheckoutModel model)
{
model.ShippingMethods = GetShippingMethods();
switch
(model.SelectedMethod)
{
case
1:
_shippingContext.SetStrategy(
new
FreeShippingStrategy());
break
;
case
2:
_shippingContext.SetStrategy(
new
LocalShippingStrategy());
break
;
case
3:
_shippingContext.SetStrategy(
new
WorldwideShippingStrategy());
break
;
}
model.FinalTotal = _shippingContext.ExecuteStrategy(model.OrderTotal);
return
View(model);
}
private
CheckoutModel GetOrderDetails()
{
var
model =
new
CheckoutModel()
{
OrderTotal = 100.00m,
ShippingMethods = GetShippingMethods()
};
return
model;
}
private
List<ShippingMethod> GetShippingMethods()
{
return
new
List<ShippingMethod>()
{
new
ShippingMethod()
{
Id = 1,
Name=
"Free Shipping ($0.00)"
},
new
ShippingMethod() {
Id = 2,
Name=
"Local Shipping ($10.00)"
},
new
ShippingMethod() {
Id = 3,
Name=
"Worldwide Shipping ($50.00)"
}
};
}
}
Summary
We can create software that is more effective, readable, and simple to maintain by using strategy patterns. It enables us to build software with reusable and interchangeable parts that can be dynamically added to or taken away at runtime. I hope you’ve enjoyed this tutorial and the example I used to clearly explain this pattern to everyone.
Andriy Kravets is writer and experience .NET developer and like .NET for regular development. He likes to build cross-platform libraries/software with .NET.