2010-02-28 20 views
6

Diciamo che abbiamo già una gerarchia di classi, ad es.Aggiunta di funzioni virtuali senza modificare le classi originali

class Shape { virtual void get_area() = 0; }; 
class Square : Shape { ... }; 
class Circle : Shape { ... }; 
etc. 

Ora diciamo che voglio (efficacemente) aggiungere un metodo virtual draw() = 0-Shape con le definizioni appropriate in ogni sottoclasse. Tuttavia,, diciamo che voglio farlo senza modificare quelle classi (dato che fanno parte di una libreria che non voglio modificare).

Quale sarebbe il modo migliore per farlo?

Se o meno effettivamente "aggiungere" un metodo virtual o non è importante, voglio solo un comportamento polimorfico dato un array di puntatori.

Il mio primo pensiero sarebbe quello di fare questo:

class IDrawable { virtual void draw() = 0; }; 
class DrawableSquare : Square, IDrawable { void draw() { ... } }; 
class DrawableCircle : Circle, IDrawable { void draw() { ... } }; 

e poi basta sostituire tutte le creazioni di Square s e Circle s con DrawableSquare s e DrawableCircle s, rispettivamente.

È il modo migliore per eseguire questa operazione o c'è qualcosa di meglio (preferibilmente qualcosa che lascia intatta la creazione di Square e Circle).

Grazie in anticipo.

risposta

5

(I do proporre ulteriormente verso il basso una soluzione ... orso con me ...)

Un modo per (quasi) a risolvere il problema è quello di utilizzare un modello di progettazione visitatore. Qualcosa di simile a questo:

class DrawVisitor 
{ 
public: 
    void draw(const Shape &shape); // dispatches to correct private method 
private: 
    void visitSquare(const Square &square); 
    void visitCircle(const Circle &circle); 
}; 

Poi invece di questo:

Shape &shape = getShape(); // returns some Shape subclass 
shape.draw(); // virtual method 

si dovrebbe fare:

DrawVisitor dv; 
Shape &shape = getShape(); 
dv.draw(shape); 

Normalmente in un modello visitatore si implementa il metodo draw in questo modo:

DrawVisitor::draw(const Shape &shape) 
{ 
    shape.accept(*this); 
} 

Ma ciò funziona solo se la gerarchia Shape è stata progettata per essere visitata: ciascuna sottoclasse implementa il metodo virtuale accept chiamando il metodo appropriato visitXxxx sul visitatore. Molto probabilmente non è stato progettato per questo.

Senza essere in grado di modificare la gerarchia delle classi per aggiungere un metodo virtuale accept-Shape (e tutte le sottoclassi), avete bisogno di qualche altro modo che vengano avviati al metodo corretto draw. Un approccio naieve è questa:

DrawVisitor::draw(const Shape &shape) 
{ 
    if (const Square *pSquare = dynamic_cast<const Square *>(&shape)) 
    { 
    visitSquare(*pSquare); 
    } 
    else if (const Circle *pCircle = dynamic_cast<const Circle *>(&shape)) 
    { 
    visitCircle(*pCircle); 
    } 
    // etc. 
} 

che funzionerà, ma v'è un calo di prestazioni a usare dynamic_cast in quel modo. Se potete permettervi quel colpo, si tratta di un approccio semplice che è facile da capire, eseguire il debug, mantenere, ecc

Supponiamo che ci fosse un'enumerazione di tutti i tipi di forme:

enum ShapeId { SQUARE, CIRCLE, ... }; 

e c'era un virtuale metodo ShapeId Shape::getId() const = 0; che ciascuna sottoclasse sovrascrivere per restituire il valore ShapeId. Quindi puoi eseguire la tua spedizione utilizzando una massiccia affermazione switch anziché l'if-elsif-elsif di dynamic_cast s. O forse invece di un switch usa una tabella hash. Lo scenario migliore consiste nel mettere questa funzione di mappatura in un unico posto, in modo da poter definire più visitatori senza dover ripetere la logica di mappatura ogni volta.

Quindi probabilmente non si dispone di un metodo getid(). Peccato. Qual è un altro modo per ottenere un ID che è unico per ogni tipo di oggetto? RTTI. Questo non è necessariamente elegante o infallibile, ma è possibile creare una tabella hash dei puntatori type_info. Puoi costruire questo hashtable in qualche codice di inizializzazione o costruirlo dinamicamente (o entrambi).

DrawVisitor::init() // static method or ctor 
{ 
    typeMap_[&typeid(Square)] = &visitSquare; 
    typeMap_[&typeid(Circle)] = &visitCircle; 
    // etc. 
} 

DrawVisitor::draw(const Shape &shape) 
{ 
    type_info *ti = typeid(shape); 
    typedef void (DrawVisitor::*VisitFun)(const Shape &shape); 
    VisitFun visit = 0; // or default draw method? 
    TypeMap::iterator iter = typeMap_.find(ti); 
    if (iter != typeMap_.end()) 
    { 
    visit = iter->second; 
    } 
    else if (const Square *pSquare = dynamic_cast<const Square *>(&shape)) 
    { 
    visit = typeMap_[ti] = &visitSquare; 
    } 
    else if (const Circle *pCircle = dynamic_cast<const Circle *>(&shape)) 
    { 
    visit = typeMap_[ti] = &visitCircle; 
    } 
    // etc. 

    if (visit) 
    { 
    // will have to do static_cast<> inside the function 
    ((*this).*(visit))(shape); 
    } 
} 

Potrebbe esserci qualche errore di bug/sintassi, non ho provato a compilare questo esempio. Ho già fatto qualcosa del genere - la tecnica funziona. Non sono sicuro che potresti incontrare problemi con le librerie condivise.

Un'ultima cosa io aggiungo: a prescindere da come si decide di fare la spedizione, probabilmente ha senso fare una classe di base visitatore:

class ShapeVisitor 
{ 
public: 
    void visit(const Shape &shape); // not virtual 
private: 
    virtual void visitSquare(const Square &square) = 0; 
    virtual void visitCircle(const Circle &circle) = 0; 
}; 
+0

Intendi 'visitCircle (const Circle & circle)' piuttosto che visitareSquare lì? –

+0

@Philip: oops ... corretto. – Dan

+0

Soluzione interessante, mi piace usare solo parte del pattern "Visitor' pattern >> per adattarsi alla situazione, e non viceversa :) –

3

Quello che stai descrivendo è un po 'come lo decorator pattern. Che è molto adatto per modificare il comportamento di runtime delle classi esistenti.

Ma io non vedo proprio come implementare il vostro esempio pratico, se le forme non hanno modo di trarre, quindi non c'è modo per cambiare il comportamento di disegno in fase di esecuzione o ...

ma suppongo che questo è solo un esempio molto semplificato per StackOverflow? Se sono disponibili tutti i blocchi di base per la funzionalità desiderata, implementare il comportamento di runtime esatto con tale modello è sicuramente un'opzione decente.

+0

Questo non è un esempio semplificato per SO. Ho letteralmente forme che voglio aggiungere a metodi di disegno. Nello specifico, le forme provengono dalle forme di collisione dell'SDK di Bullet Physics e voglio essere in grado di aggiungere funzionalità di disegno a scopo di debug. –

+0

Correggimi se sbaglio, ma non penso che il decoratore risolva questo problema. Decorator aggiunge un nuovo albero di comportamento alle classi esistenti, anziché aggiungere un comportamento all'albero esistente. –

+0

Sembra che il Bullet SDK abbia un'interfaccia 'btIDebugDraw': http://bulletphysics.com/Bullet/BulletFull/classbtIDebugDraw.html. Derivate da una classe che implementa funzioni come 'drawBox()' e 'drawSphere()', quindi assegnatela a 'btCollisionWorld' o' btDynamicsWorld', e poi disegnate quel mondo. Farebbe quello che ti serve? –

0

One 'off the wall' soluzione di come si potrebbe considerare, a seconda delle circostanze, usare i modelli per darti un comportamento polimorfico del tempo di compilazione. Prima di dire qualsiasi cosa, io so che questo non vi darà tradizionale polimorfismo runtime quindi potrebbe anche non essere utile, ma a seconda delle limitazioni dell'ambiente in cui si sta lavorando, può rivelarsi utile:

#include <iostream> 

using namespace std; 

// This bit's a bit like your library. 
struct Square{}; 
struct Circle{}; 
struct AShape{}; 

// and this is your extra stuff. 
template < class T > 
class Drawable { public: void draw() const { cout << "General Shape" << endl; } }; 

template <> void Drawable<Square>::draw() const { cout << "Square!" << endl; }; 
template <> void Drawable<Circle>::draw() const { cout << "Circle!" << endl; }; 

template < class T > 
void drawIt(const T& obj) 
{ 
    obj.draw(); 
} 

int main(int argc, char* argv[]) 
{ 
    Drawable<Square> a; 
    Drawable<Circle> b; 
    Drawable<AShape> c; 

    a.draw(); // prints "Square!" 
    b.draw(); // prints "Circle!" 
    c.draw(); // prints "General Shape" as there's no specific specialisation for an Drawable<AShape> 

    drawIt(a); // prints "Square!" 
    drawIt(b); // prints "Circle!" 
    drawIt(c); // prints "General Shape" as there's no specific specialisation for an Drawable<AShape> 
} 

Il metodo drawIt() è probabilmente la chiave qui perché rappresenta un comportamento generico per qualsiasi classe che soddisfa il requisito di avere un metodo draw(). Fai attenzione però al codice gonfiare anche se il compilatore creerà un'istanza per un metodo separato per ogni tipo passato.

Questo può essere utile in situazioni in cui è necessario scrivere una funzione per lavorare su molti tipi che non hanno una classe base comune. Sono consapevole che questa non è la domanda che hai chiesto, ma ho pensato di buttarla solo come alternativa.

Problemi correlati