DEV Community

Sam Ferree
Sam Ferree

Posted on

Using the Strategy Pattern (Examples in C#)

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

Top comments (12)

Collapse
 
jfrankcarr profile image
Frank Carr

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.

Collapse
 
thompcd profile image
Corey Thompson

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.

Collapse
 
rafalpienkowski profile image
Rafal Pienkowski

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.

Collapse
 
brunobck profile image
Bruno

Could you make an example?

Collapse
 
rafalpienkowski profile image
Rafal Pienkowski

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.

Collapse
 
bgadrian profile image
Adrian B.G.

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?

Collapse
 
sam_ferree profile image
Sam Ferree

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

Collapse
 
boumboumjack profile image
Boumboumjack

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.

Collapse
 
kylegalbraith profile image
Kyle Galbraith

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.

Collapse
 
brunobck profile image
Bruno

Nice article sir! Thank you.

Collapse
 
dhavalcharadva profile image
Dhaval Charadva • Edited

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.