Applying Strategy Pattern in C++ Applications






4.71/5 (15 votes)
Jan 9, 2001

128408

1148
When it is possible to have several different algorithms for performing a process, Strategy Pattern can be used to determine the best solution.
Introduction
Software consulting companies do projects for their customers on a "Fixed Price basis" or on a "Time and Material basis". Also, the projects can be either onsite or offsite. Usually, the customers specify how they want the project to be done (Fixed price or Time and Material basis, onsite or offsite). The ultimate aim of the consulting company is to complete the project in the scheduled time, however the Strategy (or the policy) they adapt in doing the project may differ, depending on how they do the project. This is a real life example, where a Strategy Pattern is applied.
Strategy Pattern can also be used in the software design. When it is possible to have several different algorithms for performing a process, each of which is the best solution depending on the situation, then a Strategy Pattern can be used. This article is all about Strategy Pattern. It uses a programming example to explain what, when and why a Strategy Pattern is needed. Benefits and drawbacks of using Strategy Pattern in software design is discussed. Three different approaches for implementing the Pattern in C++ and known uses of Strategy Pattern are also presented in this article.
Design Patterns are meant to provide a common vocabulary for communicating design principles. Strategy Pattern is classified under Behavioral Patterns in the book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (Addison-Wesley, 1995). In this article, I will be using the terms used by 'Gang of Four (GoF)' to explain Strategy Pattern.
An Example
A progress indicator is a window that an application can use to indicate the progress of a lengthy operation (for example, an Installation Process). It is usually a rectangular window that is gradually filled, from left to right, with the highlight color as the operation progresses. It has a range and a current position. The range represents the entire duration of the operation, and the current position represents the progress the application has made towards completing the operation. The range and the current position are used to determine the percentage of the progress indicator to fill with the highlight color.
Even though left to right direction is commonly used for filling in most progress indicators, other directions like right to left, top to bottom and bottom to top can also be used for filling. I have seen some progress indicators using a bottom to top filling. Also, different types of fills like continuous fill, broken fill or pattern based fills can be used with a given filling direction.
In short, the purpose of the progress indicator remains unchanged; however, the filling direction or the filling algorithm can change. Therefore, the family of algorithms used for filling can be encapsulated in a separate filler class hierarchy and the application can configure the progress indicator with a concrete filler class. An algorithm that is encapsulated in this way is called a Strategy. So, what is a Strategy Pattern? The Strategy Pattern is a design pattern to encapsulate the variants (algorithms) and swap them strategically to alter system behavior without changing its architecture. According to GoF, Strategy Pattern is intended to, Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Strategy Pattern has three participants that include Strategy, Concrete Strategy and Context. In this example, the abstract filler class CFiller
, is referred as the Strategy, the concrete filler classes CLToRFiller
(for providing Left to Right fill) and CRToLFiller
(for providing Right to Left fill) are referred as Concrete Strategies and the progress indicator CProgressIndicator
, is referred as the Context using Strategy. The application using the progress indicator is the client for the Context. Depending on the situation, the client specifies the progress indicator (Context
) with a concrete filler class object (Concrete Strategy).
CProgressIndicator
maintains a reference to the CFiller
object. Whenever there is a progress in the operation, the application notifies CProgressIndicator
(by calling a method like SetPos
); the CProgressIndicator
forwards the request to the CFiller
object to visually indicate the change. CFiller
subclasses, CLToRFiller
and CRToLFiller
implement the filling algorithm (in DoFill
method). By isolating the filling algorithm from the progress indicator, new filling strategies can be used without changing the progress indicator. Encapsulating the filling algorithm separately eliminates the need for multiple conditional statements to choose the right Strategy for filling. UML diagram showing the relationship between the participants of the Strategy Pattern is presented below.
Approaches for Implementing Strategy Pattern in C++
Push
and Pull
methods can be used to provide a means of safe data exchange and reduce the coupling between the Context and Strategy objects. In the Push
method, the Context
pushes the data to the Strategy
object and the Strategy
uses the data to implement the algorithm. In this method, Context
might pass some unwanted data to the Strategy
object, as all Concrete Strategy
objects might not require all the data. On the other hand, in the Pull
method, the Context
, registers itself with the Strategy
which in-turn maintains a reference to the Context
object and pulls the required data from it. In this method, the Context
must define an elaborate set of Get
methods for the Strategy
objects to pull the required data. Since the Strategy
maintains a reference to the Context
, both the classes are tightly coupled. The choice of Push
or Pull
method purely depends on the requirement.
This article discusses three different approaches for implementing the Strategy Pattern in C++. The approaches described below can use either a Push
or Pull
method:
- Strategy object as a required parameter to the Context
- Strategy object as an optional parameter to the Context
- Strategy as a template class parameter to the Context
Strategy Object as a Required Parameter to the Context
In this approach, the progress indicator (Context
) takes a filler (Strategy
) object as a parameter in its constructor and maintains a reference to it. The progress indicator delegates the request to the filler object when SetPos
method is called. Listing 1 shows this approach. Also, Layout Manager in Java uses this approach, see Java and Strategy Pattern for explanation.
Advantages
- The progress indicator depends only on the interface of the filler class and does not interact directly with the concrete subclasses of the filler class.
- Application can select the required filler class object at run-time.
SetFiller
method can be used to change the filler class object after creating the progress indicator.
Disadvantages
- Application using the progress indicator must be aware of all the filler classes and must supply the progress indicator with the required filler class object.
- Progress indicator is not having any control on the scope or the lifetime of the filler class object.
Strategy Object as an Optional Parameter to the Context
This approach is similar to the first approach, but the filler object (Strategy
) is taken as an optional parameter when progress indicator (Context
) is created. The progress indicator creates a default filler object (Left to Right filler), if the application did not specify the filler object during construction. Listing 2 contains C++ sample showing this approach. Demo application provided with this article uses this technique.
Advantages
- Application can specify the filler object only when it needs to change the default filler object.
- Application can select the required filler class object at run-time.
SetFiller
method can be used to change the filler class object after creating the progress indicator.
Disadvantages
- Progress indicator must be aware of the concrete filler class
CLToRFiller
, for providing the default behavior. This increases the coupling between theCProgressIndiator
andCLToRFiller
classes. - Progress indicator has control only on the lifetime of the default filler object, which is
CLToRFiller
object in this case. But, it is not having any control on the scope or the lifetime of other filler class objects.
Strategy as a Template Class Parameter to the Context
If there is no need to change the filler class (Strategy
) at run time, it can be passed as a template parameter to the progress indicator (Context
) class at compile time. Listing 3 shows this approach. Active Template Library (ATL) uses a variation of this approach to select the required CCOMObjectxxx<>
in which the Context
is passed as a parameter to the Strategy
class (Pull
method). See ATL and Strategy Pattern for explanation.
Advantages
- Progress indicator template class is instantiated only with concrete filler classes, so there is no need for the
abstract CFiller
class. - Passing filler class as a template parameter provides an early binding between the progress indicator and the filler classes. This avoids runtime overhead and increases the efficiency.
- Progress indicator is responsible for the creation of the filler class object. Therefore, it has full control on the lifetime of the object.
Disadvantages
Selecting filler class at compile time provides no room for changing the object at runtime.
Benefits of Using Strategy Pattern
- A family of algorithms can be defined as a class hierarchy and can be used interchangeably to alter application behavior without changing its architecture.
- By encapsulating the algorithm separately, new algorithms complying with the same interface can be easily introduced.
- The application can switch strategies at run-time.
Strategy
enables the clients to choose the required algorithm, without using a "switch
" statement or a series of "if
-else
" statements.- Data structures used for implementing the algorithm are completely encapsulated in
Strategy
classes. Therefore, the implementation of an algorithm can be changed without affecting theContext
class. Strategy
Pattern can be used instead of sub-classing theContext
class. Inheritance hardwires the behavior with theContext
and the behavior cannot be changed dynamically.- The same
Strategy
object can be strategically shared between differentContext
objects. However, the sharedStrategy
object should not maintain states across invocations.
Drawbacks of Using Strategy Pattern
- The application must be aware of all the strategies to select the right one for the right situation.
Strategy
andContext
classes may be tightly coupled. TheContext
must supply the relevant data to theStrategy
for implementing the algorithm and sometimes, all the data passed by theContext
may not be relevant to all the Concrete Strategies.Context
and theStrategy
classes normally communicate through the interface specified by theabstract Strategy
base class.Strategy
base class must expose interface for all the required behaviors, which some concreteStrategy
classes might not implement.- In most cases, the application configures the
Context
with the requiredStrategy
object. Therefore, the application needs to create and maintain two objects in place of one. - Since the
Strategy
object is created by the application in most cases; theContext
has no control on lifetime of theStrategy
object. However, theContext
can make a local copy of theStrategy
object. But, this increases the memory requirement and has a sure performance impact.
Known Uses
This section presents known uses of Strategy Pattern. Some of the known uses presented in this section are taken from the GoF book on Design Patterns.
ATL and Strategy Pattern
ATL stands for Active Template Library. It is a collection of template based classes intended to hide most of the complexities behind COM development and provide a small footprint for the component itself.
In ATL, the class of the COM object is not instantiated directly. It acts as a base class for a CComObjectxxx<>
class. For example, if CMyClass
is the COM object class, then the most derived class in the class hierarchy will be a CComObjectxxx<CMyClass>
. CComObjectxxx<>
provides the implementation of IUnknown
methods. However, these classes not only handle the basics of reference counting, but also interact appropriately with the lock count of the module. CComObjectxxx<>
classes differ slightly in their behavior and the choice of the CComObjectxxx<>
depends on the aggregation, locking and destruction models. These are generic and optional features that can be applied to any COM object. For example, some COM Objects can support aggregation, some may not and some may only support aggregation. This is again true with the other two features - locking and destruction. These features can be accommodated and easily switched around without changing the functionality of the COM object. CComObject<>
, CComAggObject<>
, CComObjectCached<>
, CComObjectNoLock<>
are some of CComObjectxxx<>
classes.
ATL uses the Strategy Pattern to encapsulate the behavior in different CComObjectxxx<>
classes and the COM class can select the required CComObjectxxx<>
based on the functionality needed. Since, there is no need to change a Strategy
at run-time; ATL uses C++ template for the implementation of the Strategy Pattern. ATL selects a Strategy
(CComObjectxxx<>)
and passes the Context
(CMyClass
) as a parameter to the Strategy
.
Java and Strategy Pattern
Strategy Pattern is also used in the implementation of the Layout Manager in Java. The Layout manager can be configured with a layout object, which can be an object of a FlowLayout
, a CardLayout
, a GridLayout
or a GridBagLayout
class. These classes encapsulate the algorithms for laying out visual components and they provide several different layouts for viewing the same visual widgets.
Other Known Uses
Borland's ObjectWindows
use strategies to encapsulate validation algorithms for dialog box entry fields. For example, a numeric field might have a validator to check proper range, a date field might have a validator to check the correctness of the input date and string
field might have a validator for proper syntax.
ET++ uses the Strategy Pattern to encapsulate layout algorithms for text viewers.
Strategy Pattern is also used in many popular sorting algorithms, graph layout algorithms and memory allocation algorithms.
Bridge and Strategy
Often, the Strategy Pattern is confused with the Bridge Pattern. Even though these two patterns are similar in structure, they are trying to solve two different design problems. Strategy
is mainly concerned in encapsulating algorithms, whereas Bridge
decouples the abstraction from the implementation, to provide different implementation for the same abstraction.
Summary
This article is all about the Strategy Pattern, it not only talked about what Strategy Pattern is, but also emphasized why and when it is needed. I have used this pattern in many of my projects including the implementation of Lexical Analyzer and Parser classes. This pattern can be applied wherever there are several different ways of performing the same task. In short, the Strategy Pattern can be used to encapsulate varying algorithms and use them to change the system behavior without altering its architecture.
Acknowledgments
Special thanks to my friend Sree Meenakshi for her helpful suggestions in improving the clarity and presentation of this article.
Listing 1 - Strategy Object as a Required Parameter to the Context
// Forward declaration for CFiller class
class CFiller;
// Class declaration for CProgressIndicator
class CProgressIndicator
{
// Method declarations
public:
CProgressIndicator(CFiller *);
INT SetPos(INT);
INT SetFiller(CFiller *);
…
…
// Data members
protected:
CFiller * m_pFiller;
};
// CProgressIndicator - Implementation
CProgressIndicator ::CProgressIndicator(CFiller * pFiller)
{
// Validate pFiller
ASSERT(pFiller != NULL);
m_pFiller = pFiller;
}
INT CProgressIndicator ::SetPos(INT nPos)
{
// Some initialization code before forwarding the request to filler object
…
…
// Request forwarding to filler object
INT nStatus = m_pFiller->DoFill(…);
…
…
return nStatus;
}
INT * CProgressIndicator ::SetFiller(CFiller * pFiller)
{
// Validate pFiller
ASSERT(pFiller != NULL);
// Set new filler object
m_pFiller = pFiller;
return 0;
}
Listing 2 - Strategy Object as an Optional Parameter to the Context
// Forward declaration for CFiller class
class CFiller;
// Class declaration for CProgressIndicator
class CProgressIndicator
{
// Method declarations
public:
CProgressIndicator(CFiller * = NULL);
virtual ~CProgressIndicator();
INT SetPos(INT);
INT SetFiller(CFiller *);
…
…
// Data members
protected:
CFiller * m_pFiller;
BOOL m_bCreated;
};
// CProgressIndicator - Implementation
CProgressIndicator ::CProgressIndicator(CFiller * pFiller)
{
// Check and create filler object
if(pFiller == NULL)
{
// Create a default Left to Right filler object
m_pFiller = new CLToRFiller;
m_bCreated = TRUE;
}
else
{
m_pFiller = pFiller;
m_bCreated = FALSE;
}
}
CProgressIndicator::~CProgressIndicator()
{
// Delete filler object, only if it is created by the progress indicator
if(m_bCreated == TRUE)
{
delete m_pFiller;
}
}
INT CProgressIndicator ::SetPos(INT nPos)
{
// Some initialization code before forwarding the request to CFiller object
ASSERT(m_pFiller != NULL);
…
…
// Request forwarding to CFiller object
INT nStatus = m_pFiller->DoFill(…);
…
…
return nStatus;
}
INT * CProgressIndicator ::SetFiller(CFiller * pFiller)
{
// Validate Filler object
ASSERT(pFiller != NULL);
// Delete filler object, only if it is created by the progress indicator
if(m_bCreated == TRUE)
{
delete m_pFiller;
m_bCreated = FALSE;
}
// Set new filler object
m_pFiller = pFiller;
return 0;
}
Listing 3 - Strategy as a Template Class Parameter
template <class TFiller> class CProgressIndicator
{
// Method declarations
public:
INT SetPos(INT);
…
…
// Data members
protected:
TFiller m_theFiller;
};
// CProgressIndicator - Implementation
INT CProgressIndicator ::SetPos(INT nPos)
{
// Some initialization code before forwarding the request to CFiller
// object
…
…
// Request forwarding to CFiller object
INT nStatus = m_theFiller.DoFill(…);
…
…
return nStatus;
}
// Application code using CProgressIndicator
CProgressIndicator<CLToRFiller> LtoRFillerObj;
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.