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.
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.
Top comments (7)
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:
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:
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.