Using the Strategy Pattern (Examples in C#)

sam_ferree

Sam Ferree

Posted on May 15, 2018

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

💖 💪 🙅 🚩
sam_ferree
Sam Ferree

Posted on May 15, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related