Patterns In Practice - Strategy and Composite

by TristanRhodes 3. April 2011 17:59

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.

 

public List FindPeople(PersonFind find)
{
    //Get source data set and create new set for return data 
    List people = PersonCache.GetPeople();
    List resultList = new List();

    //For each person 
    foreach (Person person in people)
    {
        //Name Check 
        if ((find.FirstName != null && person.FirstName != find.FirstName) ||
            (find.LastName != null && person.LastName != find.LastName))
            continue;

        //Age Check 
        if (find.MinimumAge != null || find.MaximumAge != null)
        {
            //Calculate Age 
            int age = (DateTime.Today - person.DateOfBirth).Days / 365;

            //Check Age Bands 
            if ((find.MinimumAge != null && age < find.MinimumAge) ||
                (find.MaximumAge != null && age > find.MaximumAge))
                continue;
        }

        //All tests passed - add to result set 
        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 FindPeople(PersonFind find)
{
    //Get source data set and create new set for return data  
    List people = PersonCache.GetPeople();
    List resultList = new List();

    //For each person 
    foreach (Person person in people)
    {
        //Name Check 
        if ((find.FirstName != null && person.FirstName != find.FirstName)
        || (find.LastName != null && person.LastName != find.LastName))
            continue;

        //Age Check 
        if (find.MinimumAge != null || find.MaximumAge != null)
        {
            //Calculate Age 
            int age = (DateTime.Today - person.DateOfBirth).Days / 365;

            //Check Age Bands 
            if ((find.MinimumAge != null && age < find.MinimumAge)
            || (find.MaximumAge != null && age > find.MaximumAge))
                continue;
        }

        //Gender Check 
        if (find.Gender != null && person.Gender != find.Gender.Value)
            continue;

        //SNIP: 50 More Checks 

        //All tests passed - add to result set 
        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.


///  
/// Basic check interface 
///  
public interface IPersonCheck 
{ 
    string Name { get; } 
            
    bool IsValid(Person person, PersonFind find); 
} 
        
//Implementions
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 FindPeople(PersonFind find) 
{ 
    List people = PersonCache.GetPeople(); 
    List resultList = new List(); 
            
    //Get checks to run 
    List checks = GetCheckSet(); 
            
    //For each person 
    foreach (Person person in people) 
    { 
        bool valid = true; 
                
        //Perform All Checks 
        foreach (IPersonCheck check in checks) 
        { 
            //Handle if fail 
            if (!check.IsValid(person, find)) 
            { 
                valid = false; 
                break; 
            } 
        } 
                
        //All tests passed - add to result set 
        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

//For each person 
foreach (Person person in people)
{
    //Name Check 
    if ((find.FirstName != null && person.FirstName != find.FirstName) ||
        (find.LastName != null && person.LastName != find.LastName))
        continue;

    //Age Check 
    if (find.MinimumAge != null || find.MaximumAge != null)
    {
        //Calculate Age 
        int age = (DateTime.Today - person.DateOfBirth).Days / 365;

        //Check Age Bands 
        if ((find.MinimumAge != null && age < find.MinimumAge) ||
            (find.MaximumAge != null && age > find.MaximumAge))
            continue;
    }

    //Gender Check 
    if (find.Gender != null && person.Gender != find.Gender.Value)
        continue;

    //SNIP: 50 More Checks 

    //All tests passed - add to result set 
    resultList.Add(person);
}

Strategy Loop

//For each person 
foreach (Person person in people) 
{
    bool valid = true; 
                
    //Perform All Checks 
    foreach (IPersonCheck check in checks) 
    { 
        //Handle if fail 
        if (!check.IsValid(person, find)) 
        { 
            valid = false; 
            break; 
        } 
    } 
                
    //All tests passed - add to result set 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 _checks; 

    public CompositePersonCheck() 
    { 
        _checks = new List(); 
        _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) 
    { 
        //Perform All Checks 
        foreach (IPersonCheck check in _checks) 
        { 
            //Add to removal list if not valid 
            if (!check.IsValid(person, find)) 
                return false; 
        } 

        return true; 
    } 
    
    #endregion 
}

Calling Code

public List FindPeople(PersonFind find) 
{ 
    List people = PersonCache.GetPeople(); 
    List resultList = new List(); 
    IPersonCheck check = GetCheck(); 
    
    //Test each person against the composite check. 
    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

//For each person 
foreach (Person person in people)
{
    //Name Check 
    if ((find.FirstName != null && person.FirstName != find.FirstName) ||
        (find.LastName != null && person.LastName != find.LastName))
        continue;

    //Age Check 
    if (find.MinimumAge != null || find.MaximumAge != null)
    {
        //Calculate Age 
        int age = (DateTime.Today - person.DateOfBirth).Days / 365;

        //Check Age Bands 
        if ((find.MinimumAge != null && age < find.MinimumAge) ||
            (find.MaximumAge != null && age > find.MaximumAge))
            continue;
    }

    //Gender Check 
    if (find.Gender != null && person.Gender != find.Gender.Value)
        continue;

    //SNIP: 50 More Checks 

    //All tests passed - add to result set 
    resultList.Add(person);
}

Strategy Loop

//For each person 
foreach (Person person in people) 
{
    bool valid = true; 
                
    //Perform All Checks 
    foreach (IPersonCheck check in checks) 
    { 
        //Handle if fail 
        if (!check.IsValid(person, find)) 
        { 
            valid = false; 
            break; 
        } 
    } 
                
    //All tests passed - add to result set if (valid) 
    resultList.Add(person); 
}


Composite Loop

//For each person 
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.

Tags:

Comments (5) -

toile curtains
toile curtains United States
9/12/2011 1:08:06 PM #

In case you might e-mail me with a few strategies on simply how you made your blog look this excellent, I'd be grateful.

Reply

Mutuelle Animaux
Mutuelle Animaux France
9/20/2011 11:46:45 AM #

I like the valuable information you provide in your articles. I will bookmark your weblog and check again here frequently. I am quite certain I will learn many new stuff right here! Good luck for the next!

Reply

Assurance Chat
Assurance Chat France
11/25/2011 12:51:27 PM #

Thanks a lot about this stategy pattern. Very interesting.

Reply

Mutuelle chien
Mutuelle chien France
2/14/2012 4:13:22 PM #

Thanks a lot about this information.

Reply

Hoe krijg je een sixpack
Hoe krijg je een sixpack Netherlands
4/13/2012 6:47:27 PM #

I just wanted to thank you for your time and effort to write this post. I like it!

Reply

Add comment




  Country flag
biuquote
  • Comment
  • Preview
Loading


About The Author

Tristan is an MCPD Web / Windows Developer in .Net 4.0 who works for a Microsoft Gold Partner based in London.

Tag cloud

    RecentPosts