Partners: KDAB Whole Tomato Software CppDepend

27 May 2015

Applying the Strategy Pattern

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:

Applying the strategy pattern

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.

]the visitor design pattern overview

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

Get my free ebook about C++17!

More than 50 pages about the new Language Standard.

C++17 in detail, by Bartlomiej Filipek

For now I don't have my own courses, but I promote others :) (Warning: I'll also get a little commission for every signup). Have a look my recommended C++ courses at @Pluralsight (more info in my Resource page):

© 2017, Bartlomiej Filipek, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.