Click here to Skip to main content
15,867,488 members
Articles / All Topics
Technical Blog

Patterns In Practice - Strategy and Composite

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
20 Oct 2010CPOL6 min read 10.4K   5  
Behavioural PatternsIn 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.

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)
{
    //Get source data set and create new set for return data
    List<Person> people = PersonCache.GetPeople();
    List<Person> resultList = new List<Person>();

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

    //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.

Strategy Pattern

 

Implementation Code

Here we have our interface defined in code, with some stub implementations.

/// <summary>
/// Basic check interface
/// </summary>
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>();

    //Get checks to run
    List<IPersonCheck> 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.

 

CompositePattern


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)
    {
        //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<Person> FindPeople(PersonFind find)
{
    List<Person> people = PersonCache.GetPeople();
    List<Person> resultList = new List<Person>();

    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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
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.

Comments and Discussions

 
-- There are no messages in this forum --