Using the Strategy Pattern (Examples in C#)
Sam Ferree

Sam Ferree @sam_ferree

About: Software Architect Specializing in the C# and ASP.NET Core But I've been lucky enough to dabble in Ruby, Python, Node.js, JAVA, C/C++, and more.

Location:
Midwest America.
Joined:
Feb 14, 2018

Using the Strategy Pattern (Examples in C#)

Publish Date: May 15 '18
117 12

Prerequisites

To get the most out of this post, it helps if you have a basic understanding of object oriented programming and inheritance, and an object oriented programming language like C# or Java. Although I hope you can get the main idea behind the strategy pattern even if you aren't an expert in C# syntax.

Example Problem

We're working on an application that keeps files in sync, and the initial requirements state that if a file exists in the destination directory but it doesn't exist in the source directory, then that file should be deleted. We might write something like this


public void Sync(Directory source, Directory destination)
{
    CopyFiles(source, destination)
    CleanupFiles(source, destination)
}

private void CleanupFiles(Directory source, Directory destination)
{
    foreach(var destinationSubDirectory in destination.SubDirectories)
    {
        var sourceSubDirectory = source.GetEquivalent(destinationSubDirectory);
        if(sourceSubDirectory != null)
        {
            CleanupFiles(sourceSubDirectory, destinationSubDirectory);
        }
        else
        {
            // The source sub directory doesn't exist
            // So we delete the destination sub directory
            destinationSubDirectory.Delete();
        }
    }

    // Delete top level files in this directory
    foreach(var file in destination.Files)
    {
        if(source.Contains(file) == false)
        {
            file.Delete();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Hey they looks pretty good! We used recursion, and it's all pretty readable. But then, as with all software projects, the requirements change. We find out that sometimes, we don't want to delete extra files. That's an awfully quick fix. We can do that by checking a flag.

public void Sync(Directory source, Directory destination)
{
    CopyFiles(source, destination)
    if(_shouldDeleteExtraFiles)
    {
        CleanupFiles(source, destination)
    }
}
Enter fullscreen mode Exit fullscreen mode

Because we separated the delete logic into it's own method, a simple If statement gets the job done. Until the requirements change again. Now we want the app to give the user the option to keep files that have the .usf extension. This requires us to make a change in our CleanupFiles method.

private void CleanupFiles(Directory source, Directory destination)
{
    foreach(var DestinationSubDirectory in destination.SubDirectories)
    {
        var sourceSubDirectory = source.GetEquivalent(DestinationSubDirectory);
        if(sourceSubDirectory != null)
        {
            CleanupFiles(sourceSubDirectory, DestinationSubDirectory);
        }
        else
        {
            // The source sub directory doesn't exist
            // So we delete the destination sub directory
            destinationSubDirectory.Delete();
        }
    }

    // Delete top level files in this directory
    foreach(var file in destination.Files)
    {
        if(_keepUSF && file.HasExtension("usf"))
        {
            continue;
        }

        if(source.Contains(file) == false)
        { 
            file.Delete();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Hmm, now we've added a couple of global flags, and we have the logic that depends on them spread across multiple methods. (Outside the scope of this post, we might also need to check that destination directories that don't exist in source contain files with the .usf extension.) We could really benefit from cleaning this code up. How should we separate our logic out so that the decision making about which delete logic to use, and the logic itself aren't so intertwined?

Enter the Strategy Pattern.

From w3sDesign.com:

the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.

Now I know I've been talking about the need to clean this code up, not select an algorithm at runtime. We already have the ability to change the algorithm at runtime, because of our use of flags, but we will see the benefits of cleanup.

First, let's code to an interface.

public interface IDeleteStrategy
{
    void DeleteExtraFiles(Directory source, Directory Destination);
}

Enter fullscreen mode Exit fullscreen mode

To simplify, let's say the client will pass this into our method, so we update it's signature and content like so:

public void Sync(
    Directory source,
    Directory destination,
    IDeleteStrategy deleteStrategy) //Expect to recieve a delete strategy
{
    CopyFiles(source, destination)

    //Call our delete strategy and let it handle the clean up
    deleteStrategy.DeleteExtraFiles(source, destination)
}
Enter fullscreen mode Exit fullscreen mode

Well, that certainly cleans up things. Now let's look at some implementations. First the trivial option, don't delete anything. If this strategy is passed in, the method call will do nothing, and any extra files in the destination directory will remain.

public class NoDelete : IDeleteStrategy
{
    public void DeleteExtraFiles(Directory source, Directory Destination)
    {
        //Do nothing!
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we could write two distinct strategies for deleting files that are not in source, and keeping USF files, but the policy for keeping USF files is an extension of the policy to delete files that are not in the source. So we can save some code here with inheritance.

Here is our strategy to delete files in the destination directory if they're not in the source directory. Note that we've kept the recursion from before to clean up sub-directories, but we've broken out the logic for deleting top level files, and deciding whether or not we should delete a file.

public class DeleteIfNotInSource : IDeleteStrategy
{
    public void DeleteExtraFiles(Directory source, Directory Destination)
    {
        foreach(var destinationSubDirectory in destination.SubDirectories)
        {
            var sourceSubDirectory = source.GetEquivalent(destinationSubDirectory);
            if(sourceSubDirectory != null)
            {
                // use recursion to pick up sub directories
                DeleteExtraFiles(sourceSubDirectory, destinationSubDirectory);
            }
            else
            {
                // The source sub directory doesn't exist
                // So we delete the destination sub directory
                destinationSubDirectory.Delete();
            }
        }

        DeleteTopLevelFiles(source, destination);
    }

    private void DeleteTopLevelFiles(Directory source, Directory destination)
    {
        foreach(var file in destination.Files)
        {
            if(ShouldDelete(file, source))
            { 
                file.Delete();
            }
        }
    }

    protected bool ShouldDelete(File file, Directory source)
    {
        return source.Contains(file) == false;
    }
}
Enter fullscreen mode Exit fullscreen mode

So now we actually just need to implement our strategy to Keep usf files by inheriting from DeleteIfNotInSource and overriding the ShouldDelete method!

public class KeepUSF : DeleteIfNotInSource
{
    public override bool ShouldDelete(File file, Directory source)
    {
        if(file.HasExtension("usf"))
        {
            return false;
        }

        //Defer to our base class
        return base.ShouldDelete(file, source);
    }
}
Enter fullscreen mode Exit fullscreen mode

So now if we needed to add a new strategy, we wouldn't have to touch any of our other code. We could create a new class that implements the IDeleteStrategy interface, and just add the logic for selecting it.

For instance, we can use something like configuration settings to select a strategy, then pass it into our sync method. Here's an example of what that might look like (using the Factory pattern, if you're looking for further reading)

// The application configuration tells us what delete strategy we are using
var deleteStrategyFactory = DeleteStrategyFactory.CreateFactory(configSettings);
...

// We don't know exactly what strategy we're getting, 
// better yet we don't care!
var deleteStrategy = deleteStrategyFactory.getDeleteStrategy();
SyncProcess.Sync(source, destination, deleteStrategy);

Enter fullscreen mode Exit fullscreen mode

Pumping the Brakes.

Some of you familiar with design patterns might be saying "Hey Sam, why didn't you implement the Keep USF functionality with the Decorator pattern!?" (More further reading if you're up for it.)

Sometimes, it's best not to apply a design pattern just because you can. In fact, this example was kept simple for educational purposes. An argument could have been made that the Strategy pattern here is overkill.

Make sure you weigh the gains from applying a design pattern against the cost of applying it. Applying the strategy pattern here added an interface, and three new classes to our project. In other words: "Make sure the juice is worth the squeeze."

Comments 12 total

  • Frank Carr
    Frank CarrMay 16, 2018

    This is one of my favorite patterns to use with things like manufacturing lines and work cells where there is a lot of common activity (barcode reading, inventory control, etc) but the implementation details vary to some degree from work area to area.

    • Corey Thompson
      Corey ThompsonMar 22, 2020

      Hey Frank, I know this is super old, but do you happen to have any examples around of the use case you described? I'm building basically the same type of system for manufacturing lines and trying to tackle the issue without blowing up our config files further. Also, for the first time, we'll have to conditionally load extern dll's in each strategy also, so I don't know how to prevent bundling in every extern dll into my deployment binaries.

  • Rafal Pienkowski
    Rafal PienkowskiMay 16, 2018

    Very nice post with a descriptive example of usage. Real life examples always are adding additional value to an example. I'm a big fan of design patterns too so I enjoyed this post.

    Personally I'd change inheritance to composition for ShouldDelete method, but once again great article.

    • Bruno
      BrunoMay 24, 2019

      Could you make an example?

      • Rafal Pienkowski
        Rafal PienkowskiMay 27, 2019

        Sure.

        Instead of inheriting from a base class and method overriding like here:

        public class MyClass:BaseClass
        {
            public override void Foo()
            {
               base.Foo();
            }
        }
        

        You add the class variable of a given class and calls it's method if needed.

        public class MyClass
        {
            private BaseClass _baseClass = new BaseClass();
        
            public void Foo()
            {
               _baseClass.Foo();
            }
        }
        

        More about the comparison between Inheritance and Composition you can find here. Take a closer look at the table at the end of article.

  • Boumboumjack
    BoumboumjackMay 16, 2018

    I used to do similar things, but for task that have a very similar output/process, I tend to use a "DeleteOption" argument to keep the process centralised. I find it easier to then save the parameters with a DataContract.

    I guess you use a dictionnary to select the correct function? I usually do this for very unrelated tasks:

    IDictionnary <myEnum.MethodName, myIAction>

    . Then it is as well quite easy to save it.

  • Kyle Galbraith
    Kyle GalbraithMay 16, 2018

    Great write up Sam! This is one of my favorite programming patterns to use and it can be applied it a lot of different languages. I wrote a post about the strategy pattern as well if folks are looking for more examples.

  • Adrian B.G.
    Adrian B.G.May 16, 2018

    Generally speaking, I am curious, how can one do automatic tests on this pattern?

    First step would be to test each strategy implementation I presume.
    Second would be to test if the correct strategy was selected based on the input?

    • Sam Ferree
      Sam FerreeMay 17, 2018

      That’s how I’d do it, Unit test the concrete implementations, and selection, then Integration test both.

  • Dhaval Charadva
    Dhaval CharadvaJan 10, 2019

    We are implementing data integration with netsuite. We are in process of using any good design pattern. What is best suggestion for data integration.

    Domain is e-commerce.

  • Bruno
    BrunoMay 24, 2019

    Nice article sir! Thank you.

Add comment