2013-01-23 28 views
0

研究員:啓用多個後端經由策略(戰略)模式

我的工作的,其作用於類,如「身體」,「航天器」,「星球」等各建設算法的集合實例可以通過不同的後端來完成。例如,我可以使用多種庫(如NASA的SPICE系統)來計算行星位置,並且我還可以使用多種數據源和庫來「計算」一個天體的半徑。我的算法集合應該忽略數據源:例如,如果我想計算日食的時間,我只關心物體的相對位置以及它們的半徑(無論我在哪裏得到這些數據數字來自)。

在下面的代碼中,我使用策略類來對兩個不同的後端進行參數化(簡化,因爲這是一個示例)。我有興趣提出以下問題:

  • 爲此目的使用策略模式是否合理?
  • 我怎樣可以刪除包含在類的構造函數的實現細節,並將其移動到平普爾構造函數(參數BodyImpl?)

的代碼是一個有點冗長,但我想我的理由「通過交談」 。

謝謝。

予編譯成功下面使用克++ 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] 

編輯:

我嘗試基於 「特徵」 和 「政策」 另一種實現方式。它感覺更清潔,但我仍然對你的接受感到好奇。

下面的代碼使用與上面相同的命令行參數進行編譯。

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

回答

1

有趣的問題。

有許多不同的方法值得考慮,也許跳出最多的方法是Abstract Factory。爲什麼?因爲你可以構造一系列符合一組基本接口的對象,然後使用它們而不需要經常檢查,看看你應該做什麼。另外,因爲你提出了一致性的觀點。

我在策略中看到的問題是,它通常是封裝不同方式來做同樣事情的一種方式。例如,如果我們在做薪水,每個人都有一個同意應納稅工資是毛額扣除的系統,但是如何得出這個價值可能是不同的(坦率地說,在工資單中,抽象工廠也可能有意義,因爲您不會懷疑需要一個以上的變體,一旦你起草了一個變體,它的基本要素是所有其他變體都來自同一個家族)。

這裏有趣的設計方面的另一個元素是,您需要計算一些非常不同的實體上的一些常用指標。這是Java中的接口和/或Objective-C或Scala等語言中的特性(來自可笑的自我)的巨大優勢之一。我在一段時間內沒有寫很多C++,但我知道有些方法可以做一些特性,比如Mixins(a James Coplien)。

+0

謝謝,Rob - 我已經考慮過抽象工廠模式,但它有點不像擴展那麼容易(我需要提供方法來構建我寫的「樣本」中的每種對象)。特質方法更接近(見編輯答案)。 – Escualo

+1

是啊,看起來不錯,@阿列塔。順便說一句,這裏有一些有趣的附加信息:http://www.cantrip.org/traits.html但看起來也許你看到它。我不完全清楚你對抽象工廠的看法。你可以在類的抽象版本中使用一些私有方法,然後覆蓋只是以不同的方式來操作它們。坦率地說,如果沒有真正的功能差異,那麼你需要的是一個適配器。 – Rob