0

Fellows:

I am working on a collection of algorithms which act on classes like "Body", "Spacecraft", "Planet", etc. The construction of each instance can be accomplished by means of different back-ends. For example, I can calculate the planetary positions using a multitude of libraries (like NASA's SPICE system), and I can "calculate" the radius of a celestial body using also a multitude of data sources and libraries.

My algorithm collection should be oblivious to the data sources: for example, if I want to calculate the time of an eclipse, I only care about the relative position of the bodies, and their radius (regardless of where I got those numbers from).

In the code attached below I am using Policy classes to parameterize two different back-ends (simplified because this is an example). I am interested in asking the following questions:

  • Is it reasonable to use the policy pattern for this end?
  • How can I remove the implementation details contained in the class constructor and move them to the Pimpl constructor (parameterize BodyImpl?)

The code is a bit verbose, but I wanted to "talk through" my rationale.

Thank you.

I successfully compiled the code below using g++ 4.7.2 as follows:

g++ backends.cpp -std=c++11 -Wall -O2

(notice that it uses a few c++11 constructs, like auto).

/**
   Is it reasonable to parameterize different data back-ends using the
   Policy Pattern? 

   The goal is to provide a unified interface to different classes
   (e.g., `Body`, `Star`, `Spacecraft`). However, the construction of
   specific instances requires data which can originate from different
   sources.

   For example, a "Body" has a radius and a gravitational parameter
   (called "gm"). But these values can come from different sources
   (different libraries which provide this kind of information).

   Say that library 1 (called "Spice") is capable of providing the
   radius given the body name:

   double the_radius = compute_radius_with_spice("Mercury");

   On the other hand, you could be using another library, which
   computes the radius with a completely different interface, and with
   completely different requirements:

   double radii[3];
   compute_the_radius_with_another_library("Mercury", radii)
   double the_radius = (radii[0] + radii[1] + radii[2]) / 3.0;

   Of course, the values computed with either library are similar, but
   different enough to make a difference. What matters is CONSISTENCY
   (stick to one back-end).
*/


#include<iostream>
#include<string>
#include<vector>
#include<memory>

/* Say that this is the uniform interface that I want to provide.*/
template<typename DataPolicy>
class Body:
  private DataPolicy{
public: 
  Body(const std::string& name);
  Body(const Body& body);
  ~Body();
  std::string name() const;
  double radius() const;
  double gm() const;
private:
  class BodyImpl * pimpl_;;
  //  std::unique_ptr<BodyImpl> pimpl_;
};

/* I use the pimpl_ idiom to hide the implementation */
struct BodyImpl{
  std::string m_name;
  double m_radius;
  double m_gm;  
  BodyImpl(const std::string& name):
    m_name(name){    
  }
};

/* The constructor has to build the pimpl step by step using the data
   policy as a data source. */
template<typename DataPolicy>
Body<DataPolicy>::Body(const std::string& name):
  pimpl_(new BodyImpl(name)){
  pimpl_->m_radius = DataPolicy::get_radius(name);
  pimpl_->m_gm = DataPolicy::get_gm(name);
}

template<typename DataPolicy>
Body<DataPolicy>::Body(const Body& body):
  pimpl_(new BodyImpl(body.name())){
  pimpl_->m_radius = body.radius();
  pimpl_->m_gm = body.gm();
}

template<typename DataPolicy>
Body<DataPolicy>::~Body(){
  delete pimpl_;
  pimpl_ = 0;
}

/* The methods are simple forwarding calls to the implementation (in
   reality it is not as simple as returning a primitive data type)*/
template<typename DataPolicy>
std::string Body<DataPolicy>::name() const{
  return pimpl_->m_name;
}

template<typename DataPolicy>
double Body<DataPolicy>::radius() const{
  return pimpl_->m_radius;
}

template<typename DataPolicy>
double Body<DataPolicy>::gm() const{
  return pimpl_->m_gm;
}


/* Now I create a concrete data policy - in reality this would be more
   extensive and complex, but the idea remains the same */
struct SPICEDataPolicy{
  static double get_radius(const std::string& name){
    std::cout<<"SPICEDataPolicy: calculating radius for "<<name<<std::endl;
    return 0;
  }
  static double get_gm(const std::string& name){
    std::cout<<"SPICEDataPolicy: calculating gm for "<<name<<std::endl;
    return 0;
  }
};

/* This is another data policy - it provides the same data but it may
   call a completely different underlying library, and calculate the
   values using completely different logic */
struct OtherDataPolicy{
  static double get_radius(const std::string& name){
    std::cout<<"OtherDataPolicy: calculating radius for "<<name<<std::endl;
    return 0;
  }
  static double get_gm(const std::string& name){
    std::cout<<"OtherDataPolicy: calculating gm for "<<name<<std::endl;
    return 0;
  }
};


/* My algorithms can now use the objects via the unified interface */
template<typename T>
void individual_complex_calculation(const Body<T>& body){
  // Regardless of the body's data policy, I know I can call a uniform interface.
  std::cout<<"I am making a complex calculation involving "<<body.name()<<"."<<std::endl
       <<"[This is my radius: "<<body.radius()<<", "
       <<"and this is my gm: "<<body.gm()<<"]"<<std::endl;
}
template<typename T>
void complex_calculation(const std::vector<Body<T> > bodies){
  for(auto it=bodies.begin(), finished=bodies.end(); it!=finished; it++)
    individual_complex_calculation(*it);
}

int main(){  
  /* Now I can create a vector of bodies which are consistent with one
     another */
  std::cout<<"========== Using 'SPICEDataPolicy =========='"<<std::endl;
  std::vector<Body<SPICEDataPolicy> > bodies;
  bodies.push_back(Body<SPICEDataPolicy>("Mercury"));
  bodies.push_back(Body<SPICEDataPolicy>("Venus"));
  bodies.push_back(Body<SPICEDataPolicy>("Earth"));
  bodies.push_back(Body<SPICEDataPolicy>("Mars"));

  complex_calculation(bodies);

  /* And even create other set of bodies consistent with one another,
     but inconsistent with the previous ones.*/
  std::cout<<"========== Using 'OtherDataPolicy' =========="<<std::endl;  
  std::vector<Body<OtherDataPolicy> > other_bodies;
  other_bodies.push_back(Body<OtherDataPolicy>("Mercury"));
  other_bodies.push_back(Body<OtherDataPolicy>("Venus"));
  other_bodies.push_back(Body<OtherDataPolicy>("Earth"));
  other_bodies.push_back(Body<OtherDataPolicy>("Mars"));

  complex_calculation(other_bodies);
  return 0;
}

Output of ./a.out:

========== Using 'SPICEDataPolicy =========='
SPICEDataPolicy: calculating radius for Mercury
SPICEDataPolicy: calculating gm for Mercury
SPICEDataPolicy: calculating radius for Venus
SPICEDataPolicy: calculating gm for Venus
SPICEDataPolicy: calculating radius for Earth
SPICEDataPolicy: calculating gm for Earth
SPICEDataPolicy: calculating radius for Mars
SPICEDataPolicy: calculating gm for Mars
I am making a complex calculation involving Mercury.
[This is my radius: 0, and this is my gm: 0]
I am making a complex calculation involving Venus.
[This is my radius: 0, and this is my gm: 0]
I am making a complex calculation involving Earth.
[This is my radius: 0, and this is my gm: 0]
I am making a complex calculation involving Mars.
[This is my radius: 0, and this is my gm: 0]
========== Using 'OtherDataPolicy' ==========
OtherDataPolicy: calculating radius for Mercury
OtherDataPolicy: calculating gm for Mercury
OtherDataPolicy: calculating radius for Venus
OtherDataPolicy: calculating gm for Venus
OtherDataPolicy: calculating radius for Earth
OtherDataPolicy: calculating gm for Earth
OtherDataPolicy: calculating radius for Mars
OtherDataPolicy: calculating gm for Mars
I am making a complex calculation involving Mercury.
[This is my radius: 0, and this is my gm: 0]
I am making a complex calculation involving Venus.
[This is my radius: 0, and this is my gm: 0]
I am making a complex calculation involving Earth.
[This is my radius: 0, and this is my gm: 0]
I am making a complex calculation involving Mars.
[This is my radius: 0, and this is my gm: 0]

Edit:

I experimented another implementation based on "traits" and "policy". It feels much cleaner, but I am still curious about your take on it.

The following code compiles with the same command-line argument as above.

/**
   Multiple back-ends implemented as a mix of trait classes and policies.

   This seems to be a better implementation because there is a clear
   path to extend the different back-ends, and the class front-end is
   completely independent from its back-end.

*/

#include<iostream>
#include<string>
#include<vector>
#include<memory>

// forward declaration of the trait "data_traits"
template<typename T>
struct data_traits{
};

// each class would be defined as follows (with forward declaration of
// its implementation class)
template<typename T>
struct BodyImpl;

template<typename T>
class Body{
public:
  Body(const std::string& name);
  std::string name() const;
  double radius() const;
  double gm() const;
private:
  std::unique_ptr<BodyImpl<T> > pimpl_;
};

// each class would be implemented in a cpp file with the following
// structure (notice full independence from any back-end)
template<typename T>
struct BodyImpl{
  std::string m_name;
  double m_radius;
  double m_gm;
  BodyImpl(const std::string& name):
    m_name(name){
    m_radius = data_traits<T>::get_radius(name);
    m_gm = data_traits<T>::get_gm(name);
  }    
};

/* public interface simply forwards to pimpl */
template<typename T>
Body<T>::Body(const std::string& name):
  pimpl_(new BodyImpl<T>(name)){
}

template<typename T>
std::string Body<T>::name() const{
  return pimpl_->m_name;
}

template<typename T>
double Body<T>::radius() const{
  return pimpl_->m_radius;
}

template<typename T>
double Body<T>::gm() const{
  return pimpl_->m_gm;
}


/* the user or library writer can then write specific back-ends
   according to the following interfaces */
struct SPICEBackEnd;
template<> struct data_traits<SPICEBackEnd>{
  static double get_radius(const std::string& name){
    std::cout<<"[SPICE] get radius for "<<name<<std::endl;
    return 0;
  }
  static double get_gm(const std::string& name){
    std::cout<<"[SPICE] get gm for "<<name<<std::endl;
    return 0;
  }
};

/*another back-end*/
struct OtherBackEnd;
template<> struct data_traits<OtherBackEnd>{
  static double get_radius(const std::string& name){
    std::cout<<"[OTHER] get radius for "<<name<<std::endl;
    return 0;
  }
  static double get_gm(const std::string& name){
    std::cout<<"[OTHER] get gm for "<<name<<std::endl;
    return 0;
  }
};

/* The algorithms can be obvlivious to the back-end used */
template<typename T>
void complex_calculation(const std::vector<Body<T> >& bodies){
  for(auto it=bodies.begin(), finished=bodies.end(); it!=finished; it++){
    std::cout<<"Body "<<it->name()<<" (r="<<it->radius()<<", mu="<<it->gm()<<")"<<std::endl;
  }
}


int main(){
  std::vector<Body<SPICEBackEnd> > spice_bodies;
  spice_bodies.push_back(Body<SPICEBackEnd>("Mercury"));
  spice_bodies.push_back(Body<SPICEBackEnd>("Venus"));
  spice_bodies.push_back(Body<SPICEBackEnd>("Earth"));
  spice_bodies.push_back(Body<SPICEBackEnd>("Mars"));

  complex_calculation(spice_bodies);

  std::vector<Body<OtherBackEnd> > other_bodies;
  other_bodies.push_back(Body<OtherBackEnd>("Mercury"));
  other_bodies.push_back(Body<OtherBackEnd>("Venus"));
  other_bodies.push_back(Body<OtherBackEnd>("Earth"));
  other_bodies.push_back(Body<OtherBackEnd>("Mars"));

  complex_calculation(other_bodies);
}
Escualo
  • 40,844
  • 23
  • 87
  • 135

1 Answers1

1

Interesting problem.

There are a bunch of different approaches that are worth considering, perhaps the one that jumps out the most is Abstract Factory. Why? Because you can construct a family of objects that conform to a basic set of interfaces and then use them without constantly checking to see what you should be doing. Also, because you made the point about consistency.

The problem I see with Strategy is that its generally a way of encapsulating different ways of doing the same thing. For instance, if we were doing payroll, everyone has a system that agrees that taxable wages are gross - deductions, but how that value is derived could be different (frankly, in payroll, Abstract Factory might make sense as well, as you will no doubt need more than one variant, and once you have drafted a variant, its essential that all the others are from the same family).

The other element here that's interesting design-wise is that you have a need to compute some common metrics on some very different entities. This is one of the great advantages of Interfaces in Java, and/or Traits in languages like Objective-C or Scala (from the ludicrously brilliant Self). I have not written a lot of C++ in quite a while, but I know there are ways to do something like traits, e.g. Mixins (a la James Coplien).

Community
  • 1
  • 1
Rob
  • 11,446
  • 7
  • 39
  • 57
  • Thank you, Rob - I've considered the Abstract Factory Pattern, but it somewhat did not seem as easy to extend (I need to provide methods to build every kind of object in the "sample" I wrote). The traits method is closer (see edited answer). – Escualo Jan 24 '13 at 16:01
  • 1
    Yeah that looks good, @Arrieta. BTW, there is some interesting additional info here: http://www.cantrip.org/traits.html but looks like maybe you saw it. I am not totally clear on what you are saying about Abstract Factory. You could have a bunch of private methods in the abstract versions of the class, then the overrides just manipulate them in different ways. Frankly, if there are not real functional differences, then what you need is an Adapter. – Rob Jan 24 '13 at 17:39