Last Update:
Applying the Strategy Pattern
Table of Contents
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 the Strategy pattern can help?
Basic Solution
We can easily came 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 seems 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 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();
// scale down?
if (nodeWidth > parentWidth) {
// this should scale down the object...
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;
// 'simplified' method
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();
Play with the code on Coliru online compiler: link to the file
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 do 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 be also 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’s you to add 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.
Just to remember: you can play with the code on Coliru online compiler: link to the file
Your turn
- What do you think about the proposed design?
- Would you use that in production code?
Reference
- Effective C++, 3rd edition< /> : I was inspired to write this post after reading item 35 “Consider alternatives to virtual functions.”
- Wiki - Strategy pattern
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: