Design patterns Ep.9 - Behavioral - Strategy
In the previous articles, we explored details on the following behavioral design patterns:
In today's article, we are going to examine another behavioral design pattern and in particular, the Strategy pattern.
Problem
Let us understand the Strategy Design Pattern with a simple problem statement. Imagine we want to build an application, where our users will be given the ability to upload their images. Our application will get those images, apply some "changes" on them and finally save them in some kind of storage space.
We will start by creating a class named ImageStorage and in this class, we will have a method named Store. This method will take a single string as an argument called fileName. Let's imagine now that before saving this image to some storage space, this method does a few different things on that image first:
To achieve something like the above, we will introduce two new enum types in our application, one for the type of compression algorithm (Compressor) and the other for the type of filter (Filter), the ImageStorage class will apply to every image it is going to store.
We will take these two values as constructor parameters in our ImageStorage class and initialize them as property fields of the class. Inside the Store method, before actually saving those images in some imaginary storage space, the ImageStorage class will try and apply a compression and a filter algorithm on the image, based on the values of its Compressor and Filter properties like below:
Now it seems like we have been able to initially achieve what we wanted. But what are the problems with the above code? First of all, the ImageStorage class is violating the Single Responsibility Principle (S in SOLID), because it has several different tasks to accomplish:
The second problem we have, is the two sets of decision making statements - the two switch/case - we are seeing in the above image. The issue here is, of course, that is extremely difficult for us to support new types of compression or filter algorithms. Every time we want to introduce either a new compression algorithm or a new filter in our application, we have to go inside the ImageStorage class and add a new case statement in two places. This over time will become unmaintainable and as a result our application is not extensible. Imagine we have the same decision making logic in other parts of our application as well.
Solution
Before diving into solving the above mentioned problems, let's try to answer a question. Which are the parts that vary inside the ImageStorage class? The answer is the Compressor and the Filter we apply, before storing an image. To solve these problems we can use the Polymorphism principle of Object Oriented Programming, because we want our ImageStorage class to behave differently, depending on the type of Compressor and Filter we are using.
Let's see how to extract the Compressor type part of the ImageStorage class as an example. First of all, we will introduce a new interface called ICompressor, that will contain a single method called Compress(). Then we will create classes that implement this new interface (e.g. JpegCompressor, PngCompressor, etc.). With this change, we are following the Open Closed Principle (O in SOLID), because if in the future we want to support a different compression algorithm, we would just need to create a new class that will implement the ICompressor interface and not change any of our existing classes.
Now, in our ImageStorage class above, we need to change the compressor property from a Compressor enum type to an ICompressor interface. So when we create a new ImageStorage class, we will need to pass it a concrete object that will implement the ICompressor interface (like JpegCompressor or PngCompressor). We can use the exact same steps for extracting the Filter type part of the ImageStorage class as well.
Without even knowing it, we started applying the Strategy Pattern in our code. Strategy is a behavioral design pattern that lets you define a family of algorithms (e.g. ICompressor interface we mentioned above), put each of them into a separate class (e.g. JpegCompressor, PngCompressor, etc.), and make their objects interchangeable.
The Strategy pattern suggests that you take a class that does something specific (or many different things) in a lot of different ways and extract all of these algorithms into separate classes called Strategies. The original class (ImageStorage), called Context, must have a field for storing a reference to one of the strategies. The context then just delegates the work to a linked strategy object instead of executing it on its own.
The context isn’t responsible for selecting an appropriate algorithm for the job on its own. Instead, the client code passes the desired strategy to the context. The context then works with all strategies through the same generic interface, which only exposes a single method for triggering the algorithm encapsulated within the selected strategy. That means, the context becomes independent of concrete strategies. As a result, we can add new algorithms or modify existing ones without changing the code of the context or other strategies.
Let's examine some details of the pattern and see some code in C#.
Recommended by LinkedIn
Structure
Participants
The output of the above code would be:
Applicability
Use the Strategy pattern when:
You can find the source code for the above example here.
That's all for today. Cheers!