loading...
Cover image for What You Need To Know About The Helpful Strategy Pattern

What You Need To Know About The Helpful Strategy Pattern

kylegalbraith profile image Kyle Galbraith Updated on ・4 min read

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 if....else 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.

Hungry To Learn Amazon Web Services?

There is a lot of people that are hungry to learn Amazon Web Services. Inspired by this fact I have created a course focused on learning Amazon Web Services by using it. Focusing on the problem of hosting, securing, and delivering static websites. You learn services like S3, API Gateway, CloudFront, Lambda, and WAF by building a solution to the problem.

There is a sea of information out there around AWS. It is easy to get lost and not make any progress in learning. By working through this problem we can cut through the information and speed up your learning. My goal with this book and video course is to share what I have learned with you.

Sound interesting? Check out the landing page to learn more and pick a package that works for you, here.

Posted on Jul 2 '19 by:

kylegalbraith profile

Kyle Galbraith

@kylegalbraith

Programmer by day and author by night. I am passionate about all things development related, but especially Amazon Web Services. I recently created a course about learning AWS by using it.

Discussion

markdown guide
 

Nice article.

Apologies if this is obvious, but I like to use reflection to find classes implementing ITextExtractor followed by Activator.CreateInstance to instantiate them and add them to the array at runtime, thereby reducing the steps needed in adding a new implementation to just step 1 in your list above - "Add the new text extraction class".

Of course, this is only useful if the order of the items in the array isn't important!

 

James that is a slick idea. I would like to see a demo of that with some code so that I can wrap my head around it further. Thank you for the comments.

 

Sure! Might not be suitable for a group project, it makes assumptions that all ITextExtractor classes have parameterless constructors which isn't immediately obvious and wouldn't go down well in my employers code reviews, but for personal projects I find it useful! Something along the lines of this:

private ITextExtractor[] _extractors;

public RunExtraction()
{
    _extractors =  AppDomain.CurrentDomain.GetAssemblies()
        .SelectMany(x => x.GetTypes())
        .Where(t => typeof(ITextExtractor).IsAssignableFrom(t) && !t.IsInterface)
        .Select(t => (ITextExtractor)Activator.CreateInstance(t))
        .ToArray();
}
 

Great post sir, I have some points though.
At run time the "ifs and elses" and "_extractors.FirstOrDefault(e => e.UseExtractor(fileExtension));" aproach would have not differences, right? Because all the latter line does, is to get the first "extrator" that first satisfies the condition by doing "ifs and elses" as well...
I think your code would take more advantage of interfaces if it was like this:

public string[] Extract(string filePath, ITextExtractor extractor)
{
    if(extractor != null)
    {
        return extractor.ExtractContent(filePath);
    }
    else
    {
        throw new Exception("unable to extract content");
    }
}

To decide at run time what Strategy to use, you might use some parameterized Factory Method.
Let me know what you think please.

 

This could certainly work but it loses the benefit of abstracting out the extractors. The idea is that each extractor knows what it can extract and it indicates that with the function that finds it.

This provides nice encapsulation by allowing the class that implements the interface to decide what it can extract.

Your idea would be quite different and more akin to the Factory pattern.

 

I might overuse this pattern, but I love working with it!

Tip: Use an IOC to setup all the ITextExtractors on startup and you dont have to worry about missing a step when adding another ITextExtractor

 

Great explanation, short and to the point. Always good to get refreshers on design patterns.