October 25, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Strategy Pattern: A Generic Programming Perspective

  • April 28, 2006
  • By Radu Braniste
  • Send Email »
  • More Articles »

A very interesting Java newsletter1 was the starting point of this article. After a swift presentation of a classic Strategy Pattern2, a TaxPayer Context paired with a TaxStrategy hierarchy, the newsletter tries to answer a more difficult problem: What happens if the Context becomes a hierarchy? How do you keep track of the real type of the TaxPayer inside TaxStrategy without downcasting?

You'll find several answers in this article—from a Generic Programming perspective.

What's the Buzz?

Consider the popular TaxPayer, paying unpopular taxes as dictated by TaxStrategies3:

struct TaxStrategy{
   virtual void payTax ()=0;
};
struct TaxPayer{
   TaxStrategy strategy_;
   void payTax(){ strategy_.payTax();}
};
struct CompanyTaxStrategy  : TaxStrategy{};
struct EmployeeTaxStrategy : TaxStrategy{};

In the real world, there is a family of TaxPayers, different enough to be hard to abstract behind an unique interface. Most probably, you'll find TaxPayers in a loose hierarchy (if any at all)—Employees, Companies, a.s.o. TaxStrategy has to take this diversity into account—companies are not taxed the same way as their employees and employees expose different attributes than companies.

You have to make TaxStrategy aware of the different TaxPayers. One way to accomplish this is to pass TaxPayer as a parameter 1, 2.

struct Company: TaxPayer{
   void payTax(){ strategy_.payTax(*this);}
};

If you like downcasts, this code is for you:

CompanyTaxStrategy : TaxStrategy{
   void payTax (const TaxPayer& t){
      if (Company c = dynamic_cast<Company&>(t)){...}
   }
};

If not, you have to look at an alternative.

One obvious route is to use the Visitor pattern that offers double dispatching out of the box; this will be investigated in a future article4.

On the other hand, Reference 1 describes a non-trivial solution based on generics. You'll try to match that implementation and even to improve it by using C++ templates and generic programming techniques. This has been done in a couple of steps, presented below.

Creating an Equivalent Solution (crtp.h)

A C++ replica of Reference 1 can be easily constructed by using the CRTP (The Curiously Recurring Template Pattern5). The payTax() mechanism is factored out in a TaxPayer base class:

namespace crtp
{
   template <class TAX_PAYER>
   struct TaxStrategy;

   template <class TAX_PAYER>
   struct TaxPayer
   {
      TaxPayer( TaxStrategy<TAX_PAYER>* strategy) :
         strategy_(strategy) {  }
      void payTax()
      {
         TAX_PAYER& tp = static_cast< TAX_PAYER&>(*this);
         strategy_->payTax(tp);
      }
   private:
      TaxStrategy<TAX_PAYER>* strategy_;
   };
   struct Company : TaxPayer<Company>
   {
      Company( TaxStrategy<Company>* strategy) :
         TaxPayer<Company>(strategy){  }
      void only4companies() const {};
   };
   struct Employee : TaxPayer<Employee>
   {
      Employee( TaxStrategy<Employee>* strategy) :
         TaxPayer<Employee>(strategy){  }
      void only4employees() const {};
   };
   template <class TAX_PAYER>
   struct TaxStrategy
   {
      void payTax(const TAX_PAYER& e)
      {
         std::cout << "generic payTax n" ;
      }
   };
   template <>
   struct TaxStrategy<Employee>
   {
      void payTax(const Employee& e)
      {
         e.only4employees();
         std::cout << "payTax Employeen" ;
      }
   };
   template<>
   struct TaxStrategy<Company>
   {
      void payTax(const Company& e)
      {
         e.only4companies();
         std::cout << "payTax Companyn" ;
      }
   };
   void test()
   {
      TaxStrategy<Employee> tsEmp;
      TaxPayer<Employee> e(&tsEmp);
      e.payTax();
      TaxStrategy<Company> tsComp;
      TaxPayer<Company> c(&tsComp);
      c.payTax(); 
   }
}

Note how the downcast is avoided in this case: payTax is overloaded and CRTP helps call the correct version.

But, the resulting code is far from stellar, for the following reasons:

  1. TaxPayer is a type-dependent base class and cannot be used in generic sequences such as:

    for (std::vector<TaxPayer>::iterator it = taxPayers.begin();
       it != taxPayers.end(); ++it)
    {
       it->payTax();
    }
    

    This works in Java due to raw types (but take into account that "The use of raw types in code written after the introduction of genericity into the Java programming language is discouraged. According to the Java Language Specification, it is possible that future versions of the Java programming language will disallow the use of raw types"6) , but not in C++.

  2. TaxStrategy should be a variation point (TaxStrategies of different types should be pluggable in taxPayer). This is not possible in the previous example, but is possible in Reference 1 where TaxStrategy is an interface allowing further customization in derived classes.
  3. TaxStrategy<T> are independent, unrelated classes. TaxStrategy should be seen as a family of algorithms more than anything else. (It can be a functor or a simple function as well.)

Improving the Equivalent Solution (virtual_crtp.h)

1 & 2 can be easily fixed by using abstract base classes:

template <class TAX_PAYER>
struct ITaxStrategy
{
   virtual void payTax(TAX_PAYER*) = 0;
};
struct ITaxPayer
{
   virtual void payTax() = 0;
};

and code like this is now possible:

void test()
{
   TaxStrategy<Employee> tsEmp;
   TaxPayer<Employee> e(&tsEmp);
   e.payTax();
   TaxStrategy1<Company> tsComp;
   TaxPayer<Company> c(&tsComp);
   c.payTax();
    
   std::vector<ITaxPayer*> taxPayers;
   taxPayers.push_back(&e);
   taxPayers.push_back(&c);
   for (std::vector<ITaxPayer*>::iterator it = taxPayers.begin();
      it != taxPayers.end(); ++it)
   {
      (*it)->payTax();
   }
}

This brings you on par with Reference 1 in terms of functionality, but the solution is suboptimal. As noted in Reference 7: "Operation polymorphism (keyword virtual in C++) is excluded [in parametric polymorphism; in other words, (compile-time) genericity] because dynamic binding is too expensive. Or, saying it differently, abstract methods are forbidden."

Note: This is not always possible when mixing the two types of polymorphism or is not desirable—it adds complexity.




Page 1 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel