The Lowly Strategy Pattern is Still Useful

Just a coincidence here, but I had this blog post draft in the works right as my friend and Critter Stack collaborator Oskar Dudycz wrote Is the Strategy Pattern an ultimate solution for low coupling? last week (right after Lillard was traded). My goal here is just to create some new blog content by plucking out existing usages of design patterns in my own work. I’m hoping this turns into a low key series to go revisit some older software development concepts and see if they still hold any value.

On Design Patterns

Before discussing the “Strategy Pattern,” let’s go address the obvious elephant in the room. There has been a huge backlash against design patterns in the aftermath of the famous “Gang of Four” book. As some of you will be unable from resist pointing out in the comments, design patterns have been absurdly overused by people who associate complexity in code with well engineered code leading to such atrocities as “WidgetStrategyAbstractFactoryBuilder” type names showing up in your code. A certain type of very online functional programming enthusiast loves to say “design patterns are just an indication that your OO programming language is broken and my FP language doesn’t need them” — which I think is both an inaccurate and completely useless statement because there are absolutely recurring design patterns in functional programming as well even if they aren’t the exact same set of design patterns or implemented the exact same way as they were described in the old GoF book from the C++ era.

Backing off of the negativity and cynicism, “design patterns” came out of a desire to build a taxonomy and understanding of reoccurring structural elements that developers were already frequently using to solve problems in code. The hope and goal of this effort was to build a common language that developers could use with each other to quickly describe what they were doing when they used these patterns or even to just understand what some other developer was doing in the code you just inherited. Just as importantly, once we developers have a shared name for these patterns, we could start to record and share a body of wisdom about when and where these patterns were applicable, useful, or harmful.

That’s it, that’s all they ever should have been. The problems always came about when people decided that design patterns were recommendations or goals unto themselves and then tried to maximize their usage of said patterns. Software development being very prone to quick cycles of “ooh, shiny object!” then the inevitable backlash after taking the new shiny object way too far, design patterns got an atrocious reputation across much of our community.

After all that being said, here’s what I think is absolutely still valuable about design patterns and why learning about them is worth your time:

  • They happen in code anyway
  • It’s useful to have the common language to discuss code with other developers
  • Recognizing a design pattern in usage can give you some quick insight into how some existing code works or was at least intended to work
  • There is a large amount of writing out there about the benefits, drawbacks, and applicability of all of the common design patterns

And lastly, don’t force the usage of design patterns in your code.

The Strategy Pattern

One of the simplest and most common design patterns in all of software development is the “Strategy” pattern that:

allows one of a family of algorithms to be selected on-the-fly at runtime.

Gang of Four

Fine, but let’s immediately move to a simple, concrete example. In a recent Wolverine release, I finally added an end to end multi-tenancy feature for Wolverine’s HTTP endpoints for a JasperFx client. One of the key parts of that new feature was for Wolverine to be able to identity what the active tenant was from an HTTP request. From experience, I knew that there were several commonly used ways to do that, and even knew that plenty of folks would want to mix and match approaches like:

  • Look for a named route argument
  • Look for an expected request header
  • Use a named claim for the authenticated user
  • Look for an expected query string parameter
  • Key off of the Url sub domain name

And also allow for users to add some completely custom mechanism for who knows what they’ll actually want to do in their own system.

This of course is a pretty obvious usage of the “strategy pattern” where you expose a common interface for the variable algorithms that could be used within the code that ended up looking like this:

/// <summary>
/// Used to create new strategies to detect the tenant id from an HttpContext
/// for the current request
/// </summary>
public interface ITenantDetection
{
    /// <summary>
    /// This method can return the actual tenant id or null to represent "not found"
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public ValueTask<string?> DetectTenant(HttpContext context);
}

Behind the scenes, a simple version of that interface for the route argument approach looks like this code:

internal class ArgumentDetection : ITenantDetection
{
    private readonly string _argumentName;

    public ArgumentDetection(string argumentName)
    {
        _argumentName = argumentName;
    }

    public ValueTask<string?> DetectTenant(HttpContext httpContext)
    {
        return httpContext.Request.RouteValues.TryGetValue(_argumentName, out var value) 
            ? new ValueTask<string?>(value?.ToString()) 
            : ValueTask.FromResult<string?>(null);
    }

    public override string ToString()
    {
        return $"Tenant Id is route argument named '{_argumentName}'";
    }
}

Now, you *could* accurately tell me that this whole strategy pattern nonsense with interfaces and such could be accomplished with mere functions (Func<HttpContext, string?> maybe), and you would be absolutely correct. And I’m going to respond by saying that that is still the “Strategy Pattern” in intent and its role within your code — even though the implementation is different than my C# interface up above. When learning and discussing design patterns, I highly recommend you worry more about the roles and intent of functions, methods, or classes than the actual implementation details.

But also, having the interface + implementing class structure makes it easier for a framework like Wolverine to provide meaningful diagnostics and visualizations of how the code is configured. I actually went down a more functional approach in an earlier framework, and went back to being more OO in the framework guts for reasons of traceability and diagnostics. That’s not necessarily something that I think is generally applicable inside of application code though.

More Complicated: The Marten LINQ Provider

One of the most important points I need to make about design patterns is to use them defensively as a response to a use case in your system rather than picking out design patterns upfront like you’re ordering off of a menu. As a case in point, I’m going to shift to the Marten LINQ provider code. Consider this simplistic usage of a LINQ query:

public static async Task run_linq_query(IQuerySession session)
{
    var targets = await session.Query<Target>()
        // Filter by a numeric value
        .Where(x => x.Number == 5)

        // Filter by an Enum value
        .Where(x => x.Color == Colors.Blue)

        .ToListAsync();
}

Notice the usage of the Where() clauses to merely filter based on both a numeric value and the value of a custom Color enum on the Target document (a fake document type in Marten for exactly this kind of testing). Even in this example, Marten runs into quite a bit of variance as it tries to create SQL to compare a value to the persisted JSONB field within the PostgreSQL database:

  • In the case of an integer, Marten can simply compare the JSON field to the actual number
  • In the case of an Enum value, Marten will need to compare the JSON field to either the numeric value of the Enum value or to the string name for the Enum value depending on the serialization settings for Marten in the application

In the early days of Marten, there was code that did the variation you see above with simple procedural code something like:

    if (memberType.IsEnum)
    {
        if (serializer.EnumStorage == EnumStorage.AsInteger)
        {
            // create parameter for numeric value of enum
        }
        else
        {
            // create parameter for string name
            // of enum value
        }
    }
    else
    {
        // create parameter for value
    }

In the some of the comments to Oskar’s recent post on the Strategy pattern (maybe on LinkedIn?), I saw someone point out that they thought that moving logic behind strategy pattern interfaces as opposed to simple, inline procedural code made code harder to read and understand. I absolutely understand that point of view, and I’ve run across that before too (and definitely caused myself).

However, that bit of procedural code above? That code started being repeated in a lot of places in the LINQ parsing code. Worse, that nested, branching code was showing up within surrounding code that was already deeply nested and rife with branching logic before you even got into the parameter creation code. Even worse, that repeated procedural code grew in complexity over time as we found more special handling rules for additional types like DateTime or DateTimeOffset.

As a reaction to that exploding complexity, deep code branching, and harmful duplication in our LINQ parsing code, we introduced a couple different instances of the Strategy pattern, including this interface from what will be the V7 release of Marten soon (hopefully):

public interface IComparableMember
{
    /// <summary>
    /// For a member inside of a document, create the WHERE clause
    /// for comparing itself to the supplied value "constant" using
    /// the supplied operator "op"
    /// </summary>
    /// <param name="op"></param>
    /// <param name="constant"></param>
    /// <returns></returns>
    ISqlFragment CreateComparison(string op, ConstantExpression constant);
}

And of course, there are different implementations of that interface for string members, numeric members, and even separate implementations for Enum values stored as integers in the JSON or stored as strings within the JSON. At runtime, when the LINQ parser sees an expression like Where(x => x.SomeProperty == Value), it works by:

  1. Finding the known, memoized IComparableMember for the SomeProperty member
  2. Calls the IComparableMember.CreateComparison("==", Value) to translate the LINQ expression into a SQL fragment

In the case of the Marten LINQ parsing, introducing the “Strategy” pattern usage did a lot of good to simplify the internal code by removing deeply nested branching logic and by allowing us to more easily introduce support for all new .NET types within the LINQ support by hiding the variation behind abstracted strategies for different .NET types or different .NET methods (string.StartsWith() / string.EndsWith() for example). Using the Strategy pattern also allowed us to remove quite a bit of duplicated logic in the Marten code.

The main takeaways from this more complicated sample:

  • The Strategy pattern can sometimes improve the maintainability of your code by reducing the need for branching logic within your code. Deeply nested if/then constructs in code are a veritable breeding ground for software bugs. Slimming that down and avoiding “Arrowhead code” can be a big advantage of the Strategy pattern
  • In some scenarios like the Marten LINQ processor, the Strategy pattern is a way to write much more DRY code that also made the code easier to maintain and extend over time. More. on this below.

A Quick Side Note about the Don’t Repeat Yourself Principle

Speaking of backlashes, many developers have a bad taste in their mouthes over the Don’t Repeat Yourself principle (DRY) — or differently stated by the old XP community as “once, and only once.” I’ve even seen experienced developers scream that “DRY is not important!” or go so far as to say that trying to write DRY code is a one way street to unnecessary complexity by introducing more and more abstractions in an attempt to reuse code.

I guess I’m going to end with the intellectual dodge that principles like DRY are really just heuristics that may or might not help you think through what a good structure would be for the functionality you’re coding. They are certainly not black and white rules. In some cases, trying to be DRY will make your code more complex than it might be with a little bit more duplication of some simple code. In the case of the Marten LINQ provider however, I absolutely believe that applying a little bit of DRY with our usage of the Strategy pattern made that code significantly more robust, maintainable, and even simpler in many cases.

Sorry there’s no easy set of rules you can always follow to arrive at good code, but if there actually was, then AI is gonna eat all our lunches anyway.

One thought on “The Lowly Strategy Pattern is Still Useful

  1. I, surprisingly, fully agree with your approach to programming. Just use the tools, tricks, rules-of-thumb, patterns, schools-of-thought,.. as you see fit. Don’t get religious about it. Just apply them wisely.
    But then again, maybe you are just kicking in open doors, as modern programming languages include a bit of all these things, encouraging you to do just that.
    But it’s still good to repeat both the usefulness of patterns and expose the abuse.
    I remember a J2EE project where DB access was overly layered with empty interfaces and proxies, just in case the huge Oracle db ever got switched to something else. Something unlikely to ever happen. And in case it did happen, the “switchability” would be easy to accomplish, starting from a minimum of DB separation. The J2EE scalable philosophy had been taken a little to far.
    Another example (that I was guilty of myself) was enthusiastically wrapping every UI element and then having performance issues. One of my first and worst design pattern implementations!

Leave a comment