Design Patterns: Strategy
Often times we need to change some algorithm with another without changing the client code that’s consuming it. In this post I want to show a use case I came across and utilised Strategy pattern.
What is it?
Here’s the official definition of the pattern from GoF book:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Use case: Get external IP address
In an application I was working on I needed to get the external IP address of the computer the application is running on. There are various ways to achieve that. This looked like a good opprtunity to use the Strategy pattern as I wanted to be able switch between the different methods easily.
Implementation
The interface is quite simple:
public interface IIpCheckStrategy
{
string GetExternalIp();
}
Some services return their data in JSON format, some have extra text in it. But encapsulating the algorithms in their own classes this way, the clint code doesn’t have to worry about parsing these various return values. It’s handled in each class. If one service changes it’s output and breaks the implementation I can recover just by changing the code instantiates the class.
The concrete implementations of the interface are below. They implement the IIpCheckStrategy and are responsible for getting the data and return parsed IP address as string.
AWS IP Checker:
public class AwsIPCheckStrategy : IIpCheckStrategy
{
public string GetExternalIp()
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://checkip.amazonaws.com/");
string result = client.GetStringAsync("").Result;
return result.TrimEnd('\n');
}
}
}
DynDns IP Checker:
public class DynDnsIPCheckStrategy : IIpCheckStrategy
{
public string GetExternalIp()
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://checkip.dyndns.org/");
HttpResponseMessage response = client.GetAsync("").Result;
return HelperMethods.ExtractIPAddress(response.Content.ReadAsStringAsync().Result);
}
}
}
Custom IP Checker:
public class CustomIpCheckStrategy : IIpCheckStrategy
{
public string GetExternalIp()
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://check-ip.herokuapp.com/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = client.GetAsync("").Result;
string json = response.Content.ReadAsStringAsync().Result;
dynamic ip = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
string result = ip.ipAddress;
return result;
}
}
}
Choosing the algorithm
The consumer of the algorithm can pick any class that implements IIpCheckStrategy and switch between them. For example:
class StrategyClient1
{
public void Execute()
{
IIpCheckStrategy ipChecker;
ipChecker = new DynDnsIPCheckStrategy();
Console.WriteLine(ipChecker.GetExternalIp());
ipChecker = new AwsIPCheckStrategy();
Console.WriteLine(ipChecker.GetExternalIp());
ipChecker = new CustomIpCheckStrategy();
Console.WriteLine(ipChecker.GetExternalIp());
Console.ReadKey();
}
}
Also the class name to be used can be stored in the configuration in some cases so that it can be changed at runtime without recompiling the application. For instance:
class StrategyClient2
{
public void Execute()
{
string ipcheckerTypeName = ConfigurationManager.AppSettings["IPChecker"];
IIpCheckStrategy ipchecker = Assembly.GetExecutingAssembly().CreateInstance(ipcheckerTypeName) as IIpCheckStrategy;
Console.WriteLine(ipchecker.GetExternalIp());
}
}
and the appSettings in the configuration would look like this:
<appSettings>
<add key="IPChecker" value="Strategy.AwsIPCheckStrategy"/>
</appSettings>