Pattern: Loop Decoupling in C# with Linq

2013-09-01 11:37 PM

Back in college, we all learned about loop unrolling in computer architecture/compiler classes. Though the loop decoupling pattern is not really related, the techniques are both different ways to look at loops and improve them; however, the decoupling attacks modularity, composability and testability rather than execution time.

Problem Statement

When presented with a loop that satisfies the following conditions, loop decoupling becomes an applicable pattern:

  • The enumeration of data is programmatic (not data driven) and nontrivial
  • The filtration process is nontrivial and composable
  • Bonus points if ordering or grouping is necessary

Pattern Outline

Loop decoupling essentially involves three parts:

  1. Encapsulate the data generation in a method which returns an IEnumerable<T> and performs no filtration
  2. Encapsulate the filtration logic into a method or methods which take in T and return bool
  3. Use these composable bits to write a single Linq statement, optionally grouping or ordering afterward

This pattern yields several benefits:

  • Since the data and filtration logic are separated into their own methods, they
    • Each have a single responsibility
    • Are easily understandable on their own
    • Are easily testable
  • The final composed Linq query is easy to understand, and makes the intention of the code extremely clear

Example

Since this concept is incredibly abstract, we'll need to cement it in with a good example.

For a given date d and a given day of the week m, find the date nearest to d which is day of the week m.

Since we need the nearest date rather than the next date, the enumeration operation is nontrivial. Though the filtration method is fairly simple in this example, we'll charge forth with the assumption that the real world business filtration requirement would be more complex.

So, let's go through the steps!

1. Encapsulate the data generation

We need a method that takes in a DateTime (the parameter d from the problem statement) and returns IEnumerable<DateTime>. Our strategy will be to search out from the given date in both directions and emit all dates we encounter in the process.

IEnumerable<DateTime> SearchDates(DateTime input, int max)
{
    yield return input;
    for(var i = 0; i < max; ++i)
    {
        yield return input.AddDays(i);
        yield return input.AddDays(-i);
    }
}

Some notes:

  • We take in a max value so that the loop doesn't execute into infinity
  • The input date is returned first because it may be the one we're looking for
  • We move forward and backwards at the same time, one more day each way on every iteration

2. Encapsulate the filtration logic

Next, we need a method that takes in a DateTime and a day of the week (parameter m from the problem statement) and returns a bool.

bool IsValid(DateTime date, DayOfWeek target)
{
    return date.DayOfWeek == target;
}

3. Compose a Linq statement

Now it's really easy to extract the first matching date!

DateTime GetNearestDay(DateTime target, DayOfWeek day)
{
    var results = from d in SearchDates(target, 3)
                  where IsValid(d, day)
                  select d;
    return results.FirstOrDefault();
}

Freebie: Query modification

Now that we have a composed query, we get a lot of functionality for free! When business requirements come through to add more filtration, order results differently or alter the return type to include a grouping, we can really easily modify the query without having to touch either the generation or filtration logic!

object ThisDoesntMakeSense(DateTime target, DayOfWeek day)
{
    return from d in SearchDates(target, 365)
           where IsValid(d, day)
                 && SomethingElse(target)
           group d by d.DayOfMonth into d
           order by d.Key descending
           select d;
}