0

フェロー:

「ボディ」、「宇宙船」、「惑星」などのクラスに作用するアルゴリズムのコレクションに取り組んでいます。各インスタンスの構築は、さまざまなバックエンドによって実現できます。たとえば、多数のライブラリ (NASA の SPICE システムなど) を使用して惑星の位置を計算したり、多数のデータ ソースとライブラリを使用して天体の半径を「計算」したりできます。

私のアルゴリズム コレクションは、データ ソースに気付かないようにする必要があります。たとえば、日食の時間を計算したい場合、天体の相対位置と半径だけを気にします (これらの数値をどこから取得したかに関係なく)。

以下に添付されたコードでは、Policy クラスを使用して 2 つの異なるバックエンドをパラメーター化しています (これは例であるため簡略化しています)。次の質問に興味があります。

  • この目的のためにポリシーパターンを使用することは合理的ですか?
  • クラスコンストラクターに含まれる実装の詳細を削除して、それらを Pimpl コンストラクターに移動するにはどうすればよいですか (BodyImpl をパラメーター化しますか?)

コードは少し冗長ですが、私の理論的根拠を「説明」したかったのです。

ありがとうございました。

次のように、g++ 4.7.2 を使用して以下のコードを正常にコンパイルしました。

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

( のようないくつかの c++11 コンストラクトを使用していることに注意してください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;
}

の出力./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]

編集:

「traits」と「policy」に基づく別の実装を実験しました。それはずっときれいに感じますが、私はまだあなたの考えに興味があります.

次のコードは、上記と同じコマンドライン引数でコンパイルされます。

/**
   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);
}
4

1 に答える 1

1

興味深い問題です。

検討する価値のあるさまざまなアプローチがたくさんありますが、おそらく最も飛び出すのはAbstract Factoryです。なんで?インターフェイスの基本セットに準拠するオブジェクトのファミリを構築し、何をすべきかを常にチェックすることなくそれらを使用できるためです。また、一貫性について指摘したからです。

私が Strategy で見ている問題は、それが一般的に、同じことを行うさまざまな方法をカプセル化する方法であるということです。たとえば、私たちが給与計算を行っている場合、誰もが課税対象の賃金が総控除であることに同意するシステムを持っていますが、その価値がどのように導出されるかは異なる可能性があります (率直に言って、給与計算では、Abstract Factory も理にかなっている可能性があります。間違いなく複数のバリアントが必要であり、バリアントをドラフトしたら、他のすべてが同じファミリーからのものであることが不可欠です)。

ここで設計上興味深いもう 1 つの要素は、いくつかの非常に異なるエンティティで共通のメトリックを計算する必要があることです。これは、Java の Interfaces や、Objective-C や Scala などの言語の Traits の大きな利点の 1 つです (滑稽なほど素晴らしい Self から)。私はかなり長い間多くの C++ を書いていませんが、 Mixins (James Coplien 風)など、トレイトのようなことを行う方法があることは知っています。

于 2013-01-23T22:50:37.740 に答える