The definition of strategy patterns is as follows: „Strategy patterns define a family of algorithms, encapsulate each one, and make them interchangeable.“ The strategy allows the algorithm to vary independently of the clients who use it. Okay, now we understand strategy patterns. Very simple.
Joking aside, let’s look at the definition and analyze it. So we need a set of algorithms, i.e. pieces of code that perform certain tasks. They must belong to a family, which means that they must have something in common. They also need to be encapsulated and interchangeable, which means we can use one instead of the other. After reading the definition, the following questions may arise:
- Why do I need to encapsulate algorithms and make them interchangeable? Can’t I just change the algorithm when necessary?
- How do I do it?
Why do I need to encapsulate algorithms
and make them interchangeable?
To answer the first question, let me give you the following example:
A company manufactures blocks that produce ice cream. The block uses an algorithm to produce a certain type of ice cream and to make the lamp light up in a certain color when it is finished. Now let’s imagine the following scenario that this company experiences with its customers.
Day 1
Day 2
Day 257
Our company had an idea.
What does this story show us? Binding the algorithm to the block makes it inflexible. Implementing changes or maintaining the block is more difficult. The strategy design pattern shown in the last picture provides us with a solution where the algorithms are encapsulated and inserted into the block. This reduces the dependency between the block and the algorithm.
There are a few things we need to consider:
- All algorithms that are encapsulated have similar but not identical functions. They are all algorithms that produce certain types of ice cream.
- The algorithms are encapsulated in a specific way, because the block will only accept a certain form of encapsulation.
How to do it?
Step 1: Create an interface.
Before the algorithms are encapsulated in the form of objects, a certain signature needs to be defined that dictates what the encapsulation must look like. As shown in the image above, all algorithms must have a certain form in order to fit into the block. That means that each block must have a specific gate that only accepts correctly encapsulated algorithms. The interface in OOP provides such a mechanism. So, by using an interface we can define the operation signature of the objects. The interface of an object defines the complete set of requests that can be sent to the object. Any request that matches a signature in the object’s interface can be sent to the object. Also for the client (block), the interface provides a mechanism to define the type of object that can be inserted without explicitly specifying the details. This gives the programmer the flexibility to have different variations of algorithms that can be inserted into the client (block). Let’s define an interface for our example.
INTERFACE IceCreamProduction
METHOD produceIceCream
END_METHOD
Step 2: Create specific classes based on the interface.
After the interface is defined, we need to encapsulate our algorithms accordingly. In our case, we want to produce different types of ice cream.
FUNCTION_BLOCK ChocolateIceCreamProduction IMPLEMENTS IIceCreamProduction
METHOD produceIceCream
// here is code to produce chocolate ice-cream
END_METHOD
FUNCTION_BLOCK VanillaIceCreamProduction IMPLEMENTS IIceCreamProduction
METHOD produceIceCream
// here is code to produce vanilla ice-cream
END_METHOD
FUNCTION_BLOCK PlainIceCreamProduction IMPLEMENTS IIceCreamProduction
METHOD produceIceCream
// here is code to produce plain ice-cream
END_METHOD
FUNCTION_BLOCK ChocolateVanillaIceCreamProduction IMPLEMENTS IIceCreamProduction
METHOD produceIceCream
// here is code to produce ChocolateVanilla ice-cream
END_METHOD
Step 3: Create an IceCreamBlock class.
We have encapsulated the algorithms based on our interface. Now it is time to create our block or class that the algorithms will be inserted into. The encapsulated algorithm (object) will be inserted using a constructor. In the meantime, it is possible to alter the algorithm on runtime by using the setter method of the property.
FUNCTION_BLOCK IceCreamBlock
VAR
iceCreamProduction: IIceCreamProduction;
END_VAR
METHOD FB_init
VAR_INPUT
bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change)
iceCreamProduction: IIceCreamProduction;
END_VAR
END_METHOD
PROPERTY IeCreamProductionAlgorithm : IIceCreamProduction
GET
IceCreamProductionAlgorithm:= this^.iceCreamProduction;
END_GET
SET
this^.iceCreamProduction:= IceCreamProductionAlgorithm;
END_SET
END_PROPERTY
METHOD produceIceCream
this^.iceCreamProduction.produceIceCream();
END_METHOD
Why not use inheritance instead?
In the scenario described, there are different versions of the same block, and they share similarities. It seems like a perfect use case for inheritance. Define a base class and then inherit all variations from that class. However, a problem arises when the base class needs to be change. There might be a situation where the base class only needs to be changed for one group of its children, but not for all of them. Now, this problem could be solved by introducing another level of abstraction to separate these two groups. But what if, over time, those two classes need to be divided again, but in such a way that they need some, but not all, of the behaviors of the other groups?
If there are parts of a program that are always changing, it is better to isolate the changing parts from the constant parts. This is what strategy design patterns provide. Take the parts that change and encapsulate them. This way you can later change or extend the changing parts without affecting the constant parts.