Introduction: Why Strategy Matters in Navigation Apps
Ever wondered how navigation apps instantly switch between driving, walking, and cycling routes? Behind the scenes, a clever design pattern helps them do this seamlessly: the Strategy Pattern.
When building a navigation app, you need to calculate the fastest route between two points, but different modes of transport, either driving, walking, cycling, or public transit; each have their own rules. Traffic conditions, speed limits, and accessibility all play a role. Hardcoding all these route calculations in a single class leads to bloated and difficult-to-maintain code.
The Strategy Pattern provides an elegant way to handle this problem by defining a family of interchangeable algorithms, encapsulating each one in a separate class, and allowing dynamic selection at runtime.
Let’s explore how this works in C#.
The Problem: Tight Coupling and Code Duplication
Initial Approach: Hardcoding Route Calculation
Imagine we start with just a driving route calculation:
public class NavigationApp
{
public List<string> CalculateDrivingRoute(string start, string end)
{
Console.WriteLine("Calculating driving route...");
return new List<string> { $"Start at {start}", "Drive on Highway 101", $"Arrive at {end}" };
}
}
This works fine, but what happens when we need to add walking directions?
public class NavigationApp
{
// Existing driving method...
public List<string> CalculateWalkingRoute(string start, string end)
{
Console.WriteLine("Calculating walking route...");
return new List<string> { $"Start at {start}", "Walk through the park", $"Arrive at {end}" };
}
}
As we keep adding methods for cycling and public transport, the NavigationApp class becomes cluttered, difficult to maintain, and tightly coupled to specific route calculations. Every new feature requires modifying the class, violating the Open/Closed Principle.
Here’s how you would use this approach in a Program.cs
file:
public class Program
{
public static void Main(string[] args)
{
NavigationApp app = new NavigationApp();
List<string> drivingRoute = app.CalculateDrivingRoute("Home", "Work");
Console.WriteLine("Driving Route:");
foreach (var step in drivingRoute)
{
Console.WriteLine(step);
}
List<string> walkingRoute = app.CalculateWalkingRoute("Home", "Work");
Console.WriteLine("\nWalking Route:");
foreach (var step in walkingRoute)
{
Console.WriteLine(step);
}
}
}
(Take a look at this unrefactored code here)
This approach is functional but lacks flexibility and scalability. We need a better approach.
The Strategy Pattern: A Better Approach
The Strategy Pattern solves this by defining a common interface for route calculation strategies, ensuring each method is encapsulated in its own class.
Step 1: Define the Strategy Interface
public interface IRouteCalculationStrategy
{
List<string> CalculateRoute(string start, string end);
double CalculateEstimatedTime(string start, string end);
double CalculateEstimatedCost(string start, string end);
}
Step 2: Implement Different Route Strategies
Now, we create concrete implementations for driving, walking, and public transport:
public class DrivingRoute : IRouteCalculationStrategy
{
public List<string> CalculateRoute(string start, string end)
{
return new List<string> { $"Start at {start}", "Take the highway", $"Arrive at {end}" };
}
public double CalculateEstimatedTime(string start, string end) => 30;
public double CalculateEstimatedCost(string start, string end) => 5.00;
}
public class WalkingRoute : IRouteCalculationStrategy
{
public List<string> CalculateRoute(string start, string end)
{
return new List<string> { $"Start at {start}", "Walk through the park", $"Arrive at {end}" };
}
public double CalculateEstimatedTime(string start, string end) => 60;
public double CalculateEstimatedCost(string start, string end) => 0;
}
public class PublicTransportRoute : IRouteCalculationStrategy
{
public List<string> CalculateRoute(string start, string end)
{
return new List<string> { $"Start at {start}", "Take the subway", $"Arrive at {end}" };
}
public double CalculateEstimatedTime(string start, string end) => 40;
public double CalculateEstimatedCost(string start, string end) => 2.50;
}
Step 3: Use the Strategy in NavigationApp
Now, we modify the NavigationApp class to use the Strategy Pattern:
public class NavigationApp
{
private IRouteCalculationStrategy _routeStrategy;
public NavigationApp(IRouteCalculationStrategy routeStrategy)
{
_routeStrategy = routeStrategy;
}
public void SetRouteStrategy(IRouteCalculationStrategy routeStrategy)
{
_routeStrategy = routeStrategy;
}
public void ShowRoute(string start, string end)
{
List<string> route = _routeStrategy.CalculateRoute(start, end);
foreach (string step in route)
{
Console.WriteLine(step);
}
Console.WriteLine($"Estimated Time: {_routeStrategy.CalculateEstimatedTime(start, end)} minutes");
Console.WriteLine($"Estimated Cost: ${_routeStrategy.CalculateEstimatedCost(start, end)}");
}
}
Using the Strategy Pattern in Action
Now, let’s see the Strategy Pattern in action:
public class Program
{
public static void Main(string[] args)
{
NavigationApp app = new NavigationApp(new DrivingRoute());
app.ShowRoute("Home", "Work");
Console.WriteLine("\nSwitching to walking route:\n");
app.SetRouteStrategy(new WalkingRoute());
app.ShowRoute("Home", "Work");
Console.WriteLine("\nSwitching to public transport route:\n");
app.SetRouteStrategy(new PublicTransportRoute());
app.ShowRoute("Home", "Work");
}
}
(Grab the refactored code here)
Conclusion
Whether you're building a navigation app, a payment system, or an AI engine, the Strategy Pattern keeps your code flexible, modular, and easy to extend; making it a must-have tool in any developer’s toolkit.
By implementing this design pattern, you ensure that your application remains scalable and adaptable to future changes.
Happy coding! ⚡