Design patterns Ep.9 - Behavioral - Strategy

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:

  • It will first apply some compression algorithm on it (e.g. JPEG, PNG, etc.), so that it can reduce the size of the image before saving it inside the storage space.
  • It will then apply some kind of a filter, (e.g. black and white, high contrast, etc.), so that all the images saved inside our imaginary storage to have a common look and feel.

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.

No alt text provided for this image

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:

No alt text provided for this image

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:

  • Apply a compression algorithm to the image
  • Apply a filter algorithm to the image
  • Save the image in some storage space

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#.

Structure

No alt text provided for this image

Participants

  • Strategy (ICompressorStrategy, IFilterStrategy): Declares an interface common to all supported algorithms. Context uses this interface to call the algorithm defined by ConcreteStrategy.

No alt text provided for this image
No alt text provided for this image

  • ConcreteStrategy (JpegCompressor, PngCompressor, BlackAndWhiteFilterProcessor, HighContrastFilterProcessor): Implements the algorithm using the Strategy interface

No alt text provided for this image
No alt text provided for this image
No alt text provided for this image
No alt text provided for this image

  • Context (ImageStorage): It is configured with one or more ConcreteStrategy objects. It also maintains a reference to one or more Strategy objects and may define an interface that lets Strategy access its data

No alt text provided for this image

  • Client (Program): Initializes the Context object and passes the desired strategies to it either or initialization (through the constructor), or at runtime when calling the Context's method (if the Context method supports giving different strategy implementations at runtime - see Store method of ImageStorage class for our example)

No alt text provided for this image

The output of the above code would be:

No alt text provided for this image

Applicability

Use the Strategy pattern when:

  1. Many related classes differ only in their behavior. Strategies provide a way to configure a class with one of many behaviors
  2. You need different variants of an algorithm. For example, you might define algorithms reflecting different space/time trade-offs.
  3. An algorithm uses data that clients shouldn't know about. Use the Strategy pattern to avoid exposing complex, algorithm-specific data structures
  4. A class defines many behaviors, and these appear as multiple conditional statements in its operations. Instead of many conditionals, move related conditional branches into their own concrete Strategy class.

You can find the source code for the above example here.

That's all for today. Cheers!

To view or add a comment, sign in

More articles by Orestis Meikopoulos

Insights from the community

Others also viewed

Explore topics