Click here to Skip to main content
14,634,898 members
Articles » Development Lifecycle » Design and Architecture » Patterns and Practices
Tip/Trick
Posted 23 Sep 2015

Stats

11.9K views
89 downloads
11 bookmarked

Applying the Strategy Pattern

Rate this:
4.74 (12 votes)
Please Sign up or sign in to vote.
4.74 (12 votes)
23 Sep 2015CPOL
How to apply the strategy pattern to a problem while designing a class hierarchy. What are the pros and cons of this approach?

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();

  // 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();

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

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

Reference

History

  • 23rd September, 2015 - Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Bartlomiej Filipek
Software Developer
Poland Poland
Software developer interested in creating great code and passionate about teaching.

Author of C++17 In Detail - a book that will teach you the latest features of C++17!

I have around 11 years of professional experience in C++/Windows/Visual Studio programming. Plus other technologies like: OpenGL, game development, performance optimization.

In 2018 I was awarded by Microsoft as MVP, Developer Technologies.

If you like my articles please subscribe to my weekly C++ blog or just visit www.bfilipek.com.

Comments and Discussions

 
QuestionRather use method injection Pin
Louis van Alphen27-Sep-15 5:52
MemberLouis van Alphen27-Sep-15 5:52 
AnswerRe: Rather use method injection Pin
Bartlomiej Filipek27-Sep-15 20:01
MemberBartlomiej Filipek27-Sep-15 20:01 
GeneralRe: Rather use method injection Pin
Louis van Alphen27-Sep-15 20:56
MemberLouis van Alphen27-Sep-15 20:56 
GeneralMy vote of 4 Pin
Pierre Nortje24-Sep-15 23:40
professionalPierre Nortje24-Sep-15 23:40 
GeneralRe: My vote of 4 Pin
Bartlomiej Filipek24-Sep-15 23:42
MemberBartlomiej Filipek24-Sep-15 23:42 
GeneralMy vote of 4 Pin
Abhishek Kumar Goswami23-Sep-15 21:48
professionalAbhishek Kumar Goswami23-Sep-15 21:48 
GeneralRe: My vote of 4 Pin
Bartlomiej Filipek23-Sep-15 22:02
MemberBartlomiej Filipek23-Sep-15 22:02 
GeneralRe: My vote of 4 Pin
hooodaticus25-Sep-15 8:59
Memberhooodaticus25-Sep-15 8:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.