Behavioural Patterns
In software engineering, behavioural design patterns are design patterns that identify common communication patterns between objects and realize these patterns. By doing so, these patterns increase flexibility in carrying out this communication.
Scenario
Using the Strategy and Composite patterns to pull a subset out of a large body of data.
- Imagine we have an N tier application.
- In the middle tier, we pull a large data set from the data source or cache.
- We filter the data set and return a sub set of this data based on a supplied set of criteria.
This is a reasonably common scenario, where a large and expensive database query is run, stored for a period of time and refreshed. The middle tier will return the stored data when queried, rather than going back to the database. The middle tier will therefore have to be able to filter out the required sub set of data, without relying on the back end logic.
Basic Implementation
The basic loop for returning a sub set of data could look like this. This is a perfectly reasonable solution for a single check, and could be increased to a couple of checks without any problem.
<!-- code formatted by http://manoli.net/csharpformat/ -->
public List<Person> FindPeople(PersonFind find)
{
List<Person> people = PersonCache.GetPeople();
List<Person> resultList = new List<Person>();
foreach (Person person in people)
{
if ((find.FirstName != null && person.FirstName != find.FirstName)
|| (find.LastName != null && person.LastName != find.LastName))
continue;
if (find.MinimumAge != null || find.MaximumAge != null)
{
int age = (DateTime.Today - person.DateOfBirth).Days / 365;
if ((find.MinimumAge != null && age < find.MinimumAge) ||
(find.MaximumAge != null && age > find.MaximumAge))
continue;
}
resultList.Add(person);
}
return resultList;
}
Extended Basic Implementation
This loop has continued the pattern of the first, and added a number of additional checks. As we add checks, the loop continues to grow, and we can end up with a loop that is many hundreds of lines.
public List<Person> FindPeople(PersonFind find)
{
List<Person> people = PersonCache.GetPeople();
List<Person> resultList = new List<Person>();
foreach (Person person in people)
{
if ((find.FirstName != null && person.FirstName != find.FirstName)
|| (find.LastName != null && person.LastName != find.LastName))
continue;
if (find.MinimumAge != null || find.MaximumAge != null)
{
int age = (DateTime.Today - person.DateOfBirth).Days / 365;
if ((find.MinimumAge != null && age < find.MinimumAge) ||
(find.MaximumAge != null && age > find.MaximumAge))
continue;
}
if (find.Gender != null && person.Gender != find.Gender.Value)
continue;
resultList.Add(person);
}
return resultList;
}
Implementation Issues
On a small scale, this solution works, if it is never going to grow beyond a few checks, then no further action needs to be taken, however, if this loop is going to grow with the system, then we will certainly run into issues if we continue down this path.
- We will have a large block of unwieldy code.
- Each piece of condition logic is locked in the loop, unavailable elsewhere in the application, such as when validation is required elsewhere.
- To test a specific condition, a scenario has to be created that will pass all other conditions. This can be difficult, particularly in cases where there are many hundreds of rules.
- The rule set is fixed and inflexible. It cannot be changed easily to meet different criteria
Strategy Pattern
The strategy pattern is a design pattern that enables algorithms to be selected at runtime, by encapsulating the algorithm in an object. This is achieved by the following:
- Define a family of algorithms
- Encapsulate each algorithm in an object
- Load the appropriate implementation at runtime
Identify the Method Signature
To identify the method signature, we need to identify what the recurring pattern is in the code. In this instance, the code performed the following:
- Take a Person object (From the collection)
- Take a PersonFind object (Parameter)
- Perform a check on the Person, using the PersonFind (Result: bool)
- Perform an action based on the result
From this, we can derive the following method signature:
bool IsValid(Person person, PersonFind find)
Implement the Strategy Pattern
Now we have the method signature for our recurring problem, we can use it to build an interface. This interface will define the family of algorithms we will use, as mentioned in the description of the Strategy pattern.
Implementation Code
Here we have our interface defined in code, with some stub implementations.
public interface IPersonCheck
{
string Name { get; }
bool IsValid(Person person, PersonFind find);
}
public class NameCheck : IPersonCheck [...]
public class GenderCheck : IPersonCheck [...]
public class AgeCheck : IPersonCheck [...]
public class OccupationCheck : IPersonCheck [...]
public class SalaryCheck : IPersonCheck [...]
Calling Code
With this foundation now in place, we can replace our original loop logic with the strategy implementation.
public List<Person> FindPeople(PersonFind find)
{
List<Person> people = PersonCache.GetPeople();
List<Person> resultList = new List<Person>();
List<IPersonCheck> checks = GetCheckSet();
foreach (Person person in people)
{
bool valid = true;
foreach (IPersonCheck check in checks)
{
if (!check.IsValid(person, find))
{
valid = false;
break;
}
}
if (valid)
resultList.Add(person);
}
return resultList;
}
Loop Comparison
With both the original and the strategy loop implemented, let’s compare their implementation.
Original Loop
foreach (Person person in people)
{
if ((find.FirstName != null && person.FirstName != find.FirstName)
|| (find.LastName != null && person.LastName != find.LastName))
continue;
if (find.MinimumAge != null || find.MaximumAge != null)
{
int age = (DateTime.Today - person.DateOfBirth).Days / 365;
if ((find.MinimumAge != null && age < find.MinimumAge) ||
(find.MaximumAge != null && age > find.MaximumAge))
continue;
}
if (find.Gender != null && person.Gender != find.Gender.Value)
continue;
resultList.Add(person);
}
Strategy Loop
foreach (Person person in people)
{
bool valid = true;
foreach (IPersonCheck check in checks)
{
if (!check.IsValid(person, find))
{
valid = false;
break;
}
}
if (valid)
resultList.Add(person);
}
Benefits of the Strategy Pattern
As you can see, the second implementation of the loop is much more compact, as the code base grows and new checks are added they will not clutter the boiler plate logic. It offers a clean view of what the function is supposed to do, while the individual checks, no matter how complicated, are isolated and can be handled on a case by case basis. The benefits are summarised below.
- Better modularity, smaller functions, easier to understand outside the boilerplate logic of the main loop.
- Each condition is testable independently of all the others.
- The loop will remain the same size, regardless of the number of checks added.
- Can tailor the checks used, depending on scenario.
- Can easily add tracing to diagnose rule failure in production systems.
Examples in .Net
The following classes are examples of the strategy pattern in the .Net libraries.
- IComparer - Defines a strategy for comparing objects, which can be used for list sorting.
- ICollection – Defines a collection which can be used for storing, managing and enumerating objects using a variety of algorithms, i.e. List, Queue, Stack, LinkedList, etc.
Composite Pattern
The composite pattern is a design pattern that enables a group of related objects to be treated as a single instance. This is achieved by the following
- Define a family of classes.
- Create an implementation that can contain multiple instances of the class.
Implementing the Composite Pattern
Using these steps, we can take the interface we created above for the strategy pattern, create a new implementation, and put the checks that we use into a single instance.
Implementation Code
This is our implementation of a composite check, that bundles multiple checks together into a single instance.
class CompositePersonCheck : IPersonCheck
{
private List<IPersonCheck> _checks;
public CompositePersonCheck()
{
_checks = new List<IPersonCheck>();
_checks.Add(new NameCheck());
_checks.Add(new GenderCheck());
_checks.Add(new AgeCheck());
_checks.Add(new OccupationCheck());
_checks.Add(new SalaryCheck());
}
#region IPersonCheck Members
public string Name
{
get { return "Composite Check"; }
}
public bool IsValid(Person person, PersonFind find)
{
foreach (IPersonCheck check in _checks)
{
if (!check.IsValid(person, find))
return false;
}
return true;
}
#endregion
}
Calling Code
public List<Person> FindPeople(PersonFind find)
{
List<Person> people = PersonCache.GetPeople();
List<Person> resultList = new List<Person>();
IPersonCheck check = GetCheck();
foreach (Person person in people)
{
if (check.IsValid(person, find))
resultList.Add(person);
}
return resultList;
}
Loop Comparison
With the third loop implemented, let’s look at how the different implementations stack up.
Original Loop
foreach (Person person in people)
{
if ((find.FirstName != null && person.FirstName != find.FirstName)
|| (find.LastName != null && person.LastName != find.LastName))
continue;
if (find.MinimumAge != null || find.MaximumAge != null)
{
int age = (DateTime.Today - person.DateOfBirth).Days / 365;
if ((find.MinimumAge != null && age < find.MinimumAge) ||
(find.MaximumAge != null && age > find.MaximumAge))
continue;
}
if (find.Gender != null && person.Gender != find.Gender.Value)
continue;
resultList.Add(person);
}
Strategy Loop
foreach (Person person in people)
{
bool valid = true;
foreach (IPersonCheck check in checks)
{
if (!check.IsValid(person, find))
{
valid = false;
break;
}
}
if (valid)
resultList.Add(person);
}
Composite Loop
foreach (Person person in people)
{
if (check.IsValid(person, find))
resultList.Add(person);
}
Benefits of the composite pattern
The benefits of this pattern are not as obvious as the initial strategy implementation for this particular scenario; it is just one step further in shaping the code and reducing the boiler plate loop. However, it puts us in a situation where we have a massive amount of flexibility as to how we apply our conditions and lays the groundwork for a very powerful rule system. The benefits are summarised below:
- Easy to create sets of checks / rules that are bundled together.
- Easy to create composite conditional checks such as AND / OR.
- Easy to create check / rule trees, composed of sets, and conditionals.
Examples in .Net
The following classes are examples of the composite pattern in the .Net libraries.
- Control – Winforms component that can be composed of multiple controls, handling them as a single instance.
- XmlElement – Xml element that can contain one or more XmlElements, to build up the DOM tree.
Note
Generally, a “pure” implementation of the composite pattern will have a concrete base class and use inheritance, rather than an interface. However, for this example I decided that it was too heavy weight when an interface handled it just as well, and it achieves the same results.
My name is Tristan Rhodes, i wrote my first Hello World program in 2001 and fell in love with software development (I actualy wanted to be a Vet at the time).
I enjoy working with code and design patterns.