Introduction
Let’s look at the following problem:
We are designing a drawing application. We want some objects to be automatically scaled to fit inside parent objects. For example: when you make a page wider, images can decide to scale up (because there’s more space). Or if you make a parent box narrower, image needs to scale down.
What are the design and implementation choices that we can make? And, how can the Strategy pattern help?
Basic Solution
We can easily come up with the following class design:
class IRenderableNode
{
virtual void Transform() = 0;
virtual void ScaleToFit() = 0; };
class Picture : public IRenderableNode
{
void Transform();
void ScaleToFit();
};
The ScaleToFit
method should do the job. We can write implementations for various objects that need to have the indented behaviour. But, is this the best design?
The main question we should ask: is scaling to fit a real responsibility of IRenderableNode
? Maybe it should be implemented somewhere else?
Let’s ask some basic questions before moving on:
- Is feature X a real responsibility of the object?
- Is feature X orthogonal to class X?
- Are there potential extensions to feature X?
For our example:
- Scaling to Fit seems to be not the core responsibility of the Picture/Renderable object. The
Transform()
method looks like main functionality. ScaleToFit
might be probably built on top of that. - Scaling To Fit might be implemented in different ways. For example, we might always get bounding size from the parent object, but it can also skip parents and get bounding box from the page or some dynamic/surrounding objects. We could also have a simple version for doing a live preview and more accurate one for the final computation. Those algorithm versions seem to be not related to the particular node implementation.
- Additionally, Scaling to fit is not just a few lines of code. So there is a chance that with better design from the start, it can pay off in the future.
The Strategy Pattern
A quick recall of what this pattern does…
From wiki:
The strategy pattern
- defines a family of algorithms,
- encapsulates each algorithm, and
- makes the algorithms interchangeable within that family.
Translating that rule to our context: we want to separate scaling to fit methods from the renderable group hierarchy. This way, we can add different implementations of the algorithm without touching node classes.
Improved Solution
To apply the strategy pattern, we need to extract the scaling to fit algorithm:
class IScaleToFitMethod
{
public:
virtual void ScaleToFit(IRenderableNode *pNode) = 0;
};
class BasicScaleToFit : public ScaleToFitMethod
{
public:
virtual void ScaleToFit(IRenderableNode *pNode) {
cout << "calling ScaleToFit..." << endl;
const int parentWidth = pNode->GetParentWidth();
const int nodeWidth = pNode->GetWidth();
if (nodeWidth > parentWidth) {
pNode->Transform();
}
}
};
The above code is more advanced than the simple virtual method ScaleToFit
. The whole algorithm is separated from the IRenderableNode
class hierarchy. This approach reduces coupling in the system so now we can work on algorithm and renderable nodes independently. Strategy also follows the open/closed principle: now, you can change the algorithm without changing the Node class implementation.
Renderable objects:
class IRenderableNode
{
public:
IRenderableNode(IScaleToFitMethod *pMethod) :
m_pScaleToFitMethod(pMethod) { assert(pMethod);}
virtual void Transform() = 0;
virtual int GetWidth() const = 0;
virtual int GetParentWidth() const = 0;
void ScaleToFit() {
m_pScaleToFitMethod->ScaleToFit(this);
}
protected:
IScaleToFitMethod *m_pScaleToFitMethod;
};
The core change here is that instead of a virtual method ScaleToFit
, we have a “normal” non virtual one and it calls the stored pointer to the actual implementation of the algorithm.
And now the ‘usable’ object:
class Picture : public IRenderableNode
{
public:
using IRenderableNode::IRenderableNode;
void Transform() { }
int GetWidth() const { return 10; }
int GetParentWidth() const { return 8;
};
The concrete node objects don’t have to care about scaling to fit problem.
One note
: Look at the using IRenderableNode::IRenderableNode;
- it's an inherited constructor from C++11. With that line, we do not have to write those basic constructors for the `Picture
` class, we can invoke bases class constructors.
The usage:
BasicScaleToFit scalingMethod;
Picture pic(&scalingMethod);
pic.ScaleToFit();
Here is a picture that tries to describe the above design:
Notice that Renderable Nodes aggregate the algorithm implementation.
We could even go further and not store a pointer to the implementation inside RenderbleObject
. We could just create an algorithm implementation in some place (maybe transform manager) and just pass nodes there. Then, the separation would be even more visible.
Problems
Although the code in the example is very simple, it still shows some limitations. Algorithm takes a node and uses its public
interface. But what if we need some private
data? We might extend the interface or add friends?
There might also be a problem that we need some special behaviour for a specific node class. Then we might need to add more (maybe not related?) methods into the interface.
Other options
While designing you can also look at the visitor pattern.
Visitor is more advanced and complicated pattern but works nicely in a situations where we often traverse hierarchies of nodes and algorithm need to do different things for different kind of objects. In our case, we might want to have specific code for Pictures
and something else for a TextNode
. Visitors also let you add a completely new algorithm (not just another implementation) without changing the Node
classes code.
Below, there is a picture with a general view of the visitor pattern.
Another idea might be to use std::function
instead of a pointer to an algorithm interface. This would be even more loosely coupled. Then you could use any callable object that accepts interface param set. This would look more like Command pattern.
Although the strategy pattern allows in theory for dynamic/runtime changes of the algorithm, we can skip this and use C++ templates. That way, we’ll still have the loosely coupled solution, but the setup will happen in compile time.
Summary
I must admit I rarely considered using the strategy pattern. Usually, I choose just a virtual
method… but then, such decision might cost me more in a long run. So it's time to update my toolbox.
Things to remember:
The strategy pattern allows you to separate an algorithm from the family of objects.
In real life, quite often, you start with some basic implementation and then, after requirement change, bugs, you end up with a very complicated solution for the algorithm. In the latter case, the strategy pattern can really help. The implementation might be still complicated, but at least it’s separated from objects. Maintaining and improving such architecture should be much easier.
Reference
History
- 23rd September, 2015 - Initial version