← All Posts

What You Need To Know About The Helpful Strategy Pattern

Written by
Kyle Galbraith
Published on
29 January 2018
Share
I have recently been revisiting various coding patterns while learning new languages. One pattern that is a personal favorite of mine is the strategy pattern.
Build Docker images faster using build cache banner

I have recently been revisiting various coding patterns while learning new languages. One pattern that is a personal favorite of mine is the strategy pattern. The premise of the strategy pattern is to encapsulate a family of behaviors. When encapsulated we can then select which behavior to use at runtime.

For my example I am going to show the pattern in C#, but this is applicable to a wide array of languages. Let’s assume we are creating a piece of code that extracts content from text, pdf, and image files. Our family of behaviors is therefore text extraction. But we want to extract content in different ways based on the type of file.

To get started we define an interface called ITextExtractor.

public interface ITextExtractor
{
    bool UseExtractor(string fileExtension);
    string[] ExtractContent(string filePath);
}

For each type of document we want to extract content from we create a new class that implements ITextExtractor. Take note of the UseExtractor method as we will use this to select our extractor at runtime. Let’s go ahead and create the three text extractors.

PlainTextExtractor

public class PlainTextExtractor : ITextExtractor
{
    public bool UseExtractor(string fileExtension)
    {
        return fileExtension.Equals("txt", StringComparison.OrdinalIgnoreCase);
    }

    public string[] ExtractContent(string filePath)
    {
        // extract the content here...
    }
}

PlainTextExtractor will return true from UseExtractor only if the file extension ends in txt.

PdfTextExtractor

public class PdfTextExtractor : ITextExtractor
{
    public bool UseExtractor(string fileExtension)
    {
        return fileExtension.Equals("pdf", StringComparison.OrdinalIgnoreCase);
    }

    public string[] ExtractContent(string filePath)
    {
        // extract the content here...
    }
}

PdfTextExtractor will return true from UseExtractor only if the file extension ends in pdf.

ImageTextExtractor

public class ImageTextExtractor : ITextExtractor
{
    private string[] _imageTypes = new[] { "png", "jpg", "jpeg", "tiff" };
    public bool UseExtractor(string fileExtension)
    {
        return _imageTypes.Any(it => it.Equals(fileExtension, StringComparison.OrdinalIgnoreCase);
    }

    public string[] ExtractContent(string filePath)
    {
        // extract the content here...
    }
}

ImageTextExtractor will return true from UseExtractor only if the file extension ends in png, jpg, jpeg, or tiff. There is far more image types than this, but this gives you an idea of what we are after.

The ¯_(ツ)_/¯ Approach To Selecting Our Strategy At Runtime

Now we have our various text extractors. When it comes to selecting the appropriate extractor at runtime you often see code written like this.

public class RunExtraction
{
    private PlainTextExtractor _plainTextExtractor;
    private PdfTextExtractor _pdfTextExtractor;
    private ImageTextExtractor _imageTextExtractor;

    public RunExtraction(PlainTextExtractor plainTextExtractor,
        PdfTextExtractor  pdfTextExtractor, ImageTextExtractor imageTextExtractor)
    {
        _plainTextExtractor = plainTextExtractor;
        _pdfTextExtractor = pdfTextExtractor;
        _imageTextExtractor = imageTextExtractor;
    }

    public string[] Extract(string filePath, string fileExtension)
    {
        if(_plainTextExtractor.UseExtractor(fileExtension))
        {
            return _plainTextExtractor.ExtractContent(filePath);
        }
        else if(_pdfTextExtractor.UseExtractor(fileExtension))
        {
            return _pdfTextExtractor.ExtractContent(filePath);
        }
        else if(_imageTextExtractor.UseExtractor(fileExtension))
        {
            return _imageTextExtractor.ExtractContent(filePath);
        }
        else
        {
            throw new Exception("unable to extract content");
        }
    }
}

There is technically nothing wrong with this code. But is it the most extensible for the future? What if we had to start extracting content from a Word document?

First, we would create a new WordDocumentTextExtractor class that implements ITextExtractor.

public class WordDocumentTextExtractor : ITextExtractor
{
    private string[] _docTypes = new[] { "doc", "docx" };
    public bool UseExtractor(string fileExtension)
    {
        return _docTypes.Any(it => it.Equals(fileExtension, StringComparison.OrdinalIgnoreCase);
    }

    public string[] ExtractContent(string filePath)
    {
        // extract the content here...
    }
}

We would then have to update the RunExtraction class to take the WordDocumentTextExtractor in the constructor.

private PlainTextExtractor _plainTextExtractor;
private PdfTextExtractor _pdfTextExtractor;
private ImageTextExtractor _imageTextExtractor;
private WordDocumentTextExtractor _wordDocumentTextExtractor;

public RunExtraction(PlainTextExtractor plainTextExtractor,
    PdfTextExtractor  pdfTextExtractor, ImageTextExtractor imageTextExtractor,
    WordDocumentTextExtractor wordDocumentTextExtractor)
{
    _plainTextExtractor = plainTextExtractor;
    _pdfTextExtractor = pdfTextExtractor;
    _imageTextExtractor = imageTextExtractor;
    _wordDocumentTextExtractor = wordDocumentTextExtractor;
}

We would then need to add another else if statement to check for Word documents.

else if(_wordDocumentTextExtractor.UseExtractor(fileExtension))
{
    return _wordDocumentTextExtractor.ExtractContent(filePath);
}

This becomes unruly if we are constantly having to extract content from different types of documents. Each time we have to:

  1. Add the new text extraction class.
  2. Pass the new class into RunExtraction.
  3. Update the else if conditions to detect the new document type.

My anxiety level is rising already. There must be a different approach right?

The q(❂‿❂)p Approach To Selecting Our Strategy At Runtime

Lucky for us we set ourselves up for success with our strategy pattern. Every text extractor implements the same interface ITextExtractor. In that interface we added the method UseExtractor. It returns true or false based on the extensions each extractor supports. We can leverage both of those things to our advantage.

First, we change what is passed into the constructor of RunExtraction.

private ITextExtractor[] _extractors;

public RunExtraction(ITextExtractor[] extractors)
{
    _extractors = extractors;
}

Notice we no longer pass in the concrete classes for each type of extractor. Instead we pass in an array of ITextExtractor. This is so that when we want to add a new type of extractor we just add it to the array passed in.

Next, we can change the Extract method of RunExtraction to no longer use if else ifelse if.

public string[] Extract(string filePath, string fileExtension)
{
    var extractor = _extractors.FirstOrDefault(e => e.UseExtractor(fileExtension));
    if(extractor != null)
    {
        return extractor.ExtractContent(filePath);
    }
    else
    {
        throw new Exception("unable to extract content");
    }
}

Goodbye else if and hello extensibility. Our RunExtract class can now easily support new document text extractors. Now when we want to add our WordDocumentTextExtractor here are the steps we need to complete:

  1. Add the new text extraction class.
  2. Add the new class to the passed in array of extractors to RunExtraction.

Conclusion

Here we have covered the basics of strategy pattern.

  • Define an interface for similar functionality. (like text extraction)
  • Create concrete classes that implement that functionality.
  • Use dependency injection and interfaces to seal your logic class from changes.
  • Dynamically select the necessary functionality at runtime.

Then watch as your code becomes more extensible in the future as different types of concrete classes are added.

© 2024 Kyle Galbraith