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:
- Add the new text extraction class.
- Pass the new class into
RunExtraction
. - 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:
- Add the new text extraction class.
- 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.