With this blog post I want to start a series. The series will try to shed some light upon different patterns. There are a zillion resources on the web describing patterns, why do it all over again? Well most resources base their explanation for patterns on UML boxes; I would like to explain them in a language which much easier will demonstrate the intent and benefits. For that I've chosen C#.
Initially I wanted to call it "Pattern of The week", but knowing my schedule it would probably be too much to live up to at first. So instead I will call it Pattern Focus for now. The first pattern I would like to start with is an old favorite of mine, the strategy pattern.
Rational
The idea behind the strategy pattern is to simplify the execution and maintenance of methods with complex or wide execution paths. Using strategies we usually get cleaner APIs and more readable methods and can reuse execution paths across multiple methods instead of just copy and paste it around. It is also easier to extend those methods with new options without making the API really ugly and without touching the method itself.
The Example scenario The example scenario is very simple but will be able to demonstrate several ways to implement the strategy pattern. The scenario is based on a list of consultants and the idea of trying to find one from the list based on price, skill or both. The method looks like this in its initial form:
static Consultant FindFirstSuitable(List consultants, bool prefereCheap, int maxPrice, bool prefereSkilled, int minLevel) { foreach (Consultant consultant in consultants) { if (prefereCheap && consultant.HourRate > maxPrice) continue;
if (prefereSkilled && consultant.SkillLevel < minLevel) continue;
return consultant; } return null; }
Not the most beautiful method but it does work and will produce the expected result. The problem is though that the intent of this method is not really clear cut. It?s hard to say exactly what is expected to be the right result, or the right way to call it. It could also potentially grow where more criteria?s get added to make the search finer grained. Another point is that now the rules are captured inside this specific method and not useable in any other context.
A clearer Intent
So let?s clear up the intent a bit. In the process we will get a model where we can reuse the rules and expand on the rules without bloating our API's. To successfully implement the strategy pattern in this scenario we need to identify the point where the execution differs. For our method it is the if statements in the middle of the foreach loop which holds the magic. A bit of the intent is actually clear there, where we have named the Boolean "IsSuitable", this is a great starting point for us to extract the strategy. First we want something to bind all strategies together, one option is to use an abstract base class but I like interfaces better so we will start off by creating an interface:
public interface IConsultantStrategy { bool IsSuitable(Consultant consultant); }
This interface quite clearly displays the intent of the strategy, we want to find out if a consultant is suitable for a certain strategy.
Next we create three concrete implementations of the interface, one for each scenario mentioned for this example. All of them get their initial configuration regarding max values in the constructor and all of them implement the IsSuitable method accepting the consultant object to check. I will only show one of them as an example:
public class CheapConsultantStrategy : IConsultantStrategy {
private int maxHourRate;
public CheapConsultantStrategy(int MaxHourRate) { maxHourRate = MaxHourRate; }
public bool IsSuitable(Consultant consultant) { return consultant.HourRate <= maxHourRate; } }
Now what we have is intent and rules captured in a class ready to be used in our scenario any other similar scenarios. To use this strategy, next we need to change the method declaration and the implementation of the method. The declaration needs to look something like this:
FindFirstSuitable(List consultants, IConsultantStrategy strategy)
The method is now a bit clearer on the intent; it wants a strategy and is communicating that I can choose that strategy by passing an object implementing the IConsultantStrategy interface.
The foreach loop now got a much clearer intent and cleaner code as well, instead of three if statements we only need one:
foreach (Consultant consultant in consultants) { if ( strategy.IsSuitable ( consultant ) ) return consultant; }
To use the method we call it with a strategy object, something like this:
Consultant consultant = FindFirstSuitable(consultants, new CheapConsultantStrategy(20)); Consultant consultant = FindFirstSuitable(consultants, new SkilledConsultantStrategy(2)); Consultant consultant = FindFirstSuitable(consultants, new CheapAndSkilledConsultantStrategy(20, 2));
The above method calls show clear intent which makes it much easier to read and understand what the method will do. I find this to be a very neat way to communicate through the project, not only to yourself but also to other developers that might need to work with your code or API.
Now don not run away and implement strategy all over the place, I know it is a really neat pattern, but it has its place in time and fame. Considering time and effort you put into creating the strategies, in the form shown here they are really only valuable when you can reuse the strategies in more places of your application or you find yourself writing massive code based on selection statements.
This is the classic implementation of a strategy pattern; with .NET there is a more "light-weight" way to achieve something similar. That will be discussed in a later post (maybe even later today if you are lucky).
The code for this post is attached (for you who get this via RSS readers, you need to go to the website).
|