Applying Strategy Pattern in C++ Applications

 Applying Strategy Pattern in C++ ApplicationsWhen 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

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.

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

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

1. Application can specify the filler object only when it needs to change the default filler object.
2. 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.

1. Progress indicator must be aware of the concrete filler class CLToRFiller, for providing the default behavior. This increases the coupling between the CProgressIndiator and CLToRFiller classes.
2. 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.

1. Progress indicator template class is instantiated only with concrete filler classes, so there is no need for the abstract CFiller class.
2. 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.
3. Progress indicator is responsible for the creation of the filler class object. Therefore, it has full control on the lifetime of the object.

Selecting filler class at compile time provides no room for changing the object at runtime.

Benefits in using Strategy Pattern

1. A family of algorithms can be defined as a class hierarchy and can be used interchangeably to alter application behavior without changing its architecture.
2. By encapsulating the algorithm separately, new algorithms complying with the same interface can be easily introduced.
3. The application can switch strategies at run-time.
4. Strategy enables the clients to choose the required algorithm, without using a "switch" statement or a series of "if-else" statements.
5. Data structures used for implementing the algorithm is completely encapsulated in Strategy classes. Therefore, the implementation of an algorithm can be changed without affecting the Context class.
6. Strategy Pattern can be used instead of sub-classing the Context class. Inheritance hardwires the behavior with the Context and the behavior cannot be changed dynamically.
7. The same Strategy object can be strategically shared between different Context objects. However, the shared Strategy object should not maintain states across invocations.

Drawbacks in using Strategy Pattern

1. The application must be aware of all the strategies to select the right one for the right situation.
2. Strategy and Context classes may be tightly coupled. The Context must supply the relevant data to the Strategy for implementing the algorithm and sometimes, all the data passed by the Context may not be relevant to all the Concrete Strategies.
3. Context and the Strategy classes normally communicate through the interface specified by the abstract Strategy base class. Strategy base class must expose interface for all the required behaviors, which some concrete Strategy classes might not implement.
4. In most cases, the application configures the Context with the required Strategy object. Therefore, the application needs to create and maintain two objects in place of one.
5. Since, the Strategy object is created by the application in most cases; the Context has no control on lifetime of the Strategy object. However, the Context can make a local copy of the Strategy 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 uses 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;

0 0